From 5e665cc67a619ac99757f2c2a13a242301252672 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 13 Jan 2015 13:39:06 -0700 Subject: [PATCH 001/485] Update dependencies in setup.py --- setup.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 5df7944d..2e7541fb 100644 --- a/setup.py +++ b/setup.py @@ -40,13 +40,10 @@ setup( license="GNU General Public License v3 (GPLv3)", tests_require=["pytest"], cmdclass={"test": PyTest}, - author="Jeremy Grossmann", - author_email="package-maintainer@gns3.net", - description="GNS3 server to asynchronously manage emulators", + description="GNS3 server", long_description=open("README.rst", "r").read(), install_requires=[ - "tornado>=3.1", - "pyzmq>=14.0.0", + "aiohttp", "jsonschema>=2.3.0", "apache-libcloud>=0.14.1", "requests", @@ -73,6 +70,7 @@ setup( "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", "Programming Language :: Python :: Implementation :: CPython", ], ) From 61344a16690fb50f8e73d75e44d4bb46e297b31f Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 13 Jan 2015 17:05:26 -0700 Subject: [PATCH 002/485] New base server. --- docs/Makefile | 177 +++++ .../vpcs/adapters => docs}/__init__.py | 0 docs/api/examples/get_version.txt | 17 + docs/api/examples/post_version.txt | 19 + docs/api/examples/post_vpcs.txt | 21 + docs/api/examples/post_vpcsvpcsidnio.txt | 28 + docs/api/sleep.rst | 13 + docs/api/stream.rst | 13 + docs/api/version.rst | 28 + docs/api/vpcs.rst | 46 ++ docs/api/vpcsvpcsid.rst | 63 ++ docs/api/vpcsvpcsidnio.rst | 127 ++++ docs/conf.py | 260 +++++++ docs/development.rst | 33 + docs/general.rst | 12 + docs/index.rst | 18 + docs/make.bat | 242 +++++++ gns3server/__init__.py | 12 +- gns3server/builtins/server_version.py | 37 - gns3server/config.py | 2 +- gns3server/handlers/file_upload_handler.py | 2 + gns3server/handlers/jsonrpc_websocket.py | 204 ------ gns3server/handlers/version_handler.py | 33 +- gns3server/handlers/vpcs_handler.py | 85 +++ gns3server/jsonrpc.py | 184 ----- gns3server/main.py | 23 +- gns3server/module_manager.py | 117 ---- gns3server/modules/__init__.py | 17 +- gns3server/modules/base.py | 355 ++-------- gns3server/modules/old_vpcs/__init__.py | 652 ++++++++++++++++++ .../nios => old_vpcs/adapters}/__init__.py | 0 .../{vpcs => old_vpcs}/adapters/adapter.py | 0 .../adapters/ethernet_adapter.py | 0 gns3server/modules/old_vpcs/nios/__init__.py | 0 .../{vpcs => old_vpcs}/nios/nio_tap.py | 0 .../{vpcs => old_vpcs}/nios/nio_udp.py | 0 .../modules/{vpcs => old_vpcs}/schemas.py | 0 .../modules/{vpcs => old_vpcs}/vpcs_device.py | 0 .../modules/{vpcs => old_vpcs}/vpcs_error.py | 0 gns3server/modules/vpcs/__init__.py | 639 +---------------- gns3server/schemas/__init__.py | 0 gns3server/{_compat.py => schemas/version.py} | 34 +- gns3server/schemas/vpcs.py | 270 ++++++++ gns3server/server.py | 378 +++------- gns3server/start_server.py | 2 +- gns3server/version.py | 13 +- gns3server/web/__init__.py | 0 gns3server/web/documentation.py | 131 ++++ gns3server/web/response.py | 41 ++ gns3server/web/route.py | 113 +++ setup.py | 2 +- 51 files changed, 2630 insertions(+), 1833 deletions(-) create mode 100644 docs/Makefile rename {gns3server/modules/vpcs/adapters => docs}/__init__.py (100%) create mode 100644 docs/api/examples/get_version.txt create mode 100644 docs/api/examples/post_version.txt create mode 100644 docs/api/examples/post_vpcs.txt create mode 100644 docs/api/examples/post_vpcsvpcsidnio.txt create mode 100644 docs/api/sleep.rst create mode 100644 docs/api/stream.rst create mode 100644 docs/api/version.rst create mode 100644 docs/api/vpcs.rst create mode 100644 docs/api/vpcsvpcsid.rst create mode 100644 docs/api/vpcsvpcsidnio.rst create mode 100644 docs/conf.py create mode 100644 docs/development.rst create mode 100644 docs/general.rst create mode 100644 docs/index.rst create mode 100644 docs/make.bat delete mode 100644 gns3server/builtins/server_version.py delete mode 100644 gns3server/handlers/jsonrpc_websocket.py create mode 100644 gns3server/handlers/vpcs_handler.py delete mode 100644 gns3server/jsonrpc.py delete mode 100644 gns3server/module_manager.py create mode 100644 gns3server/modules/old_vpcs/__init__.py rename gns3server/modules/{vpcs/nios => old_vpcs/adapters}/__init__.py (100%) rename gns3server/modules/{vpcs => old_vpcs}/adapters/adapter.py (100%) rename gns3server/modules/{vpcs => old_vpcs}/adapters/ethernet_adapter.py (100%) create mode 100644 gns3server/modules/old_vpcs/nios/__init__.py rename gns3server/modules/{vpcs => old_vpcs}/nios/nio_tap.py (100%) rename gns3server/modules/{vpcs => old_vpcs}/nios/nio_udp.py (100%) rename gns3server/modules/{vpcs => old_vpcs}/schemas.py (100%) rename gns3server/modules/{vpcs => old_vpcs}/vpcs_device.py (100%) rename gns3server/modules/{vpcs => old_vpcs}/vpcs_error.py (100%) create mode 100644 gns3server/schemas/__init__.py rename gns3server/{_compat.py => schemas/version.py} (59%) create mode 100644 gns3server/schemas/vpcs.py create mode 100644 gns3server/web/__init__.py create mode 100644 gns3server/web/documentation.py create mode 100644 gns3server/web/response.py create mode 100644 gns3server/web/route.py 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", From 6d20a122f816d1de871361b51534895bc8ccf946 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 13 Jan 2015 17:26:24 -0700 Subject: [PATCH 003/485] Adds tests and documentation script. --- requirements.txt | 3 - scripts/documentation.sh | 27 +++++++ tests/api/__init__.py | 0 tests/api/base.py | 159 ++++++++++++++++++++++++++++++++++++++ tests/api/test_version.py | 64 +++++++++++++++ tests/api/test_vpcs.py | 53 +++++++++++++ tests/utils.py | 33 ++++++++ 7 files changed, 336 insertions(+), 3 deletions(-) create mode 100755 scripts/documentation.sh create mode 100644 tests/api/__init__.py create mode 100644 tests/api/base.py create mode 100644 tests/api/test_version.py create mode 100644 tests/api/test_vpcs.py create mode 100644 tests/utils.py diff --git a/requirements.txt b/requirements.txt index 3e267f9a..0cb66af9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,6 @@ netifaces -tornado==3.2.2 -pyzmq jsonschema pycurl python-dateutil apache-libcloud requests - diff --git a/scripts/documentation.sh b/scripts/documentation.sh new file mode 100755 index 00000000..67f10e6d --- /dev/null +++ b/scripts/documentation.sh @@ -0,0 +1,27 @@ +#!/bin/sh +# +# 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 . + +# +# Build the documentation +# + +set -e + +py.test +python ../gns3server/web/documentation.py +cd ../docs +make html diff --git a/tests/api/__init__.py b/tests/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/api/base.py b/tests/api/base.py new file mode 100644 index 00000000..5650b217 --- /dev/null +++ b/tests/api/base.py @@ -0,0 +1,159 @@ +# -*- 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 . + +"""Base code use for all API tests""" + +import json +import re +import asyncio +import socket +import pytest +from aiohttp import web +import aiohttp + +from gns3server.web.route import Route +#TODO: get rid of * +from gns3server.handlers import * +from gns3server.modules import MODULES + + +class Query: + def __init__(self, loop, host='localhost', port=8001): + self._loop = loop + self._port = port + self._host = host + + def post(self, path, body, **kwargs): + return self._fetch("POST", path, body, **kwargs) + + def get(self, path, **kwargs): + return self._fetch("GET", path, **kwargs) + + def _get_url(self, path): + return "http://{}:{}{}".format(self._host, self._port, path) + + def _fetch(self, method, path, body=None, **kwargs): + """Fetch an url, parse the JSON and return response + + Options: + - example if True the session is included inside documentation + - raw do not JSON encode the query + """ + if body is not None and not kwargs.get("raw", False): + body = json.dumps(body) + + @asyncio.coroutine + def go(future): + response = yield from aiohttp.request(method, self._get_url(path), data=body) + future.set_result(response) + future = asyncio.Future() + asyncio.async(go(future)) + self._loop.run_until_complete(future) + response = future.result() + + @asyncio.coroutine + def go(future, response): + response = yield from response.read() + future.set_result(response) + future = asyncio.Future() + asyncio.async(go(future, response)) + self._loop.run_until_complete(future) + response.body = future.result() + response.route = response.headers.get('X-Route', None) + + if response.body is not None: + try: + response.json = json.loads(response.body.decode("utf-8")) + except ValueError: + response.json = None + else: + response.json = {} + if kwargs.get('example'): + self._dump_example(method, response.route, body, response) + return response + + def _dump_example(self, method, path, body, response): + """Dump the request for the documentation""" + if path is None: + return + with open(self._example_file_path(method, path), 'w+') as f: + f.write("curl -i -x{} 'http://localhost:8000{}'".format(method, path)) + if body: + f.write(" -d '{}'".format(re.sub(r"\n", "", json.dumps(json.loads(body), sort_keys=True)))) + f.write("\n\n") + + f.write("{} {} HTTP/1.1\n".format(method, path)) + if body: + f.write(json.dumps(json.loads(body), sort_keys=True, indent=4)) + f.write("\n\n\n") + f.write("HTTP/1.1 {}\n".format(response.status)) + for header, value in sorted(response.headers.items()): + if header == 'DATE': + # We fix the date otherwise the example is always different + value = "Thu, 08 Jan 2015 16:09:15 GMT" + f.write("{}: {}\n".format(header, value)) + f.write("\n") + f.write(json.dumps(json.loads(response.body.decode('utf-8')), sort_keys=True, indent=4)) + f.write("\n") + + def _example_file_path(self, method, path): + path = re.sub('[^a-z0-9]', '', path) + return "docs/api/examples/{}_{}.txt".format(method.lower(), path) + + +def _get_unused_port(): + """ Return an unused port on localhost. In rare occasion it can return + an already used port (race condition)""" + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.bind(('localhost', 0)) + addr, port = s.getsockname() + s.close() + return port + + +@pytest.fixture(scope="module") +def loop(request): + """Return an event loop and destroy it at the end of test""" + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) # Replace main loop to avoid conflict between tests + + def tear_down(): + loop.close() + asyncio.set_event_loop(None) + request.addfinalizer(tear_down) + return loop + + +@pytest.fixture(scope="module") +def server(request, loop): + port = _get_unused_port() + host = "localhost" + app = web.Application() + for method, route, handler in Route.get_routes(): + app.router.add_route(method, route, handler) + for module in MODULES: + instance = module.instance() + srv = loop.create_server(app.make_handler(), host, port) + srv = loop.run_until_complete(srv) + + def tear_down(): + for module in MODULES: + loop.run_until_complete(module.destroy()) + srv.close() + srv.wait_closed() + request.addfinalizer(tear_down) + return Query(loop, host=host, port=port) diff --git a/tests/api/test_version.py b/tests/api/test_version.py new file mode 100644 index 00000000..178c0918 --- /dev/null +++ b/tests/api/test_version.py @@ -0,0 +1,64 @@ +# -*- 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 . + +""" +This test suite check /version endpoint +It's also used for unittest the HTTP implementation. +""" + +from tests.utils import asyncio_patch +from tests.api.base import server, loop +from gns3server.version import __version__ + + +def test_version_output(server): + response = server.get('/version', example=True) + assert response.status == 200 + assert response.json == {'version': __version__} + + +def test_version_input(server): + query = {'version': __version__} + response = server.post('/version', query, example=True) + assert response.status == 200 + assert response.json == {'version': __version__} + + +def test_version_invalid_input(server): + query = {'version': "0.4.2"} + response = server.post('/version', query) + assert response.status == 409 + assert response.json == {'message': '409: Invalid version', 'status': 409} + + +def test_version_invalid_input_schema(server): + query = {'version': "0.4.2", "bla": "blu"} + response = server.post('/version', query) + assert response.status == 400 + + +@asyncio_patch("demoserver.handlers.version_handler.VersionHandler", return_value={}) +def test_version_invalid_output_schema(): + query = {'version': "0.4.2"} + response = server.post('/version', query) + assert response.status == 400 + + +def test_version_invalid_json(server): + query = "BOUM" + response = server.post('/version', query, raw=True) + assert response.status == 400 diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py new file mode 100644 index 00000000..fb2b3c9a --- /dev/null +++ b/tests/api/test_vpcs.py @@ -0,0 +1,53 @@ +# -*- 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 tests.api.base import server, loop +from tests.utils import asyncio_patch +from gns3server import modules + + +def test_vpcs_create(server): + response = server.post('/vpcs', {'name': 'PC TEST 1'}, example=True) + assert response.status == 200 + assert response.route == '/vpcs' + assert response.json['name'] == 'PC TEST 1' + assert response.json['vpcs_id'] == 42 + + +@asyncio_patch('demoserver.modules.VPCS.create', return_value=84) +def test_vpcs_mock(server, mock): + response = server.post('/vpcs', {'name': 'PC TEST 1'}, example=False) + assert response.status == 200 + assert response.route == '/vpcs' + assert response.json['name'] == 'PC TEST 1' + assert response.json['vpcs_id'] == 84 + + +def test_vpcs_nio_create(server): + response = server.post('/vpcs/42/nio', { + 'id': 42, + 'nio': { + 'type': 'nio_unix', + 'local_file': '/tmp/test', + 'remote_file': '/tmp/remote' + }, + 'port': 0, + 'port_id': 0}, + example=True) + assert response.status == 200 + assert response.route == '/vpcs/{vpcs_id}/nio' + assert response.json['name'] == 'PC 2' diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 00000000..bb529541 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,33 @@ +# -*- 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 asyncio +from unittest.mock import patch + + +def asyncio_patch(function, *args, **kwargs): + @asyncio.coroutine + def fake_anwser(*a, **kw): + return kwargs["return_value"] + + def register(func): + @patch(function, return_value=fake_anwser) + @asyncio.coroutine + def inner(*a, **kw): + return func(*a, **kw) + return inner + return register From 369cd06279f935772330c59e596de43fd346c83f Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 13 Jan 2015 18:26:32 -0700 Subject: [PATCH 004/485] Merge latest changes from the POC. --- docs/api/examples/post_vpcs.txt | 4 +- gns3server/handlers/vpcs_handler.py | 10 ++- gns3server/modules/base.py | 66 ----------------- gns3server/modules/base_vm.py | 99 ++++++++++++++++++++++++++ gns3server/modules/vm_error.py | 20 ++++++ gns3server/modules/vm_manager.py | 89 +++++++++++++++++++++++ gns3server/modules/vpcs/__init__.py | 19 ++--- gns3server/modules/vpcs/vpcs_device.py | 23 ++++++ tests/api/test_vpcs.py | 12 +--- 9 files changed, 245 insertions(+), 97 deletions(-) delete mode 100644 gns3server/modules/base.py create mode 100644 gns3server/modules/base_vm.py create mode 100644 gns3server/modules/vm_error.py create mode 100644 gns3server/modules/vm_manager.py create mode 100644 gns3server/modules/vpcs/vpcs_device.py diff --git a/docs/api/examples/post_vpcs.txt b/docs/api/examples/post_vpcs.txt index 08616805..b0d5e689 100644 --- a/docs/api/examples/post_vpcs.txt +++ b/docs/api/examples/post_vpcs.txt @@ -8,7 +8,7 @@ POST /vpcs HTTP/1.1 HTTP/1.1 200 CONNECTION: close -CONTENT-LENGTH: 67 +CONTENT-LENGTH: 66 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT SERVER: Python/3.4 aiohttp/0.13.1 @@ -17,5 +17,5 @@ X-ROUTE: /vpcs { "console": 4242, "name": "PC TEST 1", - "vpcs_id": 42 + "vpcs_id": 1 } diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index 74d44fc1..abc2cd15 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -16,12 +16,10 @@ # 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 +from ..modules import VPCS class VPCSHandler(object): @@ -40,9 +38,9 @@ class VPCSHandler(object): 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, + vm = yield from vpcs.create_vm(request.json['name']) + response.json({'name': vm.name, + "vpcs_id": vm.id, "console": 4242}) @classmethod diff --git a/gns3server/modules/base.py b/gns3server/modules/base.py deleted file mode 100644 index 38761bf3..00000000 --- a/gns3server/modules/base.py +++ /dev/null @@ -1,66 +0,0 @@ -# -*- 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 asyncio - - -#TODO: make this more generic (not json but *args?) -class BaseModule(object): - _instance = None - - @classmethod - def instance(cls): - if cls._instance is None: - cls._instance = cls() - asyncio.async(cls._instance.run()) - return cls._instance - - def __init__(self): - self._queue = asyncio.Queue() - - @classmethod - def destroy(cls): - future = asyncio.Future() - cls._instance._queue.put_nowait((future, None, )) - yield from asyncio.wait([future]) - cls._instance = None - - @asyncio.coroutine - def put(self, json): - - future = asyncio.Future() - self._queue.put_nowait((future, json, )) - yield from asyncio.wait([future]) - return future.result() - - @asyncio.coroutine - def run(self): - - while True: - future, json = yield from self._queue.get() - if json is None: - future.set_result(True) - break - try: - result = yield from self.process(json) - future.set_result(result) - except Exception as e: - future.set_exception(e) - - @asyncio.coroutine - def process(self, json): - raise NotImplementedError diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py new file mode 100644 index 00000000..5e618059 --- /dev/null +++ b/gns3server/modules/base_vm.py @@ -0,0 +1,99 @@ +# -*- 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 asyncio +from .vm_error import VMError + + +class BaseVM: + + def __init__(self, name, identifier): + self._queue = asyncio.Queue() + self._name = name + self._id = identifier + self._created = asyncio.Future() + self._worker = asyncio.async(self._run()) + + @property + def id(self): + """ + Returns the unique ID for this VM. + + :returns: id (integer) + """ + + return self._id + + @property + def name(self): + """ + Returns the name for this VM. + + :returns: name (string) + """ + + return self._name + + @asyncio.coroutine + def _execute(self, subcommand, args): + """Called when we receive an event""" + raise NotImplementedError + + @asyncio.coroutine + def _create(self): + """Called when the run loop start""" + raise NotImplementedError + + @asyncio.coroutine + def _run(self, timeout=60): + + try: + yield from self._create() + self._created.set_result(True) + except VMError as e: + self._created.set_exception(e) + return + + while True: + future, subcommand, args = yield from self._queue.get() + try: + try: + yield from asyncio.wait_for(self._execute(subcommand, args), timeout=timeout) + except asyncio.TimeoutError: + raise VMError("{} has timed out after {} seconds!".format(subcommand, timeout)) + future.set_result(True) + except Exception as e: + future.set_exception(e) + + def wait_for_creation(self): + return self._created + + def put(self, *args): + """ + Add to the processing queue of the VM + + :returns: future + """ + + future = asyncio.Future() + try: + args.insert(0, future) + self._queue.put_nowait(args) + except asyncio.qeues.QueueFull: + raise VMError("Queue is full") + return future diff --git a/gns3server/modules/vm_error.py b/gns3server/modules/vm_error.py new file mode 100644 index 00000000..d7b71e14 --- /dev/null +++ b/gns3server/modules/vm_error.py @@ -0,0 +1,20 @@ +# -*- 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 . + + +class VMError(Exception): + pass diff --git a/gns3server/modules/vm_manager.py b/gns3server/modules/vm_manager.py new file mode 100644 index 00000000..7065a084 --- /dev/null +++ b/gns3server/modules/vm_manager.py @@ -0,0 +1,89 @@ +# -*- 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 asyncio +import aiohttp + +from .vm_error import VMError + + +class VMManager: + """ + Base class for all VMManager. + Responsible of management of a VM pool + """ + + def __init__(self): + self._vms = {} + + @classmethod + def instance(cls): + """ + Singleton to return only one instance of Manager. + + :returns: instance of Manager + """ + + if not hasattr(cls, "_instance"): + cls._instance = cls() + return cls._instance + + @classmethod + @asyncio.coroutine + def destroy(cls): + cls._instance = None + + def _get_vm_instance(self, vm_id): + """ + Returns a VM instance. + + :param vm_id: VM identifier + + :returns: VM instance + """ + + if vm_id not in self._vms: + raise aiohttp.web.HTTPNotFound(text="ID {} doesn't exist".format(vm_id)) + return self._vms[vm_id] + + @asyncio.coroutine + def create_vm(self, vmname, identifier=None): + if not identifier: + for i in range(1, 1024): + if i not in self._vms: + identifier = i + break + if identifier == 0: + raise VMError("Maximum number of VM instances reached") + else: + if identifier in self._vms: + raise VMError("VM identifier {} is already used by another VM instance".format(identifier)) + vm = self._VM_CLASS(vmname, identifier) + yield from vm.wait_for_creation() + self._vms[vm.id] = vm + return vm + + @asyncio.coroutine + def start_vm(self, vm_id): + vm = self._get_vm_instance(vm_id) + yield from vm.start() + + @asyncio.coroutine + def stop_vm(self, vm_id): + vm = self._get_vm_instance(vm_id) + yield from vm.stop() diff --git a/gns3server/modules/vpcs/__init__.py b/gns3server/modules/vpcs/__init__.py index d544365c..618124d1 100644 --- a/gns3server/modules/vpcs/__init__.py +++ b/gns3server/modules/vpcs/__init__.py @@ -19,19 +19,12 @@ VPCS server module. """ -import asyncio +from ..vm_manager import VMManager +from .vpcs_device import VPCSDevice -from ..base import BaseModule +class VPCS(VMManager): + _VM_CLASS = VPCSDevice -class VPCS(BaseModule): - - @asyncio.coroutine - def process(self, json): - yield from asyncio.sleep(1) - return 42 - - @asyncio.coroutine - def create(self, json): - i = yield from self.put(json) - return i + def create_vm(self, name): + return super().create_vm(name) diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py new file mode 100644 index 00000000..734344db --- /dev/null +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -0,0 +1,23 @@ +# -*- 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 ..base_vm import BaseVM + + +class VPCSDevice(BaseVM): + pass diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index fb2b3c9a..d5c441ca 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -20,16 +20,8 @@ from tests.utils import asyncio_patch from gns3server import modules -def test_vpcs_create(server): - response = server.post('/vpcs', {'name': 'PC TEST 1'}, example=True) - assert response.status == 200 - assert response.route == '/vpcs' - assert response.json['name'] == 'PC TEST 1' - assert response.json['vpcs_id'] == 42 - - -@asyncio_patch('demoserver.modules.VPCS.create', return_value=84) -def test_vpcs_mock(server, mock): +@asyncio_patch('gns3server.modules.VPCS.create_vm', return_value=84) +def test_vpcs_create(server, mock): response = server.post('/vpcs', {'name': 'PC TEST 1'}, example=False) assert response.status == 200 assert response.route == '/vpcs' From 1af5513c86e7cca66d1f9f411b490cb7ffd550f4 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 14 Jan 2015 10:33:01 +0100 Subject: [PATCH 005/485] Update dependencies --- dev-requirements.txt | 5 +++-- requirements.txt | 13 +++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index efb4e7a0..4647398d 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,4 +1,5 @@ -rrequirements.txt -pytest -ws4py +Sphinx==1.2.3 +pytest==2.6.4 +ws4py==0.3.4 diff --git a/requirements.txt b/requirements.txt index 0cb66af9..a7f54074 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ -netifaces -jsonschema -pycurl -python-dateutil -apache-libcloud -requests +netifaces==0.10.4 +jsonschema==2.4.0 +pycurl==7.19.5 +python-dateutil==2.3 +apache-libcloud==0.16.0 +requests==2.5.0 +aiohttp==0.13.1 From f1a9cc9f0128e154d66ab3552edfe766d890d962 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 14 Jan 2015 10:37:49 +0100 Subject: [PATCH 006/485] PEP8 --- dev-requirements.txt | 1 + tox.ini | 3 +++ 2 files changed, 4 insertions(+) diff --git a/dev-requirements.txt b/dev-requirements.txt index 4647398d..58c218e8 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -3,3 +3,4 @@ Sphinx==1.2.3 pytest==2.6.4 ws4py==0.3.4 +pep8==1.5.7 diff --git a/tox.ini b/tox.ini index 200e7ce4..74d58489 100644 --- a/tox.ini +++ b/tox.ini @@ -5,3 +5,6 @@ envlist = py33, py34 commands = python setup.py test deps = -rdev-requirements.txt +[pep8] +ignore = E501 + From efad58a2af5795a86c868b9c93a272305276cc1f Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 14 Jan 2015 10:39:46 +0100 Subject: [PATCH 007/485] Enable travis for all branches --- .travis.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 883004a4..d9bf305c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,15 +15,15 @@ install: script: - tox -e $TOX_ENV -branches: - only: - - master +#branches: +# only: +# - master notifications: email: false - irc: - channels: - - "chat.freenode.net#gns3" - on_success: change - on_failure: always +# irc: +# channels: +# - "chat.freenode.net#gns3" +# on_success: change +# on_failure: always From aab944fb6c859d2e9bdfdeaf775049ca200c3b96 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 14 Jan 2015 11:43:23 +0100 Subject: [PATCH 008/485] Move old test to old_tests directory --- docs/api/examples/get_version.txt | 4 ++-- docs/api/examples/post_version.txt | 8 ++++---- gns3server/handlers/__init__.py | 1 + gns3server/server.py | 5 ++--- {tests => old_tests}/dynamips/.gitignore | 0 {tests => old_tests}/dynamips/conftest.py | 0 {tests => old_tests}/dynamips/dynamips.stable | Bin {tests => old_tests}/dynamips/test_atm_bridge.py | 0 {tests => old_tests}/dynamips/test_atm_switch.py | 0 {tests => old_tests}/dynamips/test_bridge.py | 0 {tests => old_tests}/dynamips/test_c1700.py | 0 {tests => old_tests}/dynamips/test_c2600.py | 0 {tests => old_tests}/dynamips/test_c2691.py | 0 {tests => old_tests}/dynamips/test_c3600.py | 0 {tests => old_tests}/dynamips/test_c3725.py | 0 {tests => old_tests}/dynamips/test_c3745.py | 0 {tests => old_tests}/dynamips/test_c7200.py | 0 .../dynamips/test_ethernet_switch.py | 0 .../dynamips/test_frame_relay_switch.py | 0 {tests => old_tests}/dynamips/test_hub.py | 0 {tests => old_tests}/dynamips/test_hypervisor.py | 0 .../dynamips/test_hypervisor_manager.py | 0 {tests => old_tests}/dynamips/test_nios.py | 0 {tests => old_tests}/dynamips/test_router.py | 0 {tests => old_tests}/dynamips/test_vmhandler.py | 0 {tests => old_tests}/iou/test_iou_device.py | 0 {tests => old_tests}/test_jsonrpc.py | 0 {tests => old_tests}/test_version_handler.py | 0 {tests => old_tests}/vpcs/test_vpcs_device.py | 0 tests/api/test_version.py | 2 +- tox.ini | 3 +++ 31 files changed, 13 insertions(+), 10 deletions(-) rename {tests => old_tests}/dynamips/.gitignore (100%) rename {tests => old_tests}/dynamips/conftest.py (100%) rename {tests => old_tests}/dynamips/dynamips.stable (100%) rename {tests => old_tests}/dynamips/test_atm_bridge.py (100%) rename {tests => old_tests}/dynamips/test_atm_switch.py (100%) rename {tests => old_tests}/dynamips/test_bridge.py (100%) rename {tests => old_tests}/dynamips/test_c1700.py (100%) rename {tests => old_tests}/dynamips/test_c2600.py (100%) rename {tests => old_tests}/dynamips/test_c2691.py (100%) rename {tests => old_tests}/dynamips/test_c3600.py (100%) rename {tests => old_tests}/dynamips/test_c3725.py (100%) rename {tests => old_tests}/dynamips/test_c3745.py (100%) rename {tests => old_tests}/dynamips/test_c7200.py (100%) rename {tests => old_tests}/dynamips/test_ethernet_switch.py (100%) rename {tests => old_tests}/dynamips/test_frame_relay_switch.py (100%) rename {tests => old_tests}/dynamips/test_hub.py (100%) rename {tests => old_tests}/dynamips/test_hypervisor.py (100%) rename {tests => old_tests}/dynamips/test_hypervisor_manager.py (100%) rename {tests => old_tests}/dynamips/test_nios.py (100%) rename {tests => old_tests}/dynamips/test_router.py (100%) rename {tests => old_tests}/dynamips/test_vmhandler.py (100%) rename {tests => old_tests}/iou/test_iou_device.py (100%) rename {tests => old_tests}/test_jsonrpc.py (100%) rename {tests => old_tests}/test_version_handler.py (100%) rename {tests => old_tests}/vpcs/test_vpcs_device.py (100%) diff --git a/docs/api/examples/get_version.txt b/docs/api/examples/get_version.txt index fc0a948e..3e711fce 100644 --- a/docs/api/examples/get_version.txt +++ b/docs/api/examples/get_version.txt @@ -6,12 +6,12 @@ GET /version HTTP/1.1 HTTP/1.1 200 CONNECTION: close -CONTENT-LENGTH: 31 +CONTENT-LENGTH: 29 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" + "version": "1.3.dev1" } diff --git a/docs/api/examples/post_version.txt b/docs/api/examples/post_version.txt index 4ee7b50c..90ab4d81 100644 --- a/docs/api/examples/post_version.txt +++ b/docs/api/examples/post_version.txt @@ -1,19 +1,19 @@ -curl -i -xPOST 'http://localhost:8000/version' -d '{"version": "1.2.2.dev2"}' +curl -i -xPOST 'http://localhost:8000/version' -d '{"version": "1.3.dev1"}' POST /version HTTP/1.1 { - "version": "1.2.2.dev2" + "version": "1.3.dev1" } HTTP/1.1 200 CONNECTION: close -CONTENT-LENGTH: 31 +CONTENT-LENGTH: 29 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" + "version": "1.3.dev1" } diff --git a/gns3server/handlers/__init__.py b/gns3server/handlers/__init__.py index e69de29b..60c6268d 100644 --- a/gns3server/handlers/__init__.py +++ b/gns3server/handlers/__init__.py @@ -0,0 +1 @@ +__all__ = ['version_handler', 'vpcs_handler'] diff --git a/gns3server/server.py b/gns3server/server.py index b3d5fbd2..12da8bfd 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -33,9 +33,8 @@ from .web.route import Route from .config import Config 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 +#TODO: get rid of * have something generic to automatically import handlers so the routes can be found +from gns3server.handlers import * import logging log = logging.getLogger(__name__) diff --git a/tests/dynamips/.gitignore b/old_tests/dynamips/.gitignore similarity index 100% rename from tests/dynamips/.gitignore rename to old_tests/dynamips/.gitignore diff --git a/tests/dynamips/conftest.py b/old_tests/dynamips/conftest.py similarity index 100% rename from tests/dynamips/conftest.py rename to old_tests/dynamips/conftest.py diff --git a/tests/dynamips/dynamips.stable b/old_tests/dynamips/dynamips.stable similarity index 100% rename from tests/dynamips/dynamips.stable rename to old_tests/dynamips/dynamips.stable diff --git a/tests/dynamips/test_atm_bridge.py b/old_tests/dynamips/test_atm_bridge.py similarity index 100% rename from tests/dynamips/test_atm_bridge.py rename to old_tests/dynamips/test_atm_bridge.py diff --git a/tests/dynamips/test_atm_switch.py b/old_tests/dynamips/test_atm_switch.py similarity index 100% rename from tests/dynamips/test_atm_switch.py rename to old_tests/dynamips/test_atm_switch.py diff --git a/tests/dynamips/test_bridge.py b/old_tests/dynamips/test_bridge.py similarity index 100% rename from tests/dynamips/test_bridge.py rename to old_tests/dynamips/test_bridge.py diff --git a/tests/dynamips/test_c1700.py b/old_tests/dynamips/test_c1700.py similarity index 100% rename from tests/dynamips/test_c1700.py rename to old_tests/dynamips/test_c1700.py diff --git a/tests/dynamips/test_c2600.py b/old_tests/dynamips/test_c2600.py similarity index 100% rename from tests/dynamips/test_c2600.py rename to old_tests/dynamips/test_c2600.py diff --git a/tests/dynamips/test_c2691.py b/old_tests/dynamips/test_c2691.py similarity index 100% rename from tests/dynamips/test_c2691.py rename to old_tests/dynamips/test_c2691.py diff --git a/tests/dynamips/test_c3600.py b/old_tests/dynamips/test_c3600.py similarity index 100% rename from tests/dynamips/test_c3600.py rename to old_tests/dynamips/test_c3600.py diff --git a/tests/dynamips/test_c3725.py b/old_tests/dynamips/test_c3725.py similarity index 100% rename from tests/dynamips/test_c3725.py rename to old_tests/dynamips/test_c3725.py diff --git a/tests/dynamips/test_c3745.py b/old_tests/dynamips/test_c3745.py similarity index 100% rename from tests/dynamips/test_c3745.py rename to old_tests/dynamips/test_c3745.py diff --git a/tests/dynamips/test_c7200.py b/old_tests/dynamips/test_c7200.py similarity index 100% rename from tests/dynamips/test_c7200.py rename to old_tests/dynamips/test_c7200.py diff --git a/tests/dynamips/test_ethernet_switch.py b/old_tests/dynamips/test_ethernet_switch.py similarity index 100% rename from tests/dynamips/test_ethernet_switch.py rename to old_tests/dynamips/test_ethernet_switch.py diff --git a/tests/dynamips/test_frame_relay_switch.py b/old_tests/dynamips/test_frame_relay_switch.py similarity index 100% rename from tests/dynamips/test_frame_relay_switch.py rename to old_tests/dynamips/test_frame_relay_switch.py diff --git a/tests/dynamips/test_hub.py b/old_tests/dynamips/test_hub.py similarity index 100% rename from tests/dynamips/test_hub.py rename to old_tests/dynamips/test_hub.py diff --git a/tests/dynamips/test_hypervisor.py b/old_tests/dynamips/test_hypervisor.py similarity index 100% rename from tests/dynamips/test_hypervisor.py rename to old_tests/dynamips/test_hypervisor.py diff --git a/tests/dynamips/test_hypervisor_manager.py b/old_tests/dynamips/test_hypervisor_manager.py similarity index 100% rename from tests/dynamips/test_hypervisor_manager.py rename to old_tests/dynamips/test_hypervisor_manager.py diff --git a/tests/dynamips/test_nios.py b/old_tests/dynamips/test_nios.py similarity index 100% rename from tests/dynamips/test_nios.py rename to old_tests/dynamips/test_nios.py diff --git a/tests/dynamips/test_router.py b/old_tests/dynamips/test_router.py similarity index 100% rename from tests/dynamips/test_router.py rename to old_tests/dynamips/test_router.py diff --git a/tests/dynamips/test_vmhandler.py b/old_tests/dynamips/test_vmhandler.py similarity index 100% rename from tests/dynamips/test_vmhandler.py rename to old_tests/dynamips/test_vmhandler.py diff --git a/tests/iou/test_iou_device.py b/old_tests/iou/test_iou_device.py similarity index 100% rename from tests/iou/test_iou_device.py rename to old_tests/iou/test_iou_device.py diff --git a/tests/test_jsonrpc.py b/old_tests/test_jsonrpc.py similarity index 100% rename from tests/test_jsonrpc.py rename to old_tests/test_jsonrpc.py diff --git a/tests/test_version_handler.py b/old_tests/test_version_handler.py similarity index 100% rename from tests/test_version_handler.py rename to old_tests/test_version_handler.py diff --git a/tests/vpcs/test_vpcs_device.py b/old_tests/vpcs/test_vpcs_device.py similarity index 100% rename from tests/vpcs/test_vpcs_device.py rename to old_tests/vpcs/test_vpcs_device.py diff --git a/tests/api/test_version.py b/tests/api/test_version.py index 178c0918..2ddc172e 100644 --- a/tests/api/test_version.py +++ b/tests/api/test_version.py @@ -51,7 +51,7 @@ def test_version_invalid_input_schema(server): assert response.status == 400 -@asyncio_patch("demoserver.handlers.version_handler.VersionHandler", return_value={}) +@asyncio_patch("gns3server.handlers.version_handler.VersionHandler", return_value={}) def test_version_invalid_output_schema(): query = {'version': "0.4.2"} response = server.post('/version', query) diff --git a/tox.ini b/tox.ini index 74d58489..50b5bb1b 100644 --- a/tox.ini +++ b/tox.ini @@ -8,3 +8,6 @@ deps = -rdev-requirements.txt [pep8] ignore = E501 +[pytest] +norecursedirs = old_tests + From 1431c66c54cf75359ecda54355a9d9124e98bfd8 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 14 Jan 2015 12:32:56 +0100 Subject: [PATCH 009/485] Documentation generation --- .gitignore | 3 +++ docs/_static/.keep | 0 docs/conf.py | 20 ++++++++++---------- documentation.sh | 27 +++++++++++++++++++++++++++ gns3server/version.py | 1 + gns3server/web/documentation.py | 2 +- 6 files changed, 42 insertions(+), 11 deletions(-) create mode 100644 docs/_static/.keep create mode 100755 documentation.sh diff --git a/.gitignore b/.gitignore index 638a9fe7..876a38ec 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,6 @@ nosetests.xml # Gedit Backup Files *~ + +#Documentation build +docs/_build diff --git a/docs/_static/.keep b/docs/_static/.keep new file mode 100644 index 00000000..e69de29b diff --git a/docs/conf.py b/docs/conf.py index c3203698..346e586a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # -# POC documentation build configuration file, created by +# GNS3 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 @@ -21,7 +21,7 @@ import os # 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__ +from gns3server.version import __version__, __version_info__ # -- General configuration ------------------------------------------------ @@ -46,8 +46,8 @@ source_suffix = '.rst' master_doc = 'index' # General information about the project. -project = 'POC' -copyright = '2015, POC Team' +project = 'GNS3' +copyright = '2015, GNS3 GNS3 Technologies Inc.' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -179,7 +179,7 @@ html_static_path = ['_static'] # html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'POCdoc' +htmlhelp_basename = 'GNS3doc' # -- Options for LaTeX output --------------------------------------------- @@ -199,7 +199,7 @@ latex_elements = { # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - ('index', 'POC.tex', 'POC Documentation', 'POC Team', 'manual'), + ('index', 'GNS3.tex', 'GNS3 Documentation', 'GNS3 Team', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -228,8 +228,8 @@ latex_documents = [ # 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) + ('index', 'gns3', 'GNS3 Documentation', + ['GNS3 Team'], 1) ] # If true, show URL addresses after external links. @@ -242,8 +242,8 @@ man_pages = [ # (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.', + ('index', 'GNS3', 'GNS3 Documentation', + 'GNS3 Team', 'GNS3', 'One line description of project.', 'Miscellaneous'), ] diff --git a/documentation.sh b/documentation.sh new file mode 100755 index 00000000..251146e2 --- /dev/null +++ b/documentation.sh @@ -0,0 +1,27 @@ +#!/bin/sh +# +# 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 . + +# +# Build the documentation +# + +set -e + +py.test +python gns3server/web/documentation.py +cd docs +make html diff --git a/gns3server/version.py b/gns3server/version.py index 977c9652..049db3b8 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -16,3 +16,4 @@ # along with this program. If not, see . __version__ = "1.3.dev1" +__version_info__ = (1, 3, 0, 0) diff --git a/gns3server/web/documentation.py b/gns3server/web/documentation.py index 4bc28c39..2a2d212c 100644 --- a/gns3server/web/documentation.py +++ b/gns3server/web/documentation.py @@ -18,7 +18,7 @@ import re import os.path -from .route import Route +from gns3server.web.route import Route class Documentation(object): From 482fdf9031bd44ae562132be9838fb059e6e8a4e Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 14 Jan 2015 17:38:03 +0100 Subject: [PATCH 010/485] Drop documentation it's already in script directory --- documentation.sh | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100755 documentation.sh diff --git a/documentation.sh b/documentation.sh deleted file mode 100755 index 251146e2..00000000 --- a/documentation.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/sh -# -# 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 . - -# -# Build the documentation -# - -set -e - -py.test -python gns3server/web/documentation.py -cd docs -make html From 6c35cc304e971dbfab4604256a1e16bf25f03cbb Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 14 Jan 2015 18:52:02 +0100 Subject: [PATCH 011/485] Dirty stop start for VPCS --- gns3server/handlers/vpcs_handler.py | 37 +- .../{vm_manager.py => base_manager.py} | 10 +- gns3server/modules/base_vm.py | 73 +++- .../modules/{vm_error.py => device_error.py} | 2 +- gns3server/modules/vpcs/__init__.py | 7 +- gns3server/modules/vpcs/adapters/__init__.py | 0 gns3server/modules/vpcs/adapters/adapter.py | 104 +++++ .../modules/vpcs/adapters/ethernet_adapter.py | 31 ++ gns3server/modules/vpcs/nios/__init__.py | 0 gns3server/modules/vpcs/nios/nio_tap.py | 46 +++ gns3server/modules/vpcs/nios/nio_udp.py | 72 ++++ gns3server/modules/vpcs/vpcs_device.py | 363 +++++++++++++++++- gns3server/modules/vpcs/vpcs_error.py | 40 ++ gns3server/web/route.py | 5 + tests/modules/vpcs/test_vpcs_device.py | 41 ++ 15 files changed, 811 insertions(+), 20 deletions(-) rename gns3server/modules/{vm_manager.py => base_manager.py} (88%) rename gns3server/modules/{vm_error.py => device_error.py} (95%) create mode 100644 gns3server/modules/vpcs/adapters/__init__.py create mode 100644 gns3server/modules/vpcs/adapters/adapter.py create mode 100644 gns3server/modules/vpcs/adapters/ethernet_adapter.py create mode 100644 gns3server/modules/vpcs/nios/__init__.py create mode 100644 gns3server/modules/vpcs/nios/nio_tap.py create mode 100644 gns3server/modules/vpcs/nios/nio_udp.py create mode 100644 gns3server/modules/vpcs/vpcs_error.py create mode 100644 tests/modules/vpcs/test_vpcs_device.py diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index abc2cd15..c84e2b02 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -19,16 +19,13 @@ from ..web.route import Route from ..schemas.vpcs import VPCS_CREATE_SCHEMA from ..schemas.vpcs import VPCS_OBJECT_SCHEMA from ..schemas.vpcs import VPCS_ADD_NIO_SCHEMA -from ..modules import VPCS +from ..modules.vpcs import VPCS 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" @@ -43,6 +40,38 @@ class VPCSHandler(object): "vpcs_id": vm.id, "console": 4242}) + @classmethod + @Route.post( + r"/vpcs/{vpcs_id}/start", + parameters={ + "vpcs_id": "Id of VPCS instance" + }, + status_codes={ + 201: "Success of creation of VPCS", + }, + description="Start VPCS", + ) + def create(request, response): + vpcs_manager = VPCS.instance() + vm = yield from vpcs_manager.start_vm(int(request.match_info['vpcs_id'])) + response.json({}) + + @classmethod + @Route.post( + r"/vpcs/{vpcs_id}/stop", + parameters={ + "vpcs_id": "Id of VPCS instance" + }, + status_codes={ + 201: "Success of stopping VPCS", + }, + description="Stop VPCS", + ) + def create(request, response): + vpcs_manager = VPCS.instance() + vm = yield from vpcs_manager.stop_vm(int(request.match_info['vpcs_id'])) + response.json({}) + @classmethod @Route.get( r"/vpcs/{vpcs_id}", diff --git a/gns3server/modules/vm_manager.py b/gns3server/modules/base_manager.py similarity index 88% rename from gns3server/modules/vm_manager.py rename to gns3server/modules/base_manager.py index 7065a084..dbb29dce 100644 --- a/gns3server/modules/vm_manager.py +++ b/gns3server/modules/base_manager.py @@ -19,12 +19,12 @@ import asyncio import aiohttp -from .vm_error import VMError +from .device_error import DeviceError -class VMManager: +class BaseManager: """ - Base class for all VMManager. + Base class for all Manager. Responsible of management of a VM pool """ @@ -69,10 +69,10 @@ class VMManager: identifier = i break if identifier == 0: - raise VMError("Maximum number of VM instances reached") + raise DeviceError("Maximum number of VM instances reached") else: if identifier in self._vms: - raise VMError("VM identifier {} is already used by another VM instance".format(identifier)) + raise DeviceError("VM identifier {} is already used by another VM instance".format(identifier)) vm = self._VM_CLASS(vmname, identifier) yield from vm.wait_for_creation() self._vms[vm.id] = vm diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index 5e618059..3f8e4723 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -17,18 +17,74 @@ import asyncio -from .vm_error import VMError +from .device_error import DeviceError +from .attic import find_unused_port +import logging +log = logging.getLogger(__name__) class BaseVM: + _allocated_console_ports = [] def __init__(self, name, identifier): + self._loop = asyncio.get_event_loop() + self._allocate_console() self._queue = asyncio.Queue() self._name = name self._id = identifier self._created = asyncio.Future() self._worker = asyncio.async(self._run()) + log.info("{type} device {name} [id={id}] has been created".format( + type=self.__class__.__name__, + name=self._name, + id=self._id)) + + + def _allocate_console(self): + if not self._console: + # allocate a console port + try: + self._console = find_unused_port(self._console_start_port_range, + self._console_end_port_range, + self._console_host, + ignore_ports=self._allocated_console_ports) + except Exception as e: + raise DeviceError(e) + + if self._console in self._allocated_console_ports: + raise DeviceError("Console port {} is already used by another device".format(console)) + self._allocated_console_ports.append(self._console) + + @property + def console(self): + """ + Returns the TCP console port. + + :returns: console port (integer) + """ + + return self._console + + @console.setter + def console(self, console): + """ + Sets the TCP console port. + + :param console: console port (integer) + """ + + if console in self._allocated_console_ports: + raise VPCSError("Console port {} is already used by another VPCS device".format(console)) + + self._allocated_console_ports.remove(self._console) + self._console = console + self._allocated_console_ports.append(self._console) + log.info("{type} {name} [id={id}]: console port set to {port}".format( + type=self.__class__.__name__, + name=self._name, + id=self._id, + port=console)) @property def id(self): """ @@ -65,7 +121,7 @@ class BaseVM: try: yield from self._create() self._created.set_result(True) - except VMError as e: + except DeviceError as e: self._created.set_exception(e) return @@ -75,7 +131,7 @@ class BaseVM: try: yield from asyncio.wait_for(self._execute(subcommand, args), timeout=timeout) except asyncio.TimeoutError: - raise VMError("{} has timed out after {} seconds!".format(subcommand, timeout)) + raise DeviceError("{} has timed out after {} seconds!".format(subcommand, timeout)) future.set_result(True) except Exception as e: future.set_exception(e) @@ -83,6 +139,15 @@ class BaseVM: def wait_for_creation(self): return self._created + @asyncio.coroutine + def start(): + """ + Starts the VM process. + """ + raise NotImplementedError + + + def put(self, *args): """ Add to the processing queue of the VM @@ -95,5 +160,5 @@ class BaseVM: args.insert(0, future) self._queue.put_nowait(args) except asyncio.qeues.QueueFull: - raise VMError("Queue is full") + raise DeviceError("Queue is full") return future diff --git a/gns3server/modules/vm_error.py b/gns3server/modules/device_error.py similarity index 95% rename from gns3server/modules/vm_error.py rename to gns3server/modules/device_error.py index d7b71e14..b8eca500 100644 --- a/gns3server/modules/vm_error.py +++ b/gns3server/modules/device_error.py @@ -16,5 +16,5 @@ # along with this program. If not, see . -class VMError(Exception): +class DeviceError(Exception): pass diff --git a/gns3server/modules/vpcs/__init__.py b/gns3server/modules/vpcs/__init__.py index 618124d1..52191f2f 100644 --- a/gns3server/modules/vpcs/__init__.py +++ b/gns3server/modules/vpcs/__init__.py @@ -19,12 +19,9 @@ VPCS server module. """ -from ..vm_manager import VMManager +from ..base_manager import BaseManager from .vpcs_device import VPCSDevice -class VPCS(VMManager): +class VPCS(BaseManager): _VM_CLASS = VPCSDevice - - def create_vm(self, name): - return super().create_vm(name) diff --git a/gns3server/modules/vpcs/adapters/__init__.py b/gns3server/modules/vpcs/adapters/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gns3server/modules/vpcs/adapters/adapter.py b/gns3server/modules/vpcs/adapters/adapter.py new file mode 100644 index 00000000..cf439427 --- /dev/null +++ b/gns3server/modules/vpcs/adapters/adapter.py @@ -0,0 +1,104 @@ +# -*- 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 . + + +class Adapter(object): + """ + Base class for adapters. + + :param interfaces: number of interfaces supported by this adapter. + """ + + def __init__(self, interfaces=1): + + self._interfaces = interfaces + + self._ports = {} + for port_id in range(0, interfaces): + self._ports[port_id] = None + + def removable(self): + """ + Returns True if the adapter can be removed from a slot + and False if not. + + :returns: boolean + """ + + return True + + def port_exists(self, port_id): + """ + Checks if a port exists on this adapter. + + :returns: True is the port exists, + False otherwise. + """ + + if port_id in self._ports: + return True + return False + + def add_nio(self, port_id, nio): + """ + Adds a NIO to a port on this adapter. + + :param port_id: port ID (integer) + :param nio: NIO instance + """ + + self._ports[port_id] = nio + + def remove_nio(self, port_id): + """ + Removes a NIO from a port on this adapter. + + :param port_id: port ID (integer) + """ + + self._ports[port_id] = None + + def get_nio(self, port_id): + """ + Returns the NIO assigned to a port. + + :params port_id: port ID (integer) + + :returns: NIO instance + """ + + return self._ports[port_id] + + @property + def ports(self): + """ + Returns port to NIO mapping + + :returns: dictionary port -> NIO + """ + + return self._ports + + @property + def interfaces(self): + """ + Returns the number of interfaces supported by this adapter. + + :returns: number of interfaces + """ + + return self._interfaces diff --git a/gns3server/modules/vpcs/adapters/ethernet_adapter.py b/gns3server/modules/vpcs/adapters/ethernet_adapter.py new file mode 100644 index 00000000..bbca7f40 --- /dev/null +++ b/gns3server/modules/vpcs/adapters/ethernet_adapter.py @@ -0,0 +1,31 @@ +# -*- 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 . + +from .adapter import Adapter + + +class EthernetAdapter(Adapter): + """ + VPCS Ethernet adapter. + """ + + def __init__(self): + Adapter.__init__(self, interfaces=1) + + def __str__(self): + + return "VPCS Ethernet adapter" diff --git a/gns3server/modules/vpcs/nios/__init__.py b/gns3server/modules/vpcs/nios/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gns3server/modules/vpcs/nios/nio_tap.py b/gns3server/modules/vpcs/nios/nio_tap.py new file mode 100644 index 00000000..4c3ed6b2 --- /dev/null +++ b/gns3server/modules/vpcs/nios/nio_tap.py @@ -0,0 +1,46 @@ +# -*- 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 . + +""" +Interface for TAP NIOs (UNIX based OSes only). +""" + + +class NIO_TAP(object): + """ + TAP NIO. + + :param tap_device: TAP device name (e.g. tap0) + """ + + def __init__(self, tap_device): + + self._tap_device = tap_device + + @property + def tap_device(self): + """ + Returns the TAP device used by this NIO. + + :returns: the TAP device name + """ + + return self._tap_device + + def __str__(self): + + return "NIO TAP" diff --git a/gns3server/modules/vpcs/nios/nio_udp.py b/gns3server/modules/vpcs/nios/nio_udp.py new file mode 100644 index 00000000..0527f675 --- /dev/null +++ b/gns3server/modules/vpcs/nios/nio_udp.py @@ -0,0 +1,72 @@ +# -*- 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 . + +""" +Interface for UDP NIOs. +""" + + +class NIO_UDP(object): + """ + UDP NIO. + + :param lport: local port number + :param rhost: remote address/host + :param rport: remote port number + """ + + _instance_count = 0 + + def __init__(self, lport, rhost, rport): + + self._lport = lport + self._rhost = rhost + self._rport = rport + + @property + def lport(self): + """ + Returns the local port + + :returns: local port number + """ + + return self._lport + + @property + def rhost(self): + """ + Returns the remote host + + :returns: remote address/host + """ + + return self._rhost + + @property + def rport(self): + """ + Returns the remote port + + :returns: remote port number + """ + + return self._rport + + def __str__(self): + + return "NIO UDP" diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py index 734344db..a5d0c6ca 100644 --- a/gns3server/modules/vpcs/vpcs_device.py +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -15,9 +15,370 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +""" +VPCS device management (creates command line, processes, files etc.) in +order to run an VPCS instance. +""" + +import os +import sys +import subprocess +import signal +import shutil +import re +import asyncio + +from pkg_resources import parse_version +from .vpcs_error import VPCSError +from .adapters.ethernet_adapter import EthernetAdapter +from .nios.nio_udp import NIO_UDP +from .nios.nio_tap import NIO_TAP from ..base_vm import BaseVM +import logging +log = logging.getLogger(__name__) class VPCSDevice(BaseVM): - pass + """ + VPCS device implementation. + + :param name: name of this VPCS device + :param vpcs_id: VPCS instance ID + :param path: path to VPCS executable + :param working_dir: path to a working directory + :param console: TCP console port + :param console_host: IP address to bind for console connections + :param console_start_port_range: TCP console port range start + :param console_end_port_range: TCP console port range end + """ + def __init__(self, name, vpcs_id, + path = None, + working_dir = None, + console=None, + console_host="0.0.0.0", + console_start_port_range=4512, + console_end_port_range=5000): + + #self._path = path + #self._working_dir = working_dir + # TODO: Hardcodded for testing + self._path = "/usr/local/bin/vpcs" + self._working_dir = "/tmp" + + self._console = console + self._console_host = console_host + self._command = [] + self._process = None + self._vpcs_stdout_file = "" + self._started = False + self._console_start_port_range = console_start_port_range + self._console_end_port_range = console_end_port_range + + # VPCS settings + self._script_file = "" + self._ethernet_adapter = EthernetAdapter() # one adapter with 1 Ethernet interface + + # working_dir_path = os.path.join(working_dir, "vpcs", "pc-{}".format(self._id)) + # + # if vpcs_id and not os.path.isdir(working_dir_path): + # raise VPCSError("Working directory {} doesn't exist".format(working_dir_path)) + # + # # create the device own working directory + # self.working_dir = working_dir_path + # + + super().__init__(name, vpcs_id) + + @asyncio.coroutine + def _create(self): + """Called when run loop is started""" + self._check_requirement() + + def _check_requirement(self): + """Check if VPCS is available with the correct version""" + if not self._path: + raise VPCSError("No path to a VPCS executable has been set") + + if not os.path.isfile(self._path): + raise VPCSError("VPCS program '{}' is not accessible".format(self._path)) + + if not os.access(self._path, os.X_OK): + raise VPCSError("VPCS program '{}' is not executable".format(self._path)) + + yield from self._check_vpcs_version() + + def defaults(self): + """ + Returns all the default attribute values for VPCS. + + :returns: default values (dictionary) + """ + + vpcs_defaults = {"name": self._name, + "script_file": self._script_file, + "console": self._console} + + return vpcs_defaults + + + @classmethod + def reset(cls): + """ + Resets allocated instance list. + """ + + cls._instances.clear() + cls._allocated_console_ports.clear() + + @property + def name(self): + """ + Returns the name of this VPCS device. + + :returns: name + """ + + return self._name + + @name.setter + def name(self, new_name): + """ + Sets the name of this VPCS device. + + :param new_name: name + """ + + if self._script_file: + # update the startup.vpc + config_path = os.path.join(self._working_dir, "startup.vpc") + if os.path.isfile(config_path): + try: + with open(config_path, "r+", errors="replace") as f: + old_config = f.read() + new_config = old_config.replace(self._name, new_name) + f.seek(0) + f.write(new_config) + except OSError as e: + raise VPCSError("Could not amend the configuration {}: {}".format(config_path, e)) + + log.info("VPCS {name} [id={id}]: renamed to {new_name}".format(name=self._name, + id=self._id, + new_name=new_name)) + self._name = new_name + + @asyncio.coroutine + def _check_vpcs_version(self): + """ + Checks if the VPCS executable version is >= 0.5b1. + """ + #TODO: should be async + try: + output = subprocess.check_output([self._path, "-v"], cwd=self._working_dir) + match = re.search("Welcome to Virtual PC Simulator, version ([0-9a-z\.]+)", output.decode("utf-8")) + if match: + version = match.group(1) + if parse_version(version) < parse_version("0.5b1"): + raise VPCSError("VPCS executable version must be >= 0.5b1") + else: + raise VPCSError("Could not determine the VPCS version for {}".format(self._path)) + except (OSError, subprocess.SubprocessError) as e: + raise VPCSError("Error while looking for the VPCS version: {}".format(e)) + + + @asyncio.coroutine + def start(self): + """ + Starts the VPCS process. + """ + + if not self.is_running(): + # if not self._ethernet_adapter.get_nio(0): + # raise VPCSError("This VPCS instance must be connected in order to start") + + self._command = self._build_command() + try: + log.info("starting VPCS: {}".format(self._command)) + self._vpcs_stdout_file = os.path.join(self._working_dir, "vpcs.log") + log.info("logging to {}".format(self._vpcs_stdout_file)) + flags = 0 + if sys.platform.startswith("win32"): + flags = subprocess.CREATE_NEW_PROCESS_GROUP + with open(self._vpcs_stdout_file, "w") as fd: + self._process = yield from asyncio.create_subprocess_exec(*self._command, + stdout=fd, + stderr=subprocess.STDOUT, + cwd=self._working_dir, + creationflags=flags) + log.info("VPCS instance {} started PID={}".format(self._id, self._process.pid)) + self._started = True + except (OSError, subprocess.SubprocessError) as e: + vpcs_stdout = self.read_vpcs_stdout() + log.error("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout)) + raise VPCSError("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout)) + + @asyncio.coroutine + def stop(self): + """ + Stops the VPCS process. + """ + + # stop the VPCS process + if self.is_running(): + log.info("stopping VPCS instance {} PID={}".format(self._id, self._process.pid)) + if sys.platform.startswith("win32"): + self._process.send_signal(signal.CTRL_BREAK_EVENT) + else: + self._process.terminate() + + self._process.wait() + + self._process = None + self._started = False + + def read_vpcs_stdout(self): + """ + Reads the standard output of the VPCS process. + Only use when the process has been stopped or has crashed. + """ + + output = "" + if self._vpcs_stdout_file: + try: + with open(self._vpcs_stdout_file, errors="replace") as file: + output = file.read() + except OSError as e: + log.warn("could not read {}: {}".format(self._vpcs_stdout_file, e)) + return output + + def is_running(self): + """ + Checks if the VPCS process is running + + :returns: True or False + """ + + if self._process: + return True + return False + + def port_add_nio_binding(self, port_id, nio): + """ + Adds a port NIO binding. + + :param port_id: port ID + :param nio: NIO instance to add to the slot/port + """ + + if not self._ethernet_adapter.port_exists(port_id): + raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter, + port_id=port_id)) + + self._ethernet_adapter.add_nio(port_id, nio) + log.info("VPCS {name} [id={id}]: {nio} added to port {port_id}".format(name=self._name, + id=self._id, + nio=nio, + port_id=port_id)) + + def port_remove_nio_binding(self, port_id): + """ + Removes a port NIO binding. + + :param port_id: port ID + + :returns: NIO instance + """ + + if not self._ethernet_adapter.port_exists(port_id): + raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter, + port_id=port_id)) + + nio = self._ethernet_adapter.get_nio(port_id) + self._ethernet_adapter.remove_nio(port_id) + log.info("VPCS {name} [id={id}]: {nio} removed from port {port_id}".format(name=self._name, + id=self._id, + nio=nio, + port_id=port_id)) + return nio + + def _build_command(self): + """ + Command to start the VPCS process. + (to be passed to subprocess.Popen()) + + VPCS command line: + usage: vpcs [options] [scriptfile] + Option: + -h print this help then exit + -v print version information then exit + + -i num number of vpc instances to start (default is 9) + -p port run as a daemon listening on the tcp 'port' + -m num start byte of ether address, default from 0 + -r file load and execute script file + compatible with older versions, DEPRECATED. + + -e tap mode, using /dev/tapx by default (linux only) + -u udp mode, default + + udp mode options: + -s port local udp base port, default from 20000 + -c port remote udp base port (dynamips udp port), default from 30000 + -t ip remote host IP, default 127.0.0.1 + + tap mode options: + -d device device name, works only when -i is set to 1 + + hypervisor mode option: + -H port run as the hypervisor listening on the tcp 'port' + + If no 'scriptfile' specified, vpcs will read and execute the file named + 'startup.vpc' if it exsits in the current directory. + + """ + + command = [self._path] + command.extend(["-p", str(self._console)]) # listen to console port + + nio = self._ethernet_adapter.get_nio(0) + if nio: + if isinstance(nio, NIO_UDP): + # UDP tunnel + command.extend(["-s", str(nio.lport)]) # source UDP port + command.extend(["-c", str(nio.rport)]) # destination UDP port + command.extend(["-t", nio.rhost]) # destination host + + elif isinstance(nio, NIO_TAP): + # TAP interface + command.extend(["-e"]) + command.extend(["-d", nio.tap_device]) + + command.extend(["-m", str(self._id)]) # the unique ID is used to set the MAC address offset + command.extend(["-i", "1"]) # option to start only one VPC instance + command.extend(["-F"]) # option to avoid the daemonization of VPCS + if self._script_file: + command.extend([self._script_file]) + return command + + @property + def script_file(self): + """ + Returns the script-file for this VPCS instance. + + :returns: path to script-file + """ + + return self._script_file + + @script_file.setter + def script_file(self, script_file): + """ + Sets the script-file for this VPCS instance. + + :param script_file: path to base-script-file + """ + + self._script_file = script_file + log.info("VPCS {name} [id={id}]: script_file set to {config}".format(name=self._name, + id=self._id, + config=self._script_file)) diff --git a/gns3server/modules/vpcs/vpcs_error.py b/gns3server/modules/vpcs/vpcs_error.py new file mode 100644 index 00000000..acb10f71 --- /dev/null +++ b/gns3server/modules/vpcs/vpcs_error.py @@ -0,0 +1,40 @@ +# -*- 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 . + +""" +Custom exceptions for VPCS module. +""" + +from ..device_error import DeviceError + +class VPCSError(DeviceError): + + def __init__(self, message, original_exception=None): + + Exception.__init__(self, message) + if isinstance(message, Exception): + message = str(message) + self._message = message + self._original_exception = original_exception + + def __repr__(self): + + return self._message + + def __str__(self): + + return self._message diff --git a/gns3server/web/route.py b/gns3server/web/route.py index 7e0a091f..381b35e0 100644 --- a/gns3server/web/route.py +++ b/gns3server/web/route.py @@ -20,6 +20,7 @@ import jsonschema import asyncio import aiohttp +from ..modules.device_error import DeviceError from .response import Response @@ -97,6 +98,10 @@ class Route(object): response = Response(route=route) response.set_status(e.status) response.json({"message": e.text, "status": e.status}) + except DeviceError as e: + response = Response(route=route) + response.set_status(400) + response.json({"message": str(e), "status": 400}) return response cls._routes.append((method, cls._path, control_schema)) diff --git a/tests/modules/vpcs/test_vpcs_device.py b/tests/modules/vpcs/test_vpcs_device.py new file mode 100644 index 00000000..69a03d0d --- /dev/null +++ b/tests/modules/vpcs/test_vpcs_device.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 pytest +from unittest.mock import patch +from gns3server.modules.vpcs.vpcs_device import VPCSDevice +from gns3server.modules.vpcs.vpcs_error import VPCSError + +@patch("subprocess.check_output", return_value="Welcome to Virtual PC Simulator, version 0.6".encode("utf-8")) +def test_vm(tmpdir): + vm = VPCSDevice("test", 42, working_dir=str(tmpdir), path="/bin/test") + assert vm.name == "test" + assert vm.id == 42 + +@patch("subprocess.check_output", return_value="Welcome to Virtual PC Simulator, version 0.1".encode("utf-8")) +def test_vm_invalid_vpcs_version(tmpdir): + with pytest.raises(VPCSError): + vm = VPCSDevice("test", 42, working_dir=str(tmpdir), path="/bin/test") + assert vm.name == "test" + assert vm.id == 42 + +def test_vm_invalid_vpcs_path(tmpdir): + with pytest.raises(VPCSError): + vm = VPCSDevice("test", 42, working_dir=str(tmpdir), path="/bin/test_fake") + assert vm.name == "test" + assert vm.id == 42 + From 5618556b42ee708fe86a1016b1822364155481aa Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 14 Jan 2015 15:05:33 -0700 Subject: [PATCH 012/485] Updates dependencies in setup.py --- setup.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index 2921a890..40a9e409 100644 --- a/setup.py +++ b/setup.py @@ -33,6 +33,15 @@ class PyTest(TestCommand): errcode = pytest.main(self.test_args) sys.exit(errcode) + +dependencies = ["aiohttp==0.13.1", + "jsonschema==2.4.0", + "apache-libcloud==0.16.0", + "requests==2.5.0"] + +if sys.version_info == (3, 3): + dependencies.append("asyncio==3.4.2") + setup( name="gns3-server", version=__import__("gns3server").__version__, @@ -42,12 +51,7 @@ setup( cmdclass={"test": PyTest}, description="GNS3 server", long_description=open("README.rst", "r").read(), - install_requires=[ - "aiohttp", - "jsonschema>=2.3.0", - "apache-libcloud>=0.14.1", - "requests", - ], + install_requires=dependencies, entry_points={ "console_scripts": [ "gns3server = gns3server.main:main", From 77686b35c25e87afa1482450d64f4bbd1b3a9490 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 15 Jan 2015 10:13:12 +0100 Subject: [PATCH 013/485] Drop zmq and tornado from Readme --- README.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.rst b/README.rst index 7cf681e0..a3fa1600 100644 --- a/README.rst +++ b/README.rst @@ -18,9 +18,7 @@ Dependencies: - Python 3.3 or above - Setuptools -- PyZMQ library - Netifaces library -- Tornado - Jsonschema The following commands will install some of these dependencies: @@ -28,7 +26,6 @@ The following commands will install some of these dependencies: .. code:: bash sudo apt-get install python3-setuptools - sudo apt-get install python3-zmq sudo apt-get install python3-netifaces Finally these commands will install the server as well as the rest of the dependencies: From 6bb2b88f1a8052cfccd7d1c42e2d5808b79c913c Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 15 Jan 2015 13:02:43 +0100 Subject: [PATCH 014/485] It's was hard but i have finally a beginning of test for start VPCS --- gns3server/modules/base_vm.py | 4 +-- gns3server/modules/vpcs/vpcs_device.py | 45 +++++++------------------- tests/api/test_version.py | 2 +- tests/api/test_vpcs.py | 2 +- tests/modules/vpcs/test_vpcs_device.py | 14 +++++++- tests/utils.py | 42 ++++++++++++++++++++---- 6 files changed, 62 insertions(+), 47 deletions(-) diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index 3f8e4723..73921266 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -75,7 +75,7 @@ class BaseVM: """ if console in self._allocated_console_ports: - raise VPCSError("Console port {} is already used by another VPCS device".format(console)) + raise DeviceError("Console port {} is already used by another VM device".format(console)) self._allocated_console_ports.remove(self._console) self._console = console @@ -146,8 +146,6 @@ class BaseVM: """ raise NotImplementedError - - def put(self, *args): """ Add to the processing queue of the VM diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py index a5d0c6ca..67eb9874 100644 --- a/gns3server/modules/vpcs/vpcs_device.py +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -87,16 +87,13 @@ class VPCSDevice(BaseVM): # # create the device own working directory # self.working_dir = working_dir_path # - + self._check_requirements() super().__init__(name, vpcs_id) - @asyncio.coroutine - def _create(self): - """Called when run loop is started""" - self._check_requirement() - - def _check_requirement(self): - """Check if VPCS is available with the correct version""" + def _check_requirements(self): + """ + Check if VPCS is available with the correct version + """ if not self._path: raise VPCSError("No path to a VPCS executable has been set") @@ -106,30 +103,7 @@ class VPCSDevice(BaseVM): if not os.access(self._path, os.X_OK): raise VPCSError("VPCS program '{}' is not executable".format(self._path)) - yield from self._check_vpcs_version() - - def defaults(self): - """ - Returns all the default attribute values for VPCS. - - :returns: default values (dictionary) - """ - - vpcs_defaults = {"name": self._name, - "script_file": self._script_file, - "console": self._console} - - return vpcs_defaults - - - @classmethod - def reset(cls): - """ - Resets allocated instance list. - """ - - cls._instances.clear() - cls._allocated_console_ports.clear() + self._check_vpcs_version() @property def name(self): @@ -167,7 +141,6 @@ class VPCSDevice(BaseVM): new_name=new_name)) self._name = new_name - @asyncio.coroutine def _check_vpcs_version(self): """ Checks if the VPCS executable version is >= 0.5b1. @@ -185,6 +158,9 @@ class VPCSDevice(BaseVM): except (OSError, subprocess.SubprocessError) as e: raise VPCSError("Error while looking for the VPCS version: {}".format(e)) + @asyncio.coroutine + def _create(self): + pass @asyncio.coroutine def start(self): @@ -204,6 +180,7 @@ class VPCSDevice(BaseVM): flags = 0 if sys.platform.startswith("win32"): flags = subprocess.CREATE_NEW_PROCESS_GROUP + yield from asyncio.create_subprocess_exec() with open(self._vpcs_stdout_file, "w") as fd: self._process = yield from asyncio.create_subprocess_exec(*self._command, stdout=fd, @@ -241,7 +218,7 @@ class VPCSDevice(BaseVM): Reads the standard output of the VPCS process. Only use when the process has been stopped or has crashed. """ - + #TODO: should be async output = "" if self._vpcs_stdout_file: try: diff --git a/tests/api/test_version.py b/tests/api/test_version.py index 2ddc172e..a052bb43 100644 --- a/tests/api/test_version.py +++ b/tests/api/test_version.py @@ -52,7 +52,7 @@ def test_version_invalid_input_schema(server): @asyncio_patch("gns3server.handlers.version_handler.VersionHandler", return_value={}) -def test_version_invalid_output_schema(): +def test_version_invalid_output_schema(server): query = {'version': "0.4.2"} response = server.post('/version', query) assert response.status == 400 diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index d5c441ca..ccecd96f 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -21,7 +21,7 @@ from gns3server import modules @asyncio_patch('gns3server.modules.VPCS.create_vm', return_value=84) -def test_vpcs_create(server, mock): +def test_vpcs_create(server): response = server.post('/vpcs', {'name': 'PC TEST 1'}, example=False) assert response.status == 200 assert response.route == '/vpcs' diff --git a/tests/modules/vpcs/test_vpcs_device.py b/tests/modules/vpcs/test_vpcs_device.py index 69a03d0d..232a3214 100644 --- a/tests/modules/vpcs/test_vpcs_device.py +++ b/tests/modules/vpcs/test_vpcs_device.py @@ -16,7 +16,13 @@ # along with this program. If not, see . import pytest -from unittest.mock import patch +import asyncio +from tests.utils import asyncio_patch + +#Move loop to util +from tests.api.base import loop +from asyncio.subprocess import Process +from unittest.mock import patch, Mock from gns3server.modules.vpcs.vpcs_device import VPCSDevice from gns3server.modules.vpcs.vpcs_error import VPCSError @@ -39,3 +45,9 @@ def test_vm_invalid_vpcs_path(tmpdir): assert vm.name == "test" assert vm.id == 42 +def test_start(tmpdir, loop): + with asyncio_patch("asyncio.create_subprocess_exec", return_value=Mock()): + vm = VPCSDevice("test", 42, working_dir=str(tmpdir), path="/bin/test_fake") + loop.run_until_complete(asyncio.async(vm.start())) + assert vm.is_running() == True + diff --git a/tests/utils.py b/tests/utils.py index bb529541..ee2aff19 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -19,15 +19,43 @@ import asyncio from unittest.mock import patch -def asyncio_patch(function, *args, **kwargs): - @asyncio.coroutine - def fake_anwser(*a, **kw): - return kwargs["return_value"] +class _asyncio_patch: + """ + A wrapper around python patch supporting asyncio. + Like the original patch you can use it as context + manager (with) or decorator + + The original patch source code is the main source of + inspiration: + https://hg.python.org/cpython/file/3.4/Lib/unittest/mock.py + """ + def __init__(self, function, *args, **kwargs): + self.function = function + self.args = args + self.kwargs = kwargs + + def __enter__(self): + """Used when enter in the with block""" + self._patcher = patch(self.function, return_value=self._fake_anwser()) + self._patcher.start() + + def __exit__(self, *exc_info): + """Used when leaving the with block""" + self._patcher.stop() - def register(func): - @patch(function, return_value=fake_anwser) + def __call__(self, func, *args, **kwargs): + """Call is used when asyncio_patch is used as decorator""" + @patch(self.function, return_value=self._fake_anwser()) @asyncio.coroutine def inner(*a, **kw): return func(*a, **kw) return inner - return register + + def _fake_anwser(self): + future = asyncio.Future() + future.set_result(self.kwargs["return_value"]) + return future + + +def asyncio_patch(function, *args, **kwargs): + return _asyncio_patch(function, *args, **kwargs) From 3abcac43ab7130068712b7b703ddce29b0222b1c Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 15 Jan 2015 14:27:33 +0100 Subject: [PATCH 015/485] Test the stop method --- gns3server/modules/vpcs/vpcs_device.py | 2 +- gns3server/server.py | 2 +- tests/modules/vpcs/test_vpcs_device.py | 16 +++++++++++++--- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py index 67eb9874..008acac8 100644 --- a/gns3server/modules/vpcs/vpcs_device.py +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -208,7 +208,7 @@ class VPCSDevice(BaseVM): else: self._process.terminate() - self._process.wait() + yield from self._process.wait() self._process = None self._started = False diff --git a/gns3server/server.py b/gns3server/server.py index 12da8bfd..1a22bbeb 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -153,7 +153,7 @@ class Server: self._loop.run_until_complete(self._run_application(app)) self._signal_handling() - #FIXME: remove it in production + #FIXME: remove it in production or in tests self._loop.call_later(1, self._reload_hook) try: self._loop.run_forever() diff --git a/tests/modules/vpcs/test_vpcs_device.py b/tests/modules/vpcs/test_vpcs_device.py index 232a3214..35a5a47e 100644 --- a/tests/modules/vpcs/test_vpcs_device.py +++ b/tests/modules/vpcs/test_vpcs_device.py @@ -22,7 +22,7 @@ from tests.utils import asyncio_patch #Move loop to util from tests.api.base import loop from asyncio.subprocess import Process -from unittest.mock import patch, Mock +from unittest.mock import patch, MagicMock from gns3server.modules.vpcs.vpcs_device import VPCSDevice from gns3server.modules.vpcs.vpcs_error import VPCSError @@ -46,8 +46,18 @@ def test_vm_invalid_vpcs_path(tmpdir): assert vm.id == 42 def test_start(tmpdir, loop): - with asyncio_patch("asyncio.create_subprocess_exec", return_value=Mock()): - vm = VPCSDevice("test", 42, working_dir=str(tmpdir), path="/bin/test_fake") + with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): + vm = VPCSDevice("test", 42, working_dir=str(tmpdir), path="/bin/test") + loop.run_until_complete(asyncio.async(vm.start())) + assert vm.is_running() == True + +def test_stop(tmpdir, loop): + process = MagicMock() + with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): + vm = VPCSDevice("test", 42, working_dir=str(tmpdir), path="/bin/test") loop.run_until_complete(asyncio.async(vm.start())) assert vm.is_running() == True + loop.run_until_complete(asyncio.async(vm.stop())) + assert vm.is_running() == False + process.terminate.assert_called_with() From c1ef406311392be8a09aa91bea2adc01ccf5bcf8 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 15 Jan 2015 16:59:01 +0100 Subject: [PATCH 016/485] A basic implementation of port manager --- gns3server/handlers/vpcs_handler.py | 2 +- gns3server/modules/base_manager.py | 2 +- gns3server/modules/base_vm.py | 3 +- gns3server/modules/port_manager.py | 73 ++++++++++++++++++++++++++ gns3server/modules/vpcs/vpcs_device.py | 24 ++++----- gns3server/server.py | 14 ++--- tests/api/base.py | 7 ++- tests/modules/vpcs/test_vpcs_device.py | 22 ++++---- 8 files changed, 109 insertions(+), 38 deletions(-) create mode 100644 gns3server/modules/port_manager.py diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index c84e2b02..5213c27e 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -38,7 +38,7 @@ class VPCSHandler(object): vm = yield from vpcs.create_vm(request.json['name']) response.json({'name': vm.name, "vpcs_id": vm.id, - "console": 4242}) + "console": vm.console}) @classmethod @Route.post( diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index dbb29dce..abfd5df1 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -73,7 +73,7 @@ class BaseManager: else: if identifier in self._vms: raise DeviceError("VM identifier {} is already used by another VM instance".format(identifier)) - vm = self._VM_CLASS(vmname, identifier) + vm = self._VM_CLASS(vmname, identifier, self.port_manager) yield from vm.wait_for_creation() self._vms[vm.id] = vm return vm diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index 73921266..f9f41827 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -26,7 +26,7 @@ log = logging.getLogger(__name__) class BaseVM: _allocated_console_ports = [] - def __init__(self, name, identifier): + def __init__(self, name, identifier, port_manager): self._loop = asyncio.get_event_loop() self._allocate_console() self._queue = asyncio.Queue() @@ -34,6 +34,7 @@ class BaseVM: self._id = identifier self._created = asyncio.Future() self._worker = asyncio.async(self._run()) + self._port_manager = port_manager log.info("{type} device {name} [id={id}] has been created".format( type=self.__class__.__name__, name=self._name, diff --git a/gns3server/modules/port_manager.py b/gns3server/modules/port_manager.py new file mode 100644 index 00000000..2b833566 --- /dev/null +++ b/gns3server/modules/port_manager.py @@ -0,0 +1,73 @@ +# -*- 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 ipaddress +from .attic import find_unused_port + +class PortManager: + """ + :param console: TCP console port + :param console_host: IP address to bind for console connections + :param console_start_port_range: TCP console port range start + :param console_end_port_range: TCP console port range end + """ + def __init__(self, + console_host, + console_bind_to_any, + console_start_port_range=10000, + console_end_port_range=15000): + + self._console_start_port_range = console_start_port_range + self._console_end_port_range = console_end_port_range + self._used_ports = set() + + if console_bind_to_any: + if ipaddress.ip_address(console_host).version == 6: + self._console_host = "::" + else: + self._console_host = "0.0.0.0" + else: + self._console_host = console_host + + def get_free_port(self): + """Get an available console port and reserve it""" + port = find_unused_port(self._console_start_port_range, + self._console_end_port_range, + host=self._console_host, + socket_type='TCP', + ignore_ports=self._used_ports) + self._used_ports.add(port) + return port + + def reserve_port(port): + """ + Reserve a specific port number + + :param port: Port number + """ + if port in self._used_ports: + raise Exception("Port already {} in use".format(port)) + self._used_ports.add(port) + + def release_port(port): + """ + Release a specific port number + + :param port: Port number + """ + self._used_ports.remove(port) + diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py index 008acac8..61c896a0 100644 --- a/gns3server/modules/vpcs/vpcs_device.py +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -48,17 +48,11 @@ class VPCSDevice(BaseVM): :param path: path to VPCS executable :param working_dir: path to a working directory :param console: TCP console port - :param console_host: IP address to bind for console connections - :param console_start_port_range: TCP console port range start - :param console_end_port_range: TCP console port range end """ - def __init__(self, name, vpcs_id, + def __init__(self, name, vpcs_id, port_manager, path = None, working_dir = None, - console=None, - console_host="0.0.0.0", - console_start_port_range=4512, - console_end_port_range=5000): + console=None): #self._path = path #self._working_dir = working_dir @@ -67,13 +61,10 @@ class VPCSDevice(BaseVM): self._working_dir = "/tmp" self._console = console - self._console_host = console_host self._command = [] self._process = None self._vpcs_stdout_file = "" self._started = False - self._console_start_port_range = console_start_port_range - self._console_end_port_range = console_end_port_range # VPCS settings self._script_file = "" @@ -87,8 +78,16 @@ class VPCSDevice(BaseVM): # # create the device own working directory # self.working_dir = working_dir_path # + try: + if not self._console: + self._console = port_manager.get_free_port() + else: + self._console = port_manager.reserve_port(self._console) + except Exception as e: + raise VPCSError(e) + self._check_requirements() - super().__init__(name, vpcs_id) + super().__init__(name, vpcs_id, port_manager) def _check_requirements(self): """ @@ -180,7 +179,6 @@ class VPCSDevice(BaseVM): flags = 0 if sys.platform.startswith("win32"): flags = subprocess.CREATE_NEW_PROCESS_GROUP - yield from asyncio.create_subprocess_exec() with open(self._vpcs_stdout_file, "w") as fd: self._process = yield from asyncio.create_subprocess_exec(*self._command, stdout=fd, diff --git a/gns3server/server.py b/gns3server/server.py index 1a22bbeb..0db04449 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -24,7 +24,6 @@ import sys import signal import asyncio import aiohttp -import ipaddress import functools import types import time @@ -32,6 +31,7 @@ import time from .web.route import Route from .config import Config from .modules import MODULES +from .modules.port_manager import PortManager #TODO: get rid of * have something generic to automatically import handlers so the routes can be found from gns3server.handlers import * @@ -48,14 +48,7 @@ class Server: self._port = port self._loop = None self._start_time = time.time() - - if console_bind_to_any: - if ipaddress.ip_address(self._host).version == 6: - self._console_host = "::" - else: - self._console_host = "0.0.0.0" - else: - self._console_host = self._host + self._port_manager = PortManager(host, console_bind_to_any) #TODO: server config file support, to be reviewed # # get the projects and temp directories from the configuration file (passed to the modules) @@ -147,7 +140,8 @@ class Server: app.router.add_route(method, route, handler) for module in MODULES: log.debug("loading module {}".format(module.__name__)) - module.instance() + m = module.instance() + m.port_manager = self._port_manager log.info("starting server on {}:{}".format(self._host, self._port)) self._loop.run_until_complete(self._run_application(app)) diff --git a/tests/api/base.py b/tests/api/base.py index 5650b217..a95d36c2 100644 --- a/tests/api/base.py +++ b/tests/api/base.py @@ -29,6 +29,7 @@ from gns3server.web.route import Route #TODO: get rid of * from gns3server.handlers import * from gns3server.modules import MODULES +from gns3server.modules.port_manager import PortManager class Query: @@ -137,9 +138,12 @@ def loop(request): request.addfinalizer(tear_down) return loop +@pytest.fixture(scope="module") +def port_manager(): + return PortManager("127.0.0.1", False) @pytest.fixture(scope="module") -def server(request, loop): +def server(request, loop, port_manager): port = _get_unused_port() host = "localhost" app = web.Application() @@ -147,6 +151,7 @@ def server(request, loop): app.router.add_route(method, route, handler) for module in MODULES: instance = module.instance() + instance.port_manager = port_manager srv = loop.create_server(app.make_handler(), host, port) srv = loop.run_until_complete(srv) diff --git a/tests/modules/vpcs/test_vpcs_device.py b/tests/modules/vpcs/test_vpcs_device.py index 35a5a47e..7eeb8e82 100644 --- a/tests/modules/vpcs/test_vpcs_device.py +++ b/tests/modules/vpcs/test_vpcs_device.py @@ -20,41 +20,41 @@ import asyncio from tests.utils import asyncio_patch #Move loop to util -from tests.api.base import loop +from tests.api.base import loop, port_manager from asyncio.subprocess import Process from unittest.mock import patch, MagicMock from gns3server.modules.vpcs.vpcs_device import VPCSDevice from gns3server.modules.vpcs.vpcs_error import VPCSError @patch("subprocess.check_output", return_value="Welcome to Virtual PC Simulator, version 0.6".encode("utf-8")) -def test_vm(tmpdir): - vm = VPCSDevice("test", 42, working_dir=str(tmpdir), path="/bin/test") +def test_vm(tmpdir, port_manager): + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir), path="/bin/test") assert vm.name == "test" assert vm.id == 42 @patch("subprocess.check_output", return_value="Welcome to Virtual PC Simulator, version 0.1".encode("utf-8")) -def test_vm_invalid_vpcs_version(tmpdir): +def test_vm_invalid_vpcs_version(tmpdir, port_manager): with pytest.raises(VPCSError): - vm = VPCSDevice("test", 42, working_dir=str(tmpdir), path="/bin/test") + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir), path="/bin/test") assert vm.name == "test" assert vm.id == 42 -def test_vm_invalid_vpcs_path(tmpdir): +def test_vm_invalid_vpcs_path(tmpdir, port_manager): with pytest.raises(VPCSError): - vm = VPCSDevice("test", 42, working_dir=str(tmpdir), path="/bin/test_fake") + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir), path="/bin/test_fake") assert vm.name == "test" assert vm.id == 42 -def test_start(tmpdir, loop): +def test_start(tmpdir, loop, port_manager): with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): - vm = VPCSDevice("test", 42, working_dir=str(tmpdir), path="/bin/test") + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir), path="/bin/test") loop.run_until_complete(asyncio.async(vm.start())) assert vm.is_running() == True -def test_stop(tmpdir, loop): +def test_stop(tmpdir, loop, port_manager): process = MagicMock() with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): - vm = VPCSDevice("test", 42, working_dir=str(tmpdir), path="/bin/test") + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir), path="/bin/test") loop.run_until_complete(asyncio.async(vm.start())) assert vm.is_running() == True loop.run_until_complete(asyncio.async(vm.stop())) From 9e83329f14b938454c396da55e5eb89eded7ea22 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 15 Jan 2015 16:50:36 -0700 Subject: [PATCH 017/485] Rename Device to VM. --- README.rst | 2 +- gns3server/modules/base_manager.py | 6 +++--- gns3server/modules/base_vm.py | 18 +++++++++--------- gns3server/modules/port_manager.py | 14 ++++++++------ .../modules/{device_error.py => vm_error.py} | 2 +- gns3server/modules/vpcs/vpcs_error.py | 5 +++-- gns3server/web/route.py | 4 ++-- 7 files changed, 27 insertions(+), 24 deletions(-) rename gns3server/modules/{device_error.py => vm_error.py} (95%) diff --git a/README.rst b/README.rst index a3fa1600..a91baeed 100644 --- a/README.rst +++ b/README.rst @@ -4,7 +4,7 @@ GNS3-server This is the GNS3 server repository. The GNS3 server manages emulators such as Dynamips, VirtualBox or Qemu/KVM. -Clients like the GNS3 GUI controls the server using a JSON-RPC API over Websockets. +Clients like the GNS3 GUI controls the server using a HTTP REST API. You will need the GNS3 GUI (gns3-gui repository) to control the server. diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index abfd5df1..923576b5 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -19,7 +19,7 @@ import asyncio import aiohttp -from .device_error import DeviceError +from .vm_error import VMError class BaseManager: @@ -69,10 +69,10 @@ class BaseManager: identifier = i break if identifier == 0: - raise DeviceError("Maximum number of VM instances reached") + raise VMError("Maximum number of VM instances reached") else: if identifier in self._vms: - raise DeviceError("VM identifier {} is already used by another VM instance".format(identifier)) + raise VMError("VM identifier {} is already used by another VM instance".format(identifier)) vm = self._VM_CLASS(vmname, identifier, self.port_manager) yield from vm.wait_for_creation() self._vms[vm.id] = vm diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index f9f41827..24c1a037 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -17,12 +17,13 @@ import asyncio -from .device_error import DeviceError +from .vm_error import VMError from .attic import find_unused_port import logging log = logging.getLogger(__name__) + class BaseVM: _allocated_console_ports = [] @@ -40,7 +41,6 @@ class BaseVM: name=self._name, id=self._id)) - def _allocate_console(self): if not self._console: # allocate a console port @@ -50,10 +50,10 @@ class BaseVM: self._console_host, ignore_ports=self._allocated_console_ports) except Exception as e: - raise DeviceError(e) + raise VMError(e) if self._console in self._allocated_console_ports: - raise DeviceError("Console port {} is already used by another device".format(console)) + raise VMError("Console port {} is already used by another device".format(self._console)) self._allocated_console_ports.append(self._console) @@ -76,7 +76,7 @@ class BaseVM: """ if console in self._allocated_console_ports: - raise DeviceError("Console port {} is already used by another VM device".format(console)) + raise VMError("Console port {} is already used by another VM device".format(console)) self._allocated_console_ports.remove(self._console) self._console = console @@ -122,7 +122,7 @@ class BaseVM: try: yield from self._create() self._created.set_result(True) - except DeviceError as e: + except VMError as e: self._created.set_exception(e) return @@ -132,7 +132,7 @@ class BaseVM: try: yield from asyncio.wait_for(self._execute(subcommand, args), timeout=timeout) except asyncio.TimeoutError: - raise DeviceError("{} has timed out after {} seconds!".format(subcommand, timeout)) + raise VMError("{} has timed out after {} seconds!".format(subcommand, timeout)) future.set_result(True) except Exception as e: future.set_exception(e) @@ -141,7 +141,7 @@ class BaseVM: return self._created @asyncio.coroutine - def start(): + def start(self): """ Starts the VM process. """ @@ -159,5 +159,5 @@ class BaseVM: args.insert(0, future) self._queue.put_nowait(args) except asyncio.qeues.QueueFull: - raise DeviceError("Queue is full") + raise VMError("Queue is full") return future diff --git a/gns3server/modules/port_manager.py b/gns3server/modules/port_manager.py index 2b833566..59aed442 100644 --- a/gns3server/modules/port_manager.py +++ b/gns3server/modules/port_manager.py @@ -18,6 +18,7 @@ import ipaddress from .attic import find_unused_port + class PortManager: """ :param console: TCP console port @@ -26,10 +27,10 @@ class PortManager: :param console_end_port_range: TCP console port range end """ def __init__(self, - console_host, - console_bind_to_any, - console_start_port_range=10000, - console_end_port_range=15000): + console_host, + console_bind_to_any, + console_start_port_range=10000, + console_end_port_range=15000): self._console_start_port_range = console_start_port_range self._console_end_port_range = console_end_port_range @@ -53,7 +54,7 @@ class PortManager: self._used_ports.add(port) return port - def reserve_port(port): + def reserve_port(self, port): """ Reserve a specific port number @@ -63,11 +64,12 @@ class PortManager: raise Exception("Port already {} in use".format(port)) self._used_ports.add(port) - def release_port(port): + def release_port(self, port): """ Release a specific port number :param port: Port number """ + self._used_ports.remove(port) diff --git a/gns3server/modules/device_error.py b/gns3server/modules/vm_error.py similarity index 95% rename from gns3server/modules/device_error.py rename to gns3server/modules/vm_error.py index b8eca500..d7b71e14 100644 --- a/gns3server/modules/device_error.py +++ b/gns3server/modules/vm_error.py @@ -16,5 +16,5 @@ # along with this program. If not, see . -class DeviceError(Exception): +class VMError(Exception): pass diff --git a/gns3server/modules/vpcs/vpcs_error.py b/gns3server/modules/vpcs/vpcs_error.py index acb10f71..f32afdaa 100644 --- a/gns3server/modules/vpcs/vpcs_error.py +++ b/gns3server/modules/vpcs/vpcs_error.py @@ -19,9 +19,10 @@ Custom exceptions for VPCS module. """ -from ..device_error import DeviceError +from ..vm_error import VMError -class VPCSError(DeviceError): + +class VPCSError(VMError): def __init__(self, message, original_exception=None): diff --git a/gns3server/web/route.py b/gns3server/web/route.py index 381b35e0..1687cde9 100644 --- a/gns3server/web/route.py +++ b/gns3server/web/route.py @@ -20,7 +20,7 @@ import jsonschema import asyncio import aiohttp -from ..modules.device_error import DeviceError +from ..modules.vm_error import VMError from .response import Response @@ -98,7 +98,7 @@ class Route(object): response = Response(route=route) response.set_status(e.status) response.json({"message": e.text, "status": e.status}) - except DeviceError as e: + except VMError as e: response = Response(route=route) response.set_status(400) response.json({"message": str(e), "status": 400}) From 2ee49fed571ea8922f9f12403eb7da775bab3526 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 15 Jan 2015 17:43:06 -0700 Subject: [PATCH 018/485] Some cleaning. --- gns3server/modules/base_vm.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index 24c1a037..19571831 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -28,6 +28,7 @@ class BaseVM: _allocated_console_ports = [] def __init__(self, name, identifier, port_manager): + self._loop = asyncio.get_event_loop() self._allocate_console() self._queue = asyncio.Queue() @@ -42,6 +43,7 @@ class BaseVM: id=self._id)) def _allocate_console(self): + if not self._console: # allocate a console port try: @@ -53,7 +55,7 @@ class BaseVM: raise VMError(e) if self._console in self._allocated_console_ports: - raise VMError("Console port {} is already used by another device".format(self._console)) + raise VMError("Console port {} is already used by another VM".format(self._console)) self._allocated_console_ports.append(self._console) @@ -76,16 +78,15 @@ class BaseVM: """ if console in self._allocated_console_ports: - raise VMError("Console port {} is already used by another VM device".format(console)) + raise VMError("Console port {} is already used by another VM".format(console)) self._allocated_console_ports.remove(self._console) self._console = console self._allocated_console_ports.append(self._console) - log.info("{type} {name} [id={id}]: console port set to {port}".format( - type=self.__class__.__name__, - name=self._name, - id=self._id, - port=console)) + log.info("{type} {name} [id={id}]: console port set to {port}".format(type=self.__class__.__name__, + name=self._name, + id=self._id, + port=console)) @property def id(self): """ From aff834f565d6524cad0f1d9091d159adf66e40ca Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 16 Jan 2015 10:18:02 +0100 Subject: [PATCH 019/485] Oops bad merge --- gns3server/version.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/gns3server/version.py b/gns3server/version.py index 80e9e442..ed60edd9 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -15,10 +15,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -<<<<<<< HEAD -__version__ = "1.3.dev1" -__version_info__ = (1, 3, 0, 0) -======= # __version__ is a human-readable version number. # __version_info__ is a four-tuple for programmatic comparison. The first @@ -27,6 +23,6 @@ __version_info__ = (1, 3, 0, 0) # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "1.2.2" -__version_info__ = (1, 2, 2, 0) ->>>>>>> origin/master +__version__ = "1.3.dev1" +__version_info__ = (1, 3, 0, 0) + From 0cdc1c3042b0b9bc52378fafbd1145fe56a5df5b Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 16 Jan 2015 16:20:10 +0100 Subject: [PATCH 020/485] VCPS create NIO work and tested --- .../post_vpcsvpcsidportsportidnio.txt | 22 +++ gns3server/handlers/vpcs_handler.py | 35 ++-- gns3server/modules/base_manager.py | 6 +- gns3server/modules/vpcs/vpcs_device.py | 33 +++- gns3server/schemas/vpcs.py | 155 +----------------- tests/api/test_vpcs.py | 10 +- tests/modules/vpcs/test_vpcs_device.py | 16 ++ 7 files changed, 90 insertions(+), 187 deletions(-) create mode 100644 docs/api/examples/post_vpcsvpcsidportsportidnio.txt diff --git a/docs/api/examples/post_vpcsvpcsidportsportidnio.txt b/docs/api/examples/post_vpcsvpcsidportsportidnio.txt new file mode 100644 index 00000000..cdcb7a4e --- /dev/null +++ b/docs/api/examples/post_vpcsvpcsidportsportidnio.txt @@ -0,0 +1,22 @@ +curl -i -xPOST 'http://localhost:8000/vpcs/{vpcs_id}/ports/{port_id}/nio' -d '{"local_file": "/tmp/test", "remote_file": "/tmp/remote", "type": "nio_unix"}' + +POST /vpcs/{vpcs_id}/ports/{port_id}/nio HTTP/1.1 +{ + "local_file": "/tmp/test", + "remote_file": "/tmp/remote", + "type": "nio_unix" +} + + +HTTP/1.1 404 +CONNECTION: close +CONTENT-LENGTH: 59 +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}/ports/{port_id}/nio + +{ + "message": "ID 42 doesn't exist", + "status": 404 +} diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index 5213c27e..6fc3495e 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -15,6 +15,7 @@ # 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 ..schemas.vpcs import VPCS_CREATE_SCHEMA from ..schemas.vpcs import VPCS_OBJECT_SCHEMA @@ -72,32 +73,9 @@ class VPCSHandler(object): vm = yield from vpcs_manager.stop_vm(int(request.match_info['vpcs_id'])) response.json({}) - @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", + r"/vpcs/{vpcs_id}/ports/{port_id}/nio", parameters={ "vpcs_id": "Id of VPCS instance" }, @@ -108,5 +86,12 @@ class VPCSHandler(object): description="ADD NIO to a VPCS", input=VPCS_ADD_NIO_SCHEMA) def create_nio(request, response): - # TODO: raise 404 if VPCS not found + # TODO: raise 404 if VPCS not found GET VM can raise an exeption + # TODO: response with nio + vpcs_manager = VPCS.instance() + vm = vpcs_manager.get_vm(int(request.match_info['vpcs_id'])) + vm.port_add_nio_binding(int(request.match_info['port_id']), request.json) + response.json({'name': "PC 2", "vpcs_id": 42, "console": 4242}) + + diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 923576b5..ab07f427 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -48,7 +48,7 @@ class BaseManager: def destroy(cls): cls._instance = None - def _get_vm_instance(self, vm_id): + def get_vm(self, vm_id): """ Returns a VM instance. @@ -80,10 +80,10 @@ class BaseManager: @asyncio.coroutine def start_vm(self, vm_id): - vm = self._get_vm_instance(vm_id) + vm = self.get_vm(vm_id) yield from vm.start() @asyncio.coroutine def stop_vm(self, vm_id): - vm = self._get_vm_instance(vm_id) + vm = self.get_vm(vm_id) yield from vm.stop() diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py index 61c896a0..5384a985 100644 --- a/gns3server/modules/vpcs/vpcs_device.py +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -27,12 +27,14 @@ import signal import shutil import re import asyncio +import socket from pkg_resources import parse_version from .vpcs_error import VPCSError from .adapters.ethernet_adapter import EthernetAdapter from .nios.nio_udp import NIO_UDP from .nios.nio_tap import NIO_TAP +from ..attic import has_privileged_access from ..base_vm import BaseVM @@ -168,8 +170,8 @@ class VPCSDevice(BaseVM): """ if not self.is_running(): - # if not self._ethernet_adapter.get_nio(0): - # raise VPCSError("This VPCS instance must be connected in order to start") + if not self._ethernet_adapter.get_nio(0): + raise VPCSError("This VPCS instance must be connected in order to start") self._command = self._build_command() try: @@ -237,7 +239,7 @@ class VPCSDevice(BaseVM): return True return False - def port_add_nio_binding(self, port_id, nio): + def port_add_nio_binding(self, port_id, nio_settings): """ Adds a port NIO binding. @@ -249,11 +251,34 @@ class VPCSDevice(BaseVM): raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter, port_id=port_id)) + nio = None + if nio_settings["type"] == "nio_udp": + lport = nio_settings["lport"] + rhost = nio_settings["rhost"] + rport = nio_settings["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 nio_settings["type"] == "nio_tap": + tap_device = nio_settings["tap_device"] + print(has_privileged_access) + if not has_privileged_access(self._path): + raise VPCSError("{} has no privileged access to {}.".format(self._path, tap_device)) + nio = NIO_TAP(tap_device) + if not nio: + raise VPCSError("Requested NIO does not exist or is not supported: {}".format(nio_settings["type"])) + + self._ethernet_adapter.add_nio(port_id, nio) log.info("VPCS {name} [id={id}]: {nio} added to port {port_id}".format(name=self._name, id=self._id, nio=nio, port_id=port_id)) + return nio def port_remove_nio_binding(self, port_id): """ @@ -317,6 +342,8 @@ class VPCSDevice(BaseVM): nio = self._ethernet_adapter.get_nio(0) if nio: + print(nio) + print(isinstance(nio, NIO_UDP)) if isinstance(nio, NIO_UDP): # UDP tunnel command.extend(["-s", str(nio.lport)]) # source UDP port diff --git a/gns3server/schemas/vpcs.py b/gns3server/schemas/vpcs.py index 7d205391..dc5ca6dd 100644 --- a/gns3server/schemas/vpcs.py +++ b/gns3server/schemas/vpcs.py @@ -75,36 +75,6 @@ VPCS_ADD_NIO_SCHEMA = { "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": { @@ -120,89 +90,14 @@ VPCS_ADD_NIO_SCHEMA = { "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"] + "oneOf": [ + {"$ref": "#/definitions/UDP"}, + {"$ref": "#/definitions/TAP"}, + ], + "additionalProperties": True, + "required": ['type'] } VPCS_OBJECT_SCHEMA = { @@ -230,41 +125,3 @@ VPCS_OBJECT_SCHEMA = { "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/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index ccecd96f..eb65610f 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from tests.api.base import server, loop +from tests.api.base import server, loop, port_manager from tests.utils import asyncio_patch from gns3server import modules @@ -30,16 +30,12 @@ def test_vpcs_create(server): def test_vpcs_nio_create(server): - response = server.post('/vpcs/42/nio', { - 'id': 42, - 'nio': { + response = server.post('/vpcs/42/ports/0/nio', { 'type': 'nio_unix', 'local_file': '/tmp/test', 'remote_file': '/tmp/remote' }, - 'port': 0, - 'port_id': 0}, example=True) assert response.status == 200 - assert response.route == '/vpcs/{vpcs_id}/nio' + assert response.route == '/vpcs/{vpcs_id}/ports/{port_id}/nio' assert response.json['name'] == 'PC 2' diff --git a/tests/modules/vpcs/test_vpcs_device.py b/tests/modules/vpcs/test_vpcs_device.py index 7eeb8e82..b56ace3f 100644 --- a/tests/modules/vpcs/test_vpcs_device.py +++ b/tests/modules/vpcs/test_vpcs_device.py @@ -61,3 +61,19 @@ def test_stop(tmpdir, loop, port_manager): assert vm.is_running() == False process.terminate.assert_called_with() +def test_add_nio_binding_udp(port_manager, tmpdir): + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir), path="/bin/test") + nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) + assert nio.lport == 4242 + +def test_add_nio_binding_tap(port_manager, tmpdir): + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir), path="/bin/test") + with patch("gns3server.modules.vpcs.vpcs_device.has_privileged_access", return_value=True): + nio = vm.port_add_nio_binding(0, {"type": "nio_tap", "tap_device": "test"}) + assert nio.tap_device == "test" + +def test_add_nio_binding_tap_no_privileged_access(port_manager, tmpdir): + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir), path="/bin/test") + with patch("gns3server.modules.vpcs.vpcs_device.has_privileged_access", return_value=False): + with pytest.raises(VPCSError): + vm.port_add_nio_binding(0, {"type": "nio_tap", "tap_device": "test"}) From bf6f62e6290582237044703acbc92ed4de2bf5ce Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 16 Jan 2015 17:09:45 +0100 Subject: [PATCH 021/485] Serialize NIO --- .../post_vpcsvpcsidportsportidnio.txt | 19 ++++++++------ gns3server/handlers/vpcs_handler.py | 18 +++++++------ gns3server/modules/base_manager.py | 2 +- gns3server/modules/vpcs/nios/nio_tap.py | 3 +++ gns3server/modules/vpcs/nios/nio_udp.py | 3 +++ gns3server/modules/vpcs/vpcs_device.py | 1 - gns3server/schemas/vpcs.py | 2 +- gns3server/web/response.py | 11 ++++++++ gns3server/web/route.py | 4 +++ tests/api/test_version.py | 2 +- tests/api/test_vpcs.py | 26 ++++++++++++++----- 11 files changed, 65 insertions(+), 26 deletions(-) diff --git a/docs/api/examples/post_vpcsvpcsidportsportidnio.txt b/docs/api/examples/post_vpcsvpcsidportsportidnio.txt index cdcb7a4e..06fbc0fb 100644 --- a/docs/api/examples/post_vpcsvpcsidportsportidnio.txt +++ b/docs/api/examples/post_vpcsvpcsidportsportidnio.txt @@ -1,22 +1,25 @@ -curl -i -xPOST 'http://localhost:8000/vpcs/{vpcs_id}/ports/{port_id}/nio' -d '{"local_file": "/tmp/test", "remote_file": "/tmp/remote", "type": "nio_unix"}' +curl -i -xPOST 'http://localhost:8000/vpcs/{vpcs_id}/ports/{port_id}/nio' -d '{"lport": 4242, "rhost": "127.0.0.1", "rport": 4343, "type": "nio_udp"}' POST /vpcs/{vpcs_id}/ports/{port_id}/nio HTTP/1.1 { - "local_file": "/tmp/test", - "remote_file": "/tmp/remote", - "type": "nio_unix" + "lport": 4242, + "rhost": "127.0.0.1", + "rport": 4343, + "type": "nio_udp" } -HTTP/1.1 404 +HTTP/1.1 200 CONNECTION: close -CONTENT-LENGTH: 59 +CONTENT-LENGTH: 89 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}/ports/{port_id}/nio { - "message": "ID 42 doesn't exist", - "status": 404 + "lport": 4242, + "rhost": "127.0.0.1", + "rport": 4343, + "type": "nio_udp" } diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index 6fc3495e..b12383b5 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -19,7 +19,7 @@ from ..web.route import Route from ..schemas.vpcs import VPCS_CREATE_SCHEMA from ..schemas.vpcs import VPCS_OBJECT_SCHEMA -from ..schemas.vpcs import VPCS_ADD_NIO_SCHEMA +from ..schemas.vpcs import VPCS_NIO_SCHEMA from ..modules.vpcs import VPCS @@ -48,7 +48,8 @@ class VPCSHandler(object): "vpcs_id": "Id of VPCS instance" }, status_codes={ - 201: "Success of creation of VPCS", + 200: "Success of starting VPCS", + 404: "If VPCS doesn't exist" }, description="Start VPCS", ) @@ -64,7 +65,8 @@ class VPCSHandler(object): "vpcs_id": "Id of VPCS instance" }, status_codes={ - 201: "Success of stopping VPCS", + 200: "Success of stopping VPCS", + 404: "If VPCS doesn't exist" }, description="Stop VPCS", ) @@ -81,17 +83,17 @@ class VPCSHandler(object): }, status_codes={ 201: "Success of creation of NIO", - 409: "Conflict" + 404: "If VPCS doesn't exist" }, description="ADD NIO to a VPCS", - input=VPCS_ADD_NIO_SCHEMA) + input=VPCS_NIO_SCHEMA, + output=VPCS_NIO_SCHEMA) def create_nio(request, response): - # TODO: raise 404 if VPCS not found GET VM can raise an exeption # TODO: response with nio vpcs_manager = VPCS.instance() vm = vpcs_manager.get_vm(int(request.match_info['vpcs_id'])) - vm.port_add_nio_binding(int(request.match_info['port_id']), request.json) + nio = vm.port_add_nio_binding(int(request.match_info['port_id']), request.json) - response.json({'name': "PC 2", "vpcs_id": 42, "console": 4242}) + response.json(nio) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index ab07f427..36d031a6 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -39,7 +39,7 @@ class BaseManager: :returns: instance of Manager """ - if not hasattr(cls, "_instance"): + if not hasattr(cls, "_instance") or cls._instance is None: cls._instance = cls() return cls._instance diff --git a/gns3server/modules/vpcs/nios/nio_tap.py b/gns3server/modules/vpcs/nios/nio_tap.py index 4c3ed6b2..39923a01 100644 --- a/gns3server/modules/vpcs/nios/nio_tap.py +++ b/gns3server/modules/vpcs/nios/nio_tap.py @@ -44,3 +44,6 @@ class NIO_TAP(object): def __str__(self): return "NIO TAP" + + def __json__(self): + return {"type": "nio_tap", "tap_device": self._tap_device} diff --git a/gns3server/modules/vpcs/nios/nio_udp.py b/gns3server/modules/vpcs/nios/nio_udp.py index 0527f675..cca313e7 100644 --- a/gns3server/modules/vpcs/nios/nio_udp.py +++ b/gns3server/modules/vpcs/nios/nio_udp.py @@ -70,3 +70,6 @@ class NIO_UDP(object): def __str__(self): return "NIO UDP" + + def __json__(self): + return {"type": "nio_udp", "lport": self._lport, "rport": self._rport, "rhost": self._rhost} diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py index 5384a985..fe079c25 100644 --- a/gns3server/modules/vpcs/vpcs_device.py +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -265,7 +265,6 @@ class VPCSDevice(BaseVM): nio = NIO_UDP(lport, rhost, rport) elif nio_settings["type"] == "nio_tap": tap_device = nio_settings["tap_device"] - print(has_privileged_access) if not has_privileged_access(self._path): raise VPCSError("{} has no privileged access to {}.".format(self._path, tap_device)) nio = NIO_TAP(tap_device) diff --git a/gns3server/schemas/vpcs.py b/gns3server/schemas/vpcs.py index dc5ca6dd..27a5bcd3 100644 --- a/gns3server/schemas/vpcs.py +++ b/gns3server/schemas/vpcs.py @@ -42,7 +42,7 @@ VPCS_CREATE_SCHEMA = { } -VPCS_ADD_NIO_SCHEMA = { +VPCS_NIO_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", "description": "Request validation to add a NIO for a VPCS instance", "type": "object", diff --git a/gns3server/web/response.py b/gns3server/web/response.py index 325455f4..d829e725 100644 --- a/gns3server/web/response.py +++ b/gns3server/web/response.py @@ -18,7 +18,9 @@ import json import jsonschema import aiohttp.web +import logging +log = logging.getLogger(__name__) class Response(aiohttp.web.Response): @@ -29,13 +31,22 @@ class Response(aiohttp.web.Response): headers['X-Route'] = self._route super().__init__(headers=headers, **kwargs) + """ + Set the response content type to application/json and serialize + the content. + + :param anwser The response as a Python object + """ def json(self, answer): """Pass a Python object and return a JSON as answer""" self.content_type = "application/json" + if hasattr(answer, '__json__'): + answer = answer.__json__() if self._output_schema is not None: try: jsonschema.validate(answer, self._output_schema) except jsonschema.ValidationError as e: + log.error("Invalid output schema") 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 index 1687cde9..e55edf06 100644 --- a/gns3server/web/route.py +++ b/gns3server/web/route.py @@ -19,6 +19,9 @@ import json import jsonschema import asyncio import aiohttp +import logging + +log = logging.getLogger(__name__) from ..modules.vm_error import VMError from .response import Response @@ -37,6 +40,7 @@ def parse_request(request, input_schema): try: jsonschema.validate(request.json, input_schema) except jsonschema.ValidationError as e: + log.error("Invalid input schema") raise aiohttp.web.HTTPBadRequest(text="Request is not {} '{}' in schema: {}".format( e.validator, e.validator_value, diff --git a/tests/api/test_version.py b/tests/api/test_version.py index a052bb43..8fb46174 100644 --- a/tests/api/test_version.py +++ b/tests/api/test_version.py @@ -21,7 +21,7 @@ It's also used for unittest the HTTP implementation. """ from tests.utils import asyncio_patch -from tests.api.base import server, loop +from tests.api.base import server, loop, port_manager from gns3server.version import __version__ diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index eb65610f..623e1b65 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from unittest.mock import patch from tests.api.base import server, loop, port_manager from tests.utils import asyncio_patch from gns3server import modules @@ -29,13 +30,26 @@ def test_vpcs_create(server): assert response.json['vpcs_id'] == 84 -def test_vpcs_nio_create(server): - response = server.post('/vpcs/42/ports/0/nio', { - 'type': 'nio_unix', - 'local_file': '/tmp/test', - 'remote_file': '/tmp/remote' +def test_vpcs_nio_create_udp(server): + vm = server.post('/vpcs', {'name': 'PC TEST 1'}) + response = server.post('/vpcs/{}/ports/0/nio'.format(vm.json["vpcs_id"]), { + 'type': 'nio_udp', + 'lport': 4242, + 'rport': 4343, + 'rhost': '127.0.0.1' }, example=True) assert response.status == 200 assert response.route == '/vpcs/{vpcs_id}/ports/{port_id}/nio' - assert response.json['name'] == 'PC 2' + assert response.json['type'] == 'nio_udp' + +@patch("gns3server.modules.vpcs.vpcs_device.has_privileged_access", return_value=True) +def test_vpcs_nio_create_tap(mock, server): + vm = server.post('/vpcs', {'name': 'PC TEST 1'}) + response = server.post('/vpcs/{}/ports/0/nio'.format(vm.json["vpcs_id"]), { + 'type': 'nio_tap', + 'tap_device': 'test', + }) + assert response.status == 200 + assert response.route == '/vpcs/{vpcs_id}/ports/{port_id}/nio' + assert response.json['type'] == 'nio_tap' From 8e307c8cbb69528776efd54728e6b318660f77d8 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 16 Jan 2015 20:23:43 +0100 Subject: [PATCH 022/485] Use PATH environnement variable for searching binary --- gns3server/modules/base_vm.py | 50 ++------------------------ gns3server/modules/vpcs/vpcs_device.py | 26 ++++++++++---- tests/modules/vpcs/test_vpcs_device.py | 21 ++++++----- 3 files changed, 35 insertions(+), 62 deletions(-) diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index 19571831..5fe85b64 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -19,74 +19,28 @@ import asyncio from .vm_error import VMError from .attic import find_unused_port +from ..config import Config import logging log = logging.getLogger(__name__) class BaseVM: - _allocated_console_ports = [] - def __init__(self, name, identifier, port_manager): self._loop = asyncio.get_event_loop() - self._allocate_console() self._queue = asyncio.Queue() self._name = name self._id = identifier self._created = asyncio.Future() self._worker = asyncio.async(self._run()) self._port_manager = port_manager + self._config = Config.instance() log.info("{type} device {name} [id={id}] has been created".format( type=self.__class__.__name__, name=self._name, id=self._id)) - def _allocate_console(self): - - if not self._console: - # allocate a console port - try: - self._console = find_unused_port(self._console_start_port_range, - self._console_end_port_range, - self._console_host, - ignore_ports=self._allocated_console_ports) - except Exception as e: - raise VMError(e) - - if self._console in self._allocated_console_ports: - raise VMError("Console port {} is already used by another VM".format(self._console)) - self._allocated_console_ports.append(self._console) - - - @property - def console(self): - """ - Returns the TCP console port. - - :returns: console port (integer) - """ - - return self._console - - @console.setter - def console(self, console): - """ - Sets the TCP console port. - - :param console: console port (integer) - """ - - if console in self._allocated_console_ports: - raise VMError("Console port {} is already used by another VM".format(console)) - - self._allocated_console_ports.remove(self._console) - self._console = console - self._allocated_console_ports.append(self._console) - log.info("{type} {name} [id={id}]: console port set to {port}".format(type=self.__class__.__name__, - name=self._name, - id=self._id, - port=console)) @property def id(self): """ diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py index fe079c25..d1531eb2 100644 --- a/gns3server/modules/vpcs/vpcs_device.py +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -28,6 +28,7 @@ import shutil import re import asyncio import socket +import shutil from pkg_resources import parse_version from .vpcs_error import VPCSError @@ -52,17 +53,17 @@ class VPCSDevice(BaseVM): :param console: TCP console port """ def __init__(self, name, vpcs_id, port_manager, - path = None, - working_dir = None, - console=None): + working_dir = None, console = None): + super().__init__(name, vpcs_id, port_manager) #self._path = path #self._working_dir = working_dir # TODO: Hardcodded for testing - self._path = "/usr/local/bin/vpcs" - self._working_dir = "/tmp" + self._path = self._config.get_section_config("VPCS").get("path", "vpcs") + self._working_dir = "/tmp" self._console = console + self._command = [] self._process = None self._vpcs_stdout_file = "" @@ -89,12 +90,15 @@ class VPCSDevice(BaseVM): raise VPCSError(e) self._check_requirements() - super().__init__(name, vpcs_id, port_manager) def _check_requirements(self): """ Check if VPCS is available with the correct version """ + if self._path == "vpcs": + self._path = shutil.which("vpcs") + + if not self._path: raise VPCSError("No path to a VPCS executable has been set") @@ -106,6 +110,16 @@ class VPCSDevice(BaseVM): self._check_vpcs_version() + @property + def console(self): + """ + Returns the console port of this VPCS device. + + :returns: console port + """ + + return self._console + @property def name(self): """ diff --git a/tests/modules/vpcs/test_vpcs_device.py b/tests/modules/vpcs/test_vpcs_device.py index b56ace3f..f5d71268 100644 --- a/tests/modules/vpcs/test_vpcs_device.py +++ b/tests/modules/vpcs/test_vpcs_device.py @@ -28,33 +28,38 @@ from gns3server.modules.vpcs.vpcs_error import VPCSError @patch("subprocess.check_output", return_value="Welcome to Virtual PC Simulator, version 0.6".encode("utf-8")) def test_vm(tmpdir, port_manager): - vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir), path="/bin/test") + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir)) assert vm.name == "test" assert vm.id == 42 @patch("subprocess.check_output", return_value="Welcome to Virtual PC Simulator, version 0.1".encode("utf-8")) def test_vm_invalid_vpcs_version(tmpdir, port_manager): with pytest.raises(VPCSError): - vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir), path="/bin/test") + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir)) assert vm.name == "test" assert vm.id == 42 +@patch("gns3server.config.Config.get_section_config", return_value = {"path": "/bin/test_fake"}) def test_vm_invalid_vpcs_path(tmpdir, port_manager): with pytest.raises(VPCSError): - vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir), path="/bin/test_fake") + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir)) assert vm.name == "test" assert vm.id == 42 def test_start(tmpdir, loop, port_manager): with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): - vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir), path="/bin/test") + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir)) + nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) + loop.run_until_complete(asyncio.async(vm.start())) assert vm.is_running() == True def test_stop(tmpdir, loop, port_manager): process = MagicMock() with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): - vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir), path="/bin/test") + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir)) + nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) + loop.run_until_complete(asyncio.async(vm.start())) assert vm.is_running() == True loop.run_until_complete(asyncio.async(vm.stop())) @@ -62,18 +67,18 @@ def test_stop(tmpdir, loop, port_manager): process.terminate.assert_called_with() def test_add_nio_binding_udp(port_manager, tmpdir): - vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir), path="/bin/test") + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir)) nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) assert nio.lport == 4242 def test_add_nio_binding_tap(port_manager, tmpdir): - vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir), path="/bin/test") + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir)) with patch("gns3server.modules.vpcs.vpcs_device.has_privileged_access", return_value=True): nio = vm.port_add_nio_binding(0, {"type": "nio_tap", "tap_device": "test"}) assert nio.tap_device == "test" def test_add_nio_binding_tap_no_privileged_access(port_manager, tmpdir): - vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir), path="/bin/test") + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir)) with patch("gns3server.modules.vpcs.vpcs_device.has_privileged_access", return_value=False): with pytest.raises(VPCSError): vm.port_add_nio_binding(0, {"type": "nio_tap", "tap_device": "test"}) From 77db08c39e035ec618c8ca402036f68ad51db4df Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 16 Jan 2015 21:39:58 +0100 Subject: [PATCH 023/485] Remove NIO from VPCS --- gns3server/handlers/vpcs_handler.py | 25 ++++++++++++++++++++++--- gns3server/web/route.py | 4 ++++ tests/api/base.py | 3 +++ tests/api/test_vpcs.py | 15 +++++++++++++++ tests/modules/vpcs/test_vpcs_device.py | 7 +++++++ 5 files changed, 51 insertions(+), 3 deletions(-) diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index b12383b5..e34dbfe8 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -79,21 +79,40 @@ class VPCSHandler(object): @Route.post( r"/vpcs/{vpcs_id}/ports/{port_id}/nio", parameters={ - "vpcs_id": "Id of VPCS instance" + "vpcs_id": "Id of VPCS instance", + "port_id": "Id of the port where the nio should be add" }, status_codes={ - 201: "Success of creation of NIO", + 200: "Success of creation of NIO", 404: "If VPCS doesn't exist" }, description="ADD NIO to a VPCS", input=VPCS_NIO_SCHEMA, output=VPCS_NIO_SCHEMA) def create_nio(request, response): - # TODO: response with nio vpcs_manager = VPCS.instance() vm = vpcs_manager.get_vm(int(request.match_info['vpcs_id'])) nio = vm.port_add_nio_binding(int(request.match_info['port_id']), request.json) response.json(nio) + @classmethod + @Route.delete( + r"/vpcs/{vpcs_id}/ports/{port_id}/nio", + parameters={ + "vpcs_id": "Id of VPCS instance", + "port_id": "Id of the port where the nio should be remove" + }, + status_codes={ + 200: "Success of deletin of NIO", + 404: "If VPCS doesn't exist" + }, + description="Remove NIO from a VPCS") + def delete_nio(request, response): + vpcs_manager = VPCS.instance() + vm = vpcs_manager.get_vm(int(request.match_info['vpcs_id'])) + nio = vm.port_remove_nio_binding(int(request.match_info['port_id'])) + response.json({}) + + diff --git a/gns3server/web/route.py b/gns3server/web/route.py index e55edf06..086a6b50 100644 --- a/gns3server/web/route.py +++ b/gns3server/web/route.py @@ -70,6 +70,10 @@ class Route(object): def put(cls, path, *args, **kw): return cls._route('PUT', path, *args, **kw) + @classmethod + def delete(cls, path, *args, **kw): + return cls._route('DELETE', path, *args, **kw) + @classmethod def _route(cls, method, path, *args, **kw): # This block is executed only the first time diff --git a/tests/api/base.py b/tests/api/base.py index a95d36c2..b8f5f395 100644 --- a/tests/api/base.py +++ b/tests/api/base.py @@ -44,6 +44,9 @@ class Query: def get(self, path, **kwargs): return self._fetch("GET", path, **kwargs) + def delete(self, path, **kwargs): + return self._fetch("DELETE", path, **kwargs) + def _get_url(self, path): return "http://{}:{}{}".format(self._host, self._port, path) diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index 623e1b65..d3ea87a1 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -53,3 +53,18 @@ def test_vpcs_nio_create_tap(mock, server): assert response.status == 200 assert response.route == '/vpcs/{vpcs_id}/ports/{port_id}/nio' assert response.json['type'] == 'nio_tap' + +def test_vpcs_delete_nio(server): + vm = server.post('/vpcs', {'name': 'PC TEST 1'}) + response = server.post('/vpcs/{}/ports/0/nio'.format(vm.json["vpcs_id"]), { + 'type': 'nio_udp', + 'lport': 4242, + 'rport': 4343, + 'rhost': '127.0.0.1' + }, + ) + response = server.delete('/vpcs/{}/ports/0/nio'.format(vm.json["vpcs_id"])) + assert response.status == 200 + assert response.route == '/vpcs/{vpcs_id}/ports/{port_id}/nio' + + diff --git a/tests/modules/vpcs/test_vpcs_device.py b/tests/modules/vpcs/test_vpcs_device.py index f5d71268..4844e820 100644 --- a/tests/modules/vpcs/test_vpcs_device.py +++ b/tests/modules/vpcs/test_vpcs_device.py @@ -82,3 +82,10 @@ def test_add_nio_binding_tap_no_privileged_access(port_manager, tmpdir): with patch("gns3server.modules.vpcs.vpcs_device.has_privileged_access", return_value=False): with pytest.raises(VPCSError): vm.port_add_nio_binding(0, {"type": "nio_tap", "tap_device": "test"}) + assert vm._ethernet_adapter.ports[0] is not None + +def test_port_remove_nio_binding(port_manager, tmpdir): + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir)) + nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) + vm.port_remove_nio_binding(0) + assert vm._ethernet_adapter.ports[0] == None From 42920e505944e0aa06eda7ae28e3a98240a38df7 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 16 Jan 2015 21:44:56 +0100 Subject: [PATCH 024/485] Haiku theme --- .../examples/delete_vpcsvpcsidportsportidnio.txt | 15 +++++++++++++++ docs/conf.py | 3 ++- tests/api/test_vpcs.py | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 docs/api/examples/delete_vpcsvpcsidportsportidnio.txt diff --git a/docs/api/examples/delete_vpcsvpcsidportsportidnio.txt b/docs/api/examples/delete_vpcsvpcsidportsportidnio.txt new file mode 100644 index 00000000..37bc3fda --- /dev/null +++ b/docs/api/examples/delete_vpcsvpcsidportsportidnio.txt @@ -0,0 +1,15 @@ +curl -i -xDELETE 'http://localhost:8000/vpcs/{vpcs_id}/ports/{port_id}/nio' + +DELETE /vpcs/{vpcs_id}/ports/{port_id}/nio HTTP/1.1 + + + +HTTP/1.1 200 +CONNECTION: close +CONTENT-LENGTH: 2 +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}/ports/{port_id}/nio + +{} diff --git a/docs/conf.py b/docs/conf.py index 346e586a..563979c4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -101,7 +101,8 @@ pygments_style = 'sphinx' # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +#html_theme = 'default' +html_theme = 'haiku' # 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 diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index d3ea87a1..db56a014 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -63,7 +63,7 @@ def test_vpcs_delete_nio(server): 'rhost': '127.0.0.1' }, ) - response = server.delete('/vpcs/{}/ports/0/nio'.format(vm.json["vpcs_id"])) + response = server.delete('/vpcs/{}/ports/0/nio'.format(vm.json["vpcs_id"]), example=True) assert response.status == 200 assert response.route == '/vpcs/{vpcs_id}/ports/{port_id}/nio' From 878532325a0a326ea2b9fce88ff45fed7c5782bf Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 16 Jan 2015 21:48:03 +0100 Subject: [PATCH 025/485] Nature --- docs/api/sleep.rst | 13 ------------- docs/api/stream.rst | 13 ------------- docs/conf.py | 2 +- 3 files changed, 1 insertion(+), 27 deletions(-) delete mode 100644 docs/api/sleep.rst delete mode 100644 docs/api/stream.rst diff --git a/docs/api/sleep.rst b/docs/api/sleep.rst deleted file mode 100644 index fbf7845d..00000000 --- a/docs/api/sleep.rst +++ /dev/null @@ -1,13 +0,0 @@ -/sleep ------------------------------- - -.. contents:: - -GET /sleep -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - -Response status codes -************************** -- **200**: OK - diff --git a/docs/api/stream.rst b/docs/api/stream.rst deleted file mode 100644 index 00180c62..00000000 --- a/docs/api/stream.rst +++ /dev/null @@ -1,13 +0,0 @@ -/stream ------------------------------- - -.. contents:: - -GET /stream -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - -Response status codes -************************** -- **200**: OK - diff --git a/docs/conf.py b/docs/conf.py index 563979c4..27de1def 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -102,7 +102,7 @@ pygments_style = 'sphinx' # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. #html_theme = 'default' -html_theme = 'haiku' +html_theme = 'nature' # 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 From 2c9a802ccab28da0f70e3ed1f7d30620ed08f475 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 16 Jan 2015 21:53:04 +0100 Subject: [PATCH 026/485] Default documentation theme --- docs/conf.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 27de1def..684c7297 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -90,6 +90,7 @@ exclude_patterns = ['_build'] # 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 = [] @@ -101,8 +102,9 @@ pygments_style = 'sphinx' # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -#html_theme = 'default' -html_theme = 'nature' +html_theme = 'default' +#html_theme = 'nature' +using_rtd_theme=False # 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 From 9f82f3826b9472b6cb0d590e01aacfb05c334e05 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 16 Jan 2015 21:58:02 +0100 Subject: [PATCH 027/485] Default doc style --- docs/conf.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 684c7297..6b87c66e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -104,7 +104,9 @@ pygments_style = 'sphinx' # a list of builtin themes. html_theme = 'default' #html_theme = 'nature' -using_rtd_theme=False + +#If uncommented it's turn off the default read the doc style +html_style = "/default.css" # 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 From 869ad026ffaa85b7a3d77cdd3153fe05c15519f8 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 16 Jan 2015 21:59:51 +0100 Subject: [PATCH 028/485] Do not add a show source in documenation --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 6b87c66e..73bef3c9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -167,7 +167,7 @@ html_static_path = ['_static'] # html_split_index = False # If true, links to the reST sources are added to the pages. -# html_show_sourcelink = True +html_show_sourcelink = False # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True From d142a9a88528d757b2b0943f9b9adb39ede128bc Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sun, 18 Jan 2015 12:12:11 -0700 Subject: [PATCH 029/485] Rename vpcs_id to id. Must be an integer in the route definition. --- gns3server/handlers/vpcs_handler.py | 60 ++++++++++++++++------------- gns3server/schemas/vpcs.py | 6 +-- tests/__init__.py | 0 tests/api/test_version.py | 1 - tests/api/test_vpcs.py | 6 +-- 5 files changed, 39 insertions(+), 34 deletions(-) create mode 100644 tests/__init__.py diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index 5213c27e..86d97144 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -23,90 +23,98 @@ from ..modules.vpcs import VPCS class VPCSHandler(object): + """ + API entry points for VPCS. + """ + @classmethod @Route.post( r"/vpcs", status_codes={ - 201: "Success of creation of VPCS", + 201: "VPCS instance created", 409: "Conflict" }, - description="Create a new VPCS and return it", + description="Create a new VPCS instance", input=VPCS_CREATE_SCHEMA, output=VPCS_OBJECT_SCHEMA) def create(request, response): + vpcs = VPCS.instance() - vm = yield from vpcs.create_vm(request.json['name']) - response.json({'name': vm.name, - "vpcs_id": vm.id, + vm = yield from vpcs.create_vm(request.json["name"]) + response.json({"name": vm.name, + "id": vm.id, "console": vm.console}) @classmethod @Route.post( - r"/vpcs/{vpcs_id}/start", + r"/vpcs/{id:\d+}/start", parameters={ - "vpcs_id": "Id of VPCS instance" + "id": "VPCS instance ID" }, status_codes={ - 201: "Success of creation of VPCS", + 204: "VPCS instance started", }, - description="Start VPCS", - ) + description="Start a VPCS instance") def create(request, response): + vpcs_manager = VPCS.instance() - vm = yield from vpcs_manager.start_vm(int(request.match_info['vpcs_id'])) + yield from vpcs_manager.start_vm(int(request.match_info["id"])) response.json({}) @classmethod @Route.post( - r"/vpcs/{vpcs_id}/stop", + r"/vpcs/{id:\d+}/stop", parameters={ - "vpcs_id": "Id of VPCS instance" + "id": "VPCS instance ID" }, status_codes={ 201: "Success of stopping VPCS", }, - description="Stop VPCS", - ) + description="Stop a VPCS instance") def create(request, response): + vpcs_manager = VPCS.instance() - vm = yield from vpcs_manager.stop_vm(int(request.match_info['vpcs_id'])) + yield from vpcs_manager.stop_vm(int(request.match_info["id"])) response.json({}) @classmethod @Route.get( - r"/vpcs/{vpcs_id}", + r"/vpcs/{id:\d+}", parameters={ - "vpcs_id": "Id of VPCS instance" + "id": "VPCS instance ID" }, description="Get information about a VPCS", output=VPCS_OBJECT_SCHEMA) def show(request, response): - response.json({'name': "PC 1", "vpcs_id": 42, "console": 4242}) + + response.json({'name': "PC 1", "id": 42, "console": 4242}) @classmethod @Route.put( - r"/vpcs/{vpcs_id}", + r"/vpcs/{id:\d+}", parameters={ - "vpcs_id": "Id of VPCS instance" + "id": "VPCS instance ID" }, 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}) + + response.json({'name': "PC 1", "id": 42, "console": 4242}) @classmethod @Route.post( - r"/vpcs/{vpcs_id}/nio", + r"/vpcs/{id:\d+}/nio", parameters={ - "vpcs_id": "Id of VPCS instance" + "id": "VPCS instance ID" }, status_codes={ - 201: "Success of creation of NIO", + 201: "NIO created", 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}) + response.json({'name': "PC 2", "id": 42, "console": 4242}) diff --git a/gns3server/schemas/vpcs.py b/gns3server/schemas/vpcs.py index 7d205391..2947efba 100644 --- a/gns3server/schemas/vpcs.py +++ b/gns3server/schemas/vpcs.py @@ -26,7 +26,7 @@ VPCS_CREATE_SCHEMA = { "type": "string", "minLength": 1, }, - "vpcs_id": { + "id": { "description": "VPCS device instance ID", "type": "integer" }, @@ -215,7 +215,7 @@ VPCS_OBJECT_SCHEMA = { "type": "string", "minLength": 1, }, - "vpcs_id": { + "id": { "description": "VPCS device instance ID", "type": "integer" }, @@ -227,7 +227,7 @@ VPCS_OBJECT_SCHEMA = { }, }, "additionalProperties": False, - "required": ["name", "vpcs_id", "console"] + "required": ["name", "id", "console"] } VBOX_CREATE_SCHEMA = { diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/api/test_version.py b/tests/api/test_version.py index a052bb43..35d0f5b8 100644 --- a/tests/api/test_version.py +++ b/tests/api/test_version.py @@ -21,7 +21,6 @@ It's also used for unittest the HTTP implementation. """ from tests.utils import asyncio_patch -from tests.api.base import server, loop from gns3server.version import __version__ diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index ccecd96f..6abb5c3c 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -15,9 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from tests.api.base import server, loop from tests.utils import asyncio_patch -from gns3server import modules @asyncio_patch('gns3server.modules.VPCS.create_vm', return_value=84) @@ -26,7 +24,7 @@ def test_vpcs_create(server): assert response.status == 200 assert response.route == '/vpcs' assert response.json['name'] == 'PC TEST 1' - assert response.json['vpcs_id'] == 84 + assert response.json['id'] == 84 def test_vpcs_nio_create(server): @@ -41,5 +39,5 @@ def test_vpcs_nio_create(server): 'port_id': 0}, example=True) assert response.status == 200 - assert response.route == '/vpcs/{vpcs_id}/nio' + assert response.route == '/vpcs/{id}/nio' assert response.json['name'] == 'PC 2' From 190096675193527f2a1609ed61f2446c592a5a86 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sun, 18 Jan 2015 13:58:19 -0700 Subject: [PATCH 030/485] Update documentation script to use Python3. --- dev-requirements.txt | 2 +- scripts/documentation.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 58c218e8..9ed5ac2a 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,6 +1,6 @@ -rrequirements.txt -Sphinx==1.2.3 +sphinx==1.2.3 pytest==2.6.4 ws4py==0.3.4 pep8==1.5.7 diff --git a/scripts/documentation.sh b/scripts/documentation.sh index 67f10e6d..92d458f7 100755 --- a/scripts/documentation.sh +++ b/scripts/documentation.sh @@ -22,6 +22,6 @@ set -e py.test -python ../gns3server/web/documentation.py +python3 ../gns3server/web/documentation.py cd ../docs make html From b6212fc8852b28e2b361f287fae26f578965bf14 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sun, 18 Jan 2015 15:41:53 -0700 Subject: [PATCH 031/485] Improve Port Manager to handle UDP ports. --- gns3server/modules/base_manager.py | 2 +- gns3server/modules/base_vm.py | 30 ++-- gns3server/modules/port_manager.py | 193 ++++++++++++++++++++----- gns3server/modules/vpcs/vpcs_device.py | 7 +- gns3server/schemas/vpcs.py | 7 + 5 files changed, 189 insertions(+), 50 deletions(-) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 36d031a6..c57d910e 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -34,7 +34,7 @@ class BaseManager: @classmethod def instance(cls): """ - Singleton to return only one instance of Manager. + Singleton to return only one instance of BaseManager. :returns: instance of Manager """ diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index 5fe85b64..17ef0ff5 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -18,7 +18,6 @@ import asyncio from .vm_error import VMError -from .attic import find_unused_port from ..config import Config import logging @@ -26,6 +25,7 @@ log = logging.getLogger(__name__) class BaseVM: + def __init__(self, name, identifier, port_manager): self._loop = asyncio.get_event_loop() @@ -33,13 +33,12 @@ class BaseVM: self._name = name self._id = identifier self._created = asyncio.Future() - self._worker = asyncio.async(self._run()) self._port_manager = port_manager self._config = Config.instance() - log.info("{type} device {name} [id={id}] has been created".format( - type=self.__class__.__name__, - name=self._name, - id=self._id)) + self._worker = asyncio.async(self._run()) + log.info("{type} device {name} [id={id}] has been created".format(type=self.__class__.__name__, + name=self._name, + id=self._id)) @property def id(self): @@ -62,13 +61,19 @@ class BaseVM: return self._name @asyncio.coroutine - def _execute(self, subcommand, args): - """Called when we receive an event""" + def _execute(self, command): + """ + Called when we receive an event. + """ + raise NotImplementedError @asyncio.coroutine def _create(self): - """Called when the run loop start""" + """ + Called when the run loop start + """ + raise NotImplementedError @asyncio.coroutine @@ -82,12 +87,12 @@ class BaseVM: return while True: - future, subcommand, args = yield from self._queue.get() + future, command = yield from self._queue.get() try: try: - yield from asyncio.wait_for(self._execute(subcommand, args), timeout=timeout) + yield from asyncio.wait_for(self._execute(command), timeout=timeout) except asyncio.TimeoutError: - raise VMError("{} has timed out after {} seconds!".format(subcommand, timeout)) + raise VMError("{} has timed out after {} seconds!".format(command, timeout)) future.set_result(True) except Exception as e: future.set_exception(e) @@ -100,6 +105,7 @@ class BaseVM: """ Starts the VM process. """ + raise NotImplementedError def put(self, *args): diff --git a/gns3server/modules/port_manager.py b/gns3server/modules/port_manager.py index 59aed442..8093538e 100644 --- a/gns3server/modules/port_manager.py +++ b/gns3server/modules/port_manager.py @@ -15,61 +15,186 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import socket import ipaddress -from .attic import find_unused_port class PortManager: """ - :param console: TCP console port - :param console_host: IP address to bind for console connections - :param console_start_port_range: TCP console port range start - :param console_end_port_range: TCP console port range end + :param host: IP address to bind for console connections """ - def __init__(self, - console_host, - console_bind_to_any, - console_start_port_range=10000, - console_end_port_range=15000): - self._console_start_port_range = console_start_port_range - self._console_end_port_range = console_end_port_range - self._used_ports = set() + def __init__(self, host="127.0.0.1", console_bind_to_any=False): + + self._console_host = host + self._udp_host = host + self._console_port_range = (2000, 4000) + self._udp_port_range = (10000, 20000) + + self._used_tcp_ports = set() + self._used_udp_ports = set() if console_bind_to_any: - if ipaddress.ip_address(console_host).version == 6: + if ipaddress.ip_address(host).version == 6: self._console_host = "::" else: self._console_host = "0.0.0.0" else: - self._console_host = console_host - - def get_free_port(self): - """Get an available console port and reserve it""" - port = find_unused_port(self._console_start_port_range, - self._console_end_port_range, - host=self._console_host, - socket_type='TCP', - ignore_ports=self._used_ports) - self._used_ports.add(port) + self._console_host = host + + @property + def console_host(self): + + return self._console_host + + @console_host.setter + def host(self, new_host): + + self._console_host = new_host + + @property + def console_port_range(self): + + return self._console_port_range + + @console_host.setter + def console_port_range(self, new_range): + + assert isinstance(new_range, tuple) + self._console_port_range = new_range + + @property + def udp_host(self): + + return self._udp_host + + @udp_host.setter + def host(self, new_host): + + self._udp_host = new_host + + @property + def udp_port_range(self): + + return self._udp_port_range + + @udp_host.setter + def udp_port_range(self, new_range): + + assert isinstance(new_range, tuple) + self._udp_port_range = new_range + + @staticmethod + def find_unused_port(start_port, end_port, host="127.0.0.1", socket_type="TCP", ignore_ports=[]): + """ + Finds an unused port in a range. + + :param start_port: first port in the range + :param end_port: last port in the range + :param host: host/address for bind() + :param socket_type: TCP (default) or UDP + :param ignore_ports: list of port to ignore within the range + """ + + if end_port < start_port: + raise Exception("Invalid port range {}-{}".format(start_port, end_port)) + + if socket_type == "UDP": + socket_type = socket.SOCK_DGRAM + else: + socket_type = socket.SOCK_STREAM + + last_exception = None + for port in range(start_port, end_port + 1): + if port in ignore_ports: + continue + try: + if ":" in host: + # IPv6 address support + with socket.socket(socket.AF_INET6, socket_type) as s: + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind((host, port)) # the port is available if bind is a success + else: + with socket.socket(socket.AF_INET, socket_type) as s: + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind((host, port)) # the port is available if bind is a success + return port + except OSError as e: + last_exception = e + if port + 1 == end_port: + break + else: + continue + + raise Exception("Could not find a free port between {} and {} on host {}, last exception: {}".format(start_port, + end_port, + host, + last_exception)) + + def get_free_console_port(self): + """ + Get an available TCP console port and reserve it + """ + + port = self.find_unused_port(self._console_port_range[0], + self._console_port_range[1], + host=self._console_host, + socket_type="TCP", + ignore_ports=self._used_tcp_ports) + + self._used_tcp_ports.add(port) return port - def reserve_port(self, port): + def reserve_console_port(self, port): + """ + Reserve a specific TCP console port number + + :param port: TCP port number + """ + + if port in self._used_tcp_ports: + raise Exception("TCP port already {} in use on host".format(port, self._host)) + self._used_tcp_ports.add(port) + + def release_console_port(self, port): + """ + Release a specific TCP console port number + + :param port: TCP port number + """ + + self._used_tcp_ports.remove(port) + + def get_free_udp_port(self): """ - Reserve a specific port number + Get an available UDP port and reserve it + """ + + port = self.find_unused_port(self._udp_port_range[0], + self._udp_port_range[1], + host=self._udp_host, + socket_type="UDP", + ignore_ports=self._used_udp_ports) + + self._used_udp_ports.add(port) + return port - :param port: Port number + def reserve_udp_port(self, port): """ - if port in self._used_ports: - raise Exception("Port already {} in use".format(port)) - self._used_ports.add(port) + Reserve a specific UDP port number - def release_port(self, port): + :param port: UDP port number """ - Release a specific port number - :param port: Port number + if port in self._used_udp_ports: + raise Exception("UDP port already {} in use on host".format(port, self._host)) + self._used_udp_ports.add(port) + + def release_udp_port(self, port): """ + Release a specific UDP port number - self._used_ports.remove(port) + :param port: UDP port number + """ + self._used_udp_ports.remove(port) diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py index d1531eb2..7d5ed339 100644 --- a/gns3server/modules/vpcs/vpcs_device.py +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -42,6 +42,7 @@ from ..base_vm import BaseVM import logging log = logging.getLogger(__name__) + class VPCSDevice(BaseVM): """ VPCS device implementation. @@ -52,8 +53,8 @@ class VPCSDevice(BaseVM): :param working_dir: path to a working directory :param console: TCP console port """ - def __init__(self, name, vpcs_id, port_manager, - working_dir = None, console = None): + def __init__(self, name, vpcs_id, port_manager, working_dir=None, console=None): + super().__init__(name, vpcs_id, port_manager) #self._path = path @@ -95,10 +96,10 @@ class VPCSDevice(BaseVM): """ Check if VPCS is available with the correct version """ + if self._path == "vpcs": self._path = shutil.which("vpcs") - if not self._path: raise VPCSError("No path to a VPCS executable has been set") diff --git a/gns3server/schemas/vpcs.py b/gns3server/schemas/vpcs.py index a2819471..c4b7c71c 100644 --- a/gns3server/schemas/vpcs.py +++ b/gns3server/schemas/vpcs.py @@ -30,6 +30,13 @@ VPCS_CREATE_SCHEMA = { "description": "VPCS device instance ID", "type": "integer" }, + "uuid": { + "description": "VPCS device UUID", + "type": "string", + "minLength": 36, + "maxLength": 36, + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + }, "console": { "description": "console TCP port", "minimum": 1, From ae8e2f4199629311e3df9fe29586350129a7db24 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sun, 18 Jan 2015 16:26:56 -0700 Subject: [PATCH 032/485] Prepare VirtualBox module. --- .../{virtualbox => }/adapters/__init__.py | 0 .../{virtualbox => }/adapters/adapter.py | 0 .../{vpcs => }/adapters/ethernet_adapter.py | 0 .../modules/{virtualbox => }/nios/__init__.py | 0 gns3server/modules/{vpcs => }/nios/nio_tap.py | 1 + gns3server/modules/{vpcs => }/nios/nio_udp.py | 1 + gns3server/modules/virtualbox/__init__.py | 780 +----------------- .../virtualbox/adapters/ethernet_adapter.py | 31 - gns3server/modules/virtualbox/nios/nio.py | 65 -- gns3server/modules/virtualbox/nios/nio_udp.py | 75 -- .../modules/virtualbox/virtualbox_error.py | 6 +- .../modules/virtualbox/virtualbox_vm.py | 2 +- gns3server/modules/vpcs/adapters/__init__.py | 0 gns3server/modules/vpcs/adapters/adapter.py | 104 --- gns3server/modules/vpcs/nios/__init__.py | 0 gns3server/modules/vpcs/vpcs_device.py | 7 +- .../schemas.py => schemas/virtualbox.py} | 0 17 files changed, 14 insertions(+), 1058 deletions(-) rename gns3server/modules/{virtualbox => }/adapters/__init__.py (100%) rename gns3server/modules/{virtualbox => }/adapters/adapter.py (100%) rename gns3server/modules/{vpcs => }/adapters/ethernet_adapter.py (100%) rename gns3server/modules/{virtualbox => }/nios/__init__.py (100%) rename gns3server/modules/{vpcs => }/nios/nio_tap.py (99%) rename gns3server/modules/{vpcs => }/nios/nio_udp.py (99%) delete mode 100644 gns3server/modules/virtualbox/adapters/ethernet_adapter.py delete mode 100644 gns3server/modules/virtualbox/nios/nio.py delete mode 100644 gns3server/modules/virtualbox/nios/nio_udp.py delete mode 100644 gns3server/modules/vpcs/adapters/__init__.py delete mode 100644 gns3server/modules/vpcs/adapters/adapter.py delete mode 100644 gns3server/modules/vpcs/nios/__init__.py rename gns3server/{modules/virtualbox/schemas.py => schemas/virtualbox.py} (100%) diff --git a/gns3server/modules/virtualbox/adapters/__init__.py b/gns3server/modules/adapters/__init__.py similarity index 100% rename from gns3server/modules/virtualbox/adapters/__init__.py rename to gns3server/modules/adapters/__init__.py diff --git a/gns3server/modules/virtualbox/adapters/adapter.py b/gns3server/modules/adapters/adapter.py similarity index 100% rename from gns3server/modules/virtualbox/adapters/adapter.py rename to gns3server/modules/adapters/adapter.py diff --git a/gns3server/modules/vpcs/adapters/ethernet_adapter.py b/gns3server/modules/adapters/ethernet_adapter.py similarity index 100% rename from gns3server/modules/vpcs/adapters/ethernet_adapter.py rename to gns3server/modules/adapters/ethernet_adapter.py diff --git a/gns3server/modules/virtualbox/nios/__init__.py b/gns3server/modules/nios/__init__.py similarity index 100% rename from gns3server/modules/virtualbox/nios/__init__.py rename to gns3server/modules/nios/__init__.py diff --git a/gns3server/modules/vpcs/nios/nio_tap.py b/gns3server/modules/nios/nio_tap.py similarity index 99% rename from gns3server/modules/vpcs/nios/nio_tap.py rename to gns3server/modules/nios/nio_tap.py index 39923a01..85d89990 100644 --- a/gns3server/modules/vpcs/nios/nio_tap.py +++ b/gns3server/modules/nios/nio_tap.py @@ -46,4 +46,5 @@ class NIO_TAP(object): return "NIO TAP" def __json__(self): + return {"type": "nio_tap", "tap_device": self._tap_device} diff --git a/gns3server/modules/vpcs/nios/nio_udp.py b/gns3server/modules/nios/nio_udp.py similarity index 99% rename from gns3server/modules/vpcs/nios/nio_udp.py rename to gns3server/modules/nios/nio_udp.py index cca313e7..f499ca7e 100644 --- a/gns3server/modules/vpcs/nios/nio_udp.py +++ b/gns3server/modules/nios/nio_udp.py @@ -72,4 +72,5 @@ class NIO_UDP(object): return "NIO UDP" def __json__(self): + return {"type": "nio_udp", "lport": self._lport, "rport": self._rport, "rhost": self._rhost} diff --git a/gns3server/modules/virtualbox/__init__.py b/gns3server/modules/virtualbox/__init__.py index 09f8054e..072d8ca9 100644 --- a/gns3server/modules/virtualbox/__init__.py +++ b/gns3server/modules/virtualbox/__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,781 +19,9 @@ VirtualBox server module. """ -import sys -import os -import socket -import shutil -import subprocess - -from gns3server.modules import IModule -from gns3server.config import Config +from ..base_manager import BaseManager from .virtualbox_vm import VirtualBoxVM -from .virtualbox_error import VirtualBoxError -from .nios.nio_udp import NIO_UDP -from ..attic import find_unused_port - -from .schemas import VBOX_CREATE_SCHEMA -from .schemas import VBOX_DELETE_SCHEMA -from .schemas import VBOX_UPDATE_SCHEMA -from .schemas import VBOX_START_SCHEMA -from .schemas import VBOX_STOP_SCHEMA -from .schemas import VBOX_SUSPEND_SCHEMA -from .schemas import VBOX_RELOAD_SCHEMA -from .schemas import VBOX_ALLOCATE_UDP_PORT_SCHEMA -from .schemas import VBOX_ADD_NIO_SCHEMA -from .schemas import VBOX_DELETE_NIO_SCHEMA -from .schemas import VBOX_START_CAPTURE_SCHEMA -from .schemas import VBOX_STOP_CAPTURE_SCHEMA - -import logging -log = logging.getLogger(__name__) - - -class VirtualBox(IModule): - """ - VirtualBox 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 vboxmanage location - self._vboxmanage_path = None - if sys.platform.startswith("win"): - if "VBOX_INSTALL_PATH" in os.environ: - self._vboxmanage_path = os.path.join(os.environ["VBOX_INSTALL_PATH"], "VBoxManage.exe") - elif "VBOX_MSI_INSTALL_PATH" in os.environ: - self._vboxmanage_path = os.path.join(os.environ["VBOX_MSI_INSTALL_PATH"], "VBoxManage.exe") - elif sys.platform.startswith("darwin"): - self._vboxmanage_path = "/Applications/VirtualBox.app/Contents/MacOS/VBoxManage" - else: - config = Config.instance() - vbox_config = config.get_section_config(name.upper()) - self._vboxmanage_path = vbox_config.get("vboxmanage_path") - if not self._vboxmanage_path or not os.path.isfile(self._vboxmanage_path): - paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep) - # look for vboxmanage in the current working directory and $PATH - for path in paths: - try: - if "vboxmanage" in [s.lower() for s in os.listdir(path)] and os.access(os.path.join(path, "vboxmanage"), os.X_OK): - self._vboxmanage_path = os.path.join(path, "vboxmanage") - break - except OSError: - continue - - if not self._vboxmanage_path: - log.warning("vboxmanage couldn't be found!") - elif not os.access(self._vboxmanage_path, os.X_OK): - log.warning("vboxmanage is not executable") - - self._vbox_user = None - - # a new process start when calling IModule - IModule.__init__(self, name, *args, **kwargs) - self._vbox_instances = {} - - config = Config.instance() - vbox_config = config.get_section_config(name.upper()) - self._console_start_port_range = vbox_config.get("console_start_port_range", 3501) - self._console_end_port_range = vbox_config.get("console_end_port_range", 4000) - self._allocated_udp_ports = [] - self._udp_start_port_range = vbox_config.get("udp_start_port_range", 35001) - self._udp_end_port_range = vbox_config.get("udp_end_port_range", 35500) - self._host = vbox_config.get("host", kwargs["host"]) - self._console_host = vbox_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 VirtualBox instances - for vbox_id in self._vbox_instances: - vbox_instance = self._vbox_instances[vbox_id] - try: - vbox_instance.delete() - except VirtualBoxError: - continue - - IModule.stop(self, signum) # this will stop the I/O loop - - def get_vbox_instance(self, vbox_id): - """ - Returns a VirtualBox VM instance. - - :param vbox_id: VirtualBox VM identifier - - :returns: VirtualBoxVM instance - """ - - if vbox_id not in self._vbox_instances: - log.debug("VirtualBox VM ID {} doesn't exist".format(vbox_id), exc_info=1) - self.send_custom_error("VirtualBox VM ID {} doesn't exist".format(vbox_id)) - return None - return self._vbox_instances[vbox_id] - - @IModule.route("virtualbox.reset") - def reset(self, request): - """ - Resets the module. - - :param request: JSON request - """ - - # delete all VirtualBox instances - for vbox_id in self._vbox_instances: - vbox_instance = self._vbox_instances[vbox_id] - vbox_instance.delete() - - # resets the instance IDs - VirtualBoxVM.reset() - - self._vbox_instances.clear() - self._allocated_udp_ports.clear() - - self._working_dir = self._projects_dir - log.info("VirtualBox module has been reset") - - @IModule.route("virtualbox.settings") - def settings(self, request): - """ - Set or update settings. - - Optional request parameters: - - working_dir (path to a working directory) - - vboxmanage_path (path to vboxmanage) - - 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 "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 vbox_id in self._vbox_instances: - vbox_instance = self._vbox_instances[vbox_id] - vbox_instance.working_dir = os.path.join(self._working_dir, "vbox", "{}".format(vbox_instance.name)) - - if "vboxmanage_path" in request: - self._vboxmanage_path = request["vboxmanage_path"] - - if "vbox_user" in request: - self._vbox_user = request["vbox_user"] - - 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("virtualbox.create") - def vbox_create(self, request): - """ - Creates a new VirtualBox VM instance. - - Mandatory request parameters: - - name (VirtualBox VM name) - - vmname (VirtualBox VM name in VirtualBox) - - linked_clone (Flag to create a linked clone) - - Optional request parameters: - - console (VirtualBox VM console port) - - Response parameters: - - id (VirtualBox VM instance identifier) - - name (VirtualBox VM name) - - default settings - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VBOX_CREATE_SCHEMA): - return - - name = request["name"] - vmname = request["vmname"] - linked_clone = request["linked_clone"] - console = request.get("console") - vbox_id = request.get("vbox_id") - - try: - - if not self._vboxmanage_path or not os.path.exists(self._vboxmanage_path): - raise VirtualBoxError("Could not find VBoxManage, is VirtualBox correctly installed?") - - vbox_instance = VirtualBoxVM(self._vboxmanage_path, - self._vbox_user, - name, - vmname, - linked_clone, - self._working_dir, - vbox_id, - console, - self._console_host, - self._console_start_port_range, - self._console_end_port_range) - - except VirtualBoxError as e: - self.send_custom_error(str(e)) - return - - response = {"name": vbox_instance.name, - "id": vbox_instance.id} - - defaults = vbox_instance.defaults() - response.update(defaults) - self._vbox_instances[vbox_instance.id] = vbox_instance - self.send_response(response) - - @IModule.route("virtualbox.delete") - def vbox_delete(self, request): - """ - Deletes a VirtualBox VM instance. - - Mandatory request parameters: - - id (VirtualBox VM instance identifier) - - Response parameter: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VBOX_DELETE_SCHEMA): - return - - # get the instance - vbox_instance = self.get_vbox_instance(request["id"]) - if not vbox_instance: - return - - try: - vbox_instance.clean_delete() - del self._vbox_instances[request["id"]] - except VirtualBoxError as e: - self.send_custom_error(str(e)) - return - - self.send_response(True) - - @IModule.route("virtualbox.update") - def vbox_update(self, request): - """ - Updates a VirtualBox VM instance - - Mandatory request parameters: - - id (VirtualBox VM instance identifier) - - Optional request parameters: - - any setting to update - - Response parameters: - - updated settings - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VBOX_UPDATE_SCHEMA): - return - - # get the instance - vbox_instance = self.get_vbox_instance(request["id"]) - if not vbox_instance: - return - - # update the VirtualBox VM settings - response = {} - for name, value in request.items(): - if hasattr(vbox_instance, name) and getattr(vbox_instance, name) != value: - try: - setattr(vbox_instance, name, value) - response[name] = value - except VirtualBoxError as e: - self.send_custom_error(str(e)) - return - - self.send_response(response) - - @IModule.route("virtualbox.start") - def vbox_start(self, request): - """ - Starts a VirtualBox VM instance. - - Mandatory request parameters: - - id (VirtualBox VM instance identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VBOX_START_SCHEMA): - return - - # get the instance - vbox_instance = self.get_vbox_instance(request["id"]) - if not vbox_instance: - return - - try: - vbox_instance.start() - except VirtualBoxError as e: - self.send_custom_error(str(e)) - return - self.send_response(True) - - @IModule.route("virtualbox.stop") - def vbox_stop(self, request): - """ - Stops a VirtualBox VM instance. - - Mandatory request parameters: - - id (VirtualBox VM instance identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VBOX_STOP_SCHEMA): - return - - # get the instance - vbox_instance = self.get_vbox_instance(request["id"]) - if not vbox_instance: - return - - try: - vbox_instance.stop() - except VirtualBoxError as e: - self.send_custom_error(str(e)) - return - self.send_response(True) - - @IModule.route("virtualbox.reload") - def vbox_reload(self, request): - """ - Reloads a VirtualBox VM instance. - - Mandatory request parameters: - - id (VirtualBox VM identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VBOX_RELOAD_SCHEMA): - return - - # get the instance - vbox_instance = self.get_vbox_instance(request["id"]) - if not vbox_instance: - return - - try: - vbox_instance.reload() - except VirtualBoxError as e: - self.send_custom_error(str(e)) - return - self.send_response(True) - - @IModule.route("virtualbox.stop") - def vbox_stop(self, request): - """ - Stops a VirtualBox VM instance. - - Mandatory request parameters: - - id (VirtualBox VM instance identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VBOX_STOP_SCHEMA): - return - - # get the instance - vbox_instance = self.get_vbox_instance(request["id"]) - if not vbox_instance: - return - - try: - vbox_instance.stop() - except VirtualBoxError as e: - self.send_custom_error(str(e)) - return - self.send_response(True) - - @IModule.route("virtualbox.suspend") - def vbox_suspend(self, request): - """ - Suspends a VirtualBox VM instance. - - Mandatory request parameters: - - id (VirtualBox VM instance identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VBOX_SUSPEND_SCHEMA): - return - - # get the instance - vbox_instance = self.get_vbox_instance(request["id"]) - if not vbox_instance: - return - - try: - vbox_instance.suspend() - except VirtualBoxError as e: - self.send_custom_error(str(e)) - return - self.send_response(True) - - @IModule.route("virtualbox.allocate_udp_port") - def allocate_udp_port(self, request): - """ - Allocates a UDP port in order to create an UDP NIO. - - Mandatory request parameters: - - id (VirtualBox VM 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, VBOX_ALLOCATE_UDP_PORT_SCHEMA): - return - - # get the instance - vbox_instance = self.get_vbox_instance(request["id"]) - if not vbox_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(vbox_instance.name, - vbox_instance.id, - port, - self._host)) - - response = {"lport": port, - "port_id": request["port_id"]} - self.send_response(response) - - @IModule.route("virtualbox.add_nio") - def add_nio(self, request): - """ - Adds an NIO (Network Input/Output) for a VirtualBox VM instance. - - Mandatory request parameters: - - id (VirtualBox VM 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) - - Response parameters: - - port_id (unique port identifier) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VBOX_ADD_NIO_SCHEMA): - return - - # get the instance - vbox_instance = self.get_vbox_instance(request["id"]) - if not vbox_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 VirtualBoxError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e)) - nio = NIO_UDP(lport, rhost, rport) - if not nio: - raise VirtualBoxError("Requested NIO does not exist or is not supported: {}".format(request["nio"]["type"])) - except VirtualBoxError as e: - self.send_custom_error(str(e)) - return - - try: - vbox_instance.port_add_nio_binding(port, nio) - except VirtualBoxError as e: - self.send_custom_error(str(e)) - return - - self.send_response({"port_id": request["port_id"]}) - - @IModule.route("virtualbox.delete_nio") - def delete_nio(self, request): - """ - Deletes an NIO (Network Input/Output). - - Mandatory request parameters: - - id (VirtualBox instance identifier) - - port (port identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VBOX_DELETE_NIO_SCHEMA): - return - - # get the instance - vbox_instance = self.get_vbox_instance(request["id"]) - if not vbox_instance: - return - - port = request["port"] - try: - nio = vbox_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 VirtualBoxError as e: - self.send_custom_error(str(e)) - return - - self.send_response(True) - - @IModule.route("virtualbox.start_capture") - def vbox_start_capture(self, request): - """ - Starts a packet capture. - - Mandatory request parameters: - - id (VirtualBox VM identifier) - - port (port number) - - port_id (port identifier) - - capture_file_name - - Response parameters: - - port_id (port identifier) - - capture_file_path (path to the capture file) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VBOX_START_CAPTURE_SCHEMA): - return - - # get the instance - vbox_instance = self.get_vbox_instance(request["id"]) - if not vbox_instance: - return - - port = request["port"] - capture_file_name = request["capture_file_name"] - - try: - capture_file_path = os.path.join(self._working_dir, "captures", capture_file_name) - vbox_instance.start_capture(port, capture_file_path) - except VirtualBoxError as e: - self.send_custom_error(str(e)) - return - - response = {"port_id": request["port_id"], - "capture_file_path": capture_file_path} - self.send_response(response) - - @IModule.route("virtualbox.stop_capture") - def vbox_stop_capture(self, request): - """ - Stops a packet capture. - - Mandatory request parameters: - - id (VirtualBox VM identifier) - - port (port number) - - port_id (port identifier) - - Response parameters: - - port_id (port identifier) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VBOX_STOP_CAPTURE_SCHEMA): - return - - # get the instance - vbox_instance = self.get_vbox_instance(request["id"]) - if not vbox_instance: - return - - port = request["port"] - try: - vbox_instance.stop_capture(port) - except VirtualBoxError as e: - self.send_custom_error(str(e)) - return - - response = {"port_id": request["port_id"]} - self.send_response(response) - - def _execute_vboxmanage(self, user, command): - """ - Executes VBoxManage and return its result. - - :param command: command to execute (list) - - :returns: VBoxManage output - """ - - try: - if not user.strip() or sys.platform.startswith("win"): - result = subprocess.check_output(command, stderr=subprocess.STDOUT, timeout=60) - else: - sudo_command = "sudo -i -u " + user.strip() + " " + " ".join(command) - result = subprocess.check_output(sudo_command, stderr=subprocess.STDOUT, shell=True, timeout=60) - except (OSError, subprocess.SubprocessError) as e: - raise VirtualBoxError("Could not execute VBoxManage {}".format(e)) - return result.decode("utf-8", errors="ignore") - - @IModule.route("virtualbox.vm_list") - def vm_list(self, request): - """ - Gets VirtualBox VM list. - - Response parameters: - - Server address/host - - List of VM names - """ - - try: - - if request and "vboxmanage_path" in request: - vboxmanage_path = request["vboxmanage_path"] - else: - vboxmanage_path = self._vboxmanage_path - - if request and "vbox_user" in request: - vbox_user = request["vbox_user"] - else: - vbox_user = self._vbox_user - - if not vboxmanage_path or not os.path.exists(vboxmanage_path): - raise VirtualBoxError("Could not find VBoxManage, is VirtualBox correctly installed?") - - command = [vboxmanage_path, "--nologo", "list", "vms"] - result = self._execute_vboxmanage(vbox_user, command) - except VirtualBoxError as e: - self.send_custom_error(str(e)) - return - - vms = [] - for line in result.splitlines(): - vmname, uuid = line.rsplit(' ', 1) - vmname = vmname.strip('"') - if vmname == "": - continue # ignore inaccessible VMs - try: - extra_data = self._execute_vboxmanage(vbox_user, [vboxmanage_path, "getextradata", vmname, "GNS3/Clone"]).strip() - except VirtualBoxError as e: - self.send_custom_error(str(e)) - return - if not extra_data == "Value: yes": - vms.append(vmname) - - response = {"vms": vms} - self.send_response(response) - - @IModule.route("virtualbox.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) +class VirtualBox(BaseManager): + _VM_CLASS = VirtualBoxVM diff --git a/gns3server/modules/virtualbox/adapters/ethernet_adapter.py b/gns3server/modules/virtualbox/adapters/ethernet_adapter.py deleted file mode 100644 index 8951ee8d..00000000 --- a/gns3server/modules/virtualbox/adapters/ethernet_adapter.py +++ /dev/null @@ -1,31 +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 . - -from .adapter import Adapter - - -class EthernetAdapter(Adapter): - """ - VirtualBox Ethernet adapter. - """ - - def __init__(self): - Adapter.__init__(self, interfaces=1) - - def __str__(self): - - return "VirtualBox Ethernet adapter" diff --git a/gns3server/modules/virtualbox/nios/nio.py b/gns3server/modules/virtualbox/nios/nio.py deleted file mode 100644 index eee5f1d5..00000000 --- a/gns3server/modules/virtualbox/nios/nio.py +++ /dev/null @@ -1,65 +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 . - -""" -Base interface for NIOs. -""" - - -class NIO(object): - """ - Network Input/Output. - """ - - def __init__(self): - - self._capturing = False - self._pcap_output_file = "" - - def startPacketCapture(self, pcap_output_file): - """ - - :param pcap_output_file: PCAP destination file for the capture - """ - - self._capturing = True - self._pcap_output_file = pcap_output_file - - def stopPacketCapture(self): - - self._capturing = False - self._pcap_output_file = "" - - @property - def capturing(self): - """ - Returns either a capture is configured on this NIO. - - :returns: boolean - """ - - return self._capturing - - @property - def pcap_output_file(self): - """ - Returns the path to the PCAP output file. - - :returns: path to the PCAP output file - """ - - return self._pcap_output_file diff --git a/gns3server/modules/virtualbox/nios/nio_udp.py b/gns3server/modules/virtualbox/nios/nio_udp.py deleted file mode 100644 index 2c850351..00000000 --- a/gns3server/modules/virtualbox/nios/nio_udp.py +++ /dev/null @@ -1,75 +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 . - -""" -Interface for UDP NIOs. -""" - -from .nio import NIO - - -class NIO_UDP(NIO): - """ - UDP NIO. - - :param lport: local port number - :param rhost: remote address/host - :param rport: remote port number - """ - - _instance_count = 0 - - def __init__(self, lport, rhost, rport): - - NIO.__init__(self) - self._lport = lport - self._rhost = rhost - self._rport = rport - - @property - def lport(self): - """ - Returns the local port - - :returns: local port number - """ - - return self._lport - - @property - def rhost(self): - """ - Returns the remote host - - :returns: remote address/host - """ - - return self._rhost - - @property - def rport(self): - """ - Returns the remote port - - :returns: remote port number - """ - - return self._rport - - def __str__(self): - - return "NIO UDP" diff --git a/gns3server/modules/virtualbox/virtualbox_error.py b/gns3server/modules/virtualbox/virtualbox_error.py index 74b05171..ec05bfb6 100644 --- a/gns3server/modules/virtualbox/virtualbox_error.py +++ b/gns3server/modules/virtualbox/virtualbox_error.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,8 +19,10 @@ Custom exceptions for VirtualBox module. """ +from ..vm_error import VMError -class VirtualBoxError(Exception): + +class VirtualBoxError(VMError): def __init__(self, message, original_exception=None): diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 22294edf..efebb34a 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -30,7 +30,7 @@ import socket import time from .virtualbox_error import VirtualBoxError -from .adapters.ethernet_adapter import EthernetAdapter +from ..adapters.ethernet_adapter import EthernetAdapter from ..attic import find_unused_port from .telnet_server import TelnetServer diff --git a/gns3server/modules/vpcs/adapters/__init__.py b/gns3server/modules/vpcs/adapters/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/gns3server/modules/vpcs/adapters/adapter.py b/gns3server/modules/vpcs/adapters/adapter.py deleted file mode 100644 index cf439427..00000000 --- a/gns3server/modules/vpcs/adapters/adapter.py +++ /dev/null @@ -1,104 +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 . - - -class Adapter(object): - """ - Base class for adapters. - - :param interfaces: number of interfaces supported by this adapter. - """ - - def __init__(self, interfaces=1): - - self._interfaces = interfaces - - self._ports = {} - for port_id in range(0, interfaces): - self._ports[port_id] = None - - def removable(self): - """ - Returns True if the adapter can be removed from a slot - and False if not. - - :returns: boolean - """ - - return True - - def port_exists(self, port_id): - """ - Checks if a port exists on this adapter. - - :returns: True is the port exists, - False otherwise. - """ - - if port_id in self._ports: - return True - return False - - def add_nio(self, port_id, nio): - """ - Adds a NIO to a port on this adapter. - - :param port_id: port ID (integer) - :param nio: NIO instance - """ - - self._ports[port_id] = nio - - def remove_nio(self, port_id): - """ - Removes a NIO from a port on this adapter. - - :param port_id: port ID (integer) - """ - - self._ports[port_id] = None - - def get_nio(self, port_id): - """ - Returns the NIO assigned to a port. - - :params port_id: port ID (integer) - - :returns: NIO instance - """ - - return self._ports[port_id] - - @property - def ports(self): - """ - Returns port to NIO mapping - - :returns: dictionary port -> NIO - """ - - return self._ports - - @property - def interfaces(self): - """ - Returns the number of interfaces supported by this adapter. - - :returns: number of interfaces - """ - - return self._interfaces diff --git a/gns3server/modules/vpcs/nios/__init__.py b/gns3server/modules/vpcs/nios/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py index 7d5ed339..3976d402 100644 --- a/gns3server/modules/vpcs/vpcs_device.py +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -24,7 +24,6 @@ import os import sys import subprocess import signal -import shutil import re import asyncio import socket @@ -32,9 +31,9 @@ import shutil from pkg_resources import parse_version from .vpcs_error import VPCSError -from .adapters.ethernet_adapter import EthernetAdapter -from .nios.nio_udp import NIO_UDP -from .nios.nio_tap import NIO_TAP +from ..adapters.ethernet_adapter import EthernetAdapter +from ..nios.nio_udp import NIO_UDP +from ..nios.nio_tap import NIO_TAP from ..attic import has_privileged_access from ..base_vm import BaseVM diff --git a/gns3server/modules/virtualbox/schemas.py b/gns3server/schemas/virtualbox.py similarity index 100% rename from gns3server/modules/virtualbox/schemas.py rename to gns3server/schemas/virtualbox.py From 73a481e510c7d0dfe834f5cd395e5843b99f1d4a Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 19 Jan 2015 11:22:24 +0100 Subject: [PATCH 033/485] Refactor port manager --- gns3server/modules/base_manager.py | 17 +++++++++- gns3server/modules/base_vm.py | 7 ++-- gns3server/modules/port_manager.py | 19 ++++++++++- gns3server/modules/vpcs/vpcs_device.py | 16 ++++----- tests/api/base.py | 12 +++---- tests/api/test_version.py | 2 +- tests/api/test_vpcs.py | 2 +- tests/modules/vpcs/test_vpcs_device.py | 46 +++++++++++++++----------- 8 files changed, 80 insertions(+), 41 deletions(-) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index c57d910e..263cd1c3 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -43,6 +43,21 @@ class BaseManager: cls._instance = cls() return cls._instance + @property + def port_manager(self): + """ + Returns the port_manager for this VMs + + :returns: Port manager + """ + + return self._port_manager + + @port_manager.setter + def port_manager(self, new_port_manager): + self._port_manager = new_port_manager + + @classmethod @asyncio.coroutine def destroy(cls): @@ -73,7 +88,7 @@ class BaseManager: else: if identifier in self._vms: raise VMError("VM identifier {} is already used by another VM instance".format(identifier)) - vm = self._VM_CLASS(vmname, identifier, self.port_manager) + vm = self._VM_CLASS(vmname, identifier, self) yield from vm.wait_for_creation() self._vms[vm.id] = vm return vm diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index 17ef0ff5..6f7dd0e9 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -26,20 +26,23 @@ log = logging.getLogger(__name__) class BaseVM: - def __init__(self, name, identifier, port_manager): + def __init__(self, name, identifier, manager): self._loop = asyncio.get_event_loop() self._queue = asyncio.Queue() self._name = name self._id = identifier self._created = asyncio.Future() - self._port_manager = port_manager + self._manager = manager self._config = Config.instance() self._worker = asyncio.async(self._run()) log.info("{type} device {name} [id={id}] has been created".format(type=self.__class__.__name__, name=self._name, id=self._id)) + #TODO: When delete release console ports + + @property def id(self): """ diff --git a/gns3server/modules/port_manager.py b/gns3server/modules/port_manager.py index 8093538e..6bbb01bc 100644 --- a/gns3server/modules/port_manager.py +++ b/gns3server/modules/port_manager.py @@ -17,7 +17,7 @@ import socket import ipaddress - +import asyncio class PortManager: """ @@ -42,6 +42,23 @@ class PortManager: else: self._console_host = host + @classmethod + def instance(cls): + """ + Singleton to return only one instance of BaseManager. + + :returns: instance of Manager + """ + + if not hasattr(cls, "_instance") or cls._instance is None: + cls._instance = cls() + return cls._instance + + @classmethod + @asyncio.coroutine + def destroy(cls): + cls._instance = None + @property def console_host(self): diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py index 3976d402..e93454ba 100644 --- a/gns3server/modules/vpcs/vpcs_device.py +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -48,20 +48,20 @@ class VPCSDevice(BaseVM): :param name: name of this VPCS device :param vpcs_id: VPCS instance ID - :param path: path to VPCS executable + :param manager: parent VM Manager :param working_dir: path to a working directory :param console: TCP console port """ - def __init__(self, name, vpcs_id, port_manager, working_dir=None, console=None): + def __init__(self, name, vpcs_id, manager, working_dir=None, console=None): - super().__init__(name, vpcs_id, port_manager) + super().__init__(name, vpcs_id, manager) - #self._path = path - #self._working_dir = working_dir # TODO: Hardcodded for testing + #self._working_dir = working_dir + self._working_dir = "/tmp" + self._path = self._config.get_section_config("VPCS").get("path", "vpcs") - self._working_dir = "/tmp" self._console = console self._command = [] @@ -83,9 +83,9 @@ class VPCSDevice(BaseVM): # try: if not self._console: - self._console = port_manager.get_free_port() + self._console = self._manager.port_manager.get_free_console_port() else: - self._console = port_manager.reserve_port(self._console) + self._console = self._manager.port_manager.reserve_console_port(self._console) except Exception as e: raise VPCSError(e) diff --git a/tests/api/base.py b/tests/api/base.py index b8f5f395..10a432a4 100644 --- a/tests/api/base.py +++ b/tests/api/base.py @@ -129,7 +129,7 @@ def _get_unused_port(): return port -@pytest.fixture(scope="module") +@pytest.fixture(scope="session") def loop(request): """Return an event loop and destroy it at the end of test""" loop = asyncio.new_event_loop() @@ -141,12 +141,8 @@ def loop(request): request.addfinalizer(tear_down) return loop -@pytest.fixture(scope="module") -def port_manager(): - return PortManager("127.0.0.1", False) - -@pytest.fixture(scope="module") -def server(request, loop, port_manager): +@pytest.fixture(scope="session") +def server(request, loop): port = _get_unused_port() host = "localhost" app = web.Application() @@ -154,7 +150,7 @@ def server(request, loop, port_manager): app.router.add_route(method, route, handler) for module in MODULES: instance = module.instance() - instance.port_manager = port_manager + instance.port_manager = PortManager("127.0.0.1", False) srv = loop.create_server(app.make_handler(), host, port) srv = loop.run_until_complete(srv) diff --git a/tests/api/test_version.py b/tests/api/test_version.py index 8fb46174..a052bb43 100644 --- a/tests/api/test_version.py +++ b/tests/api/test_version.py @@ -21,7 +21,7 @@ It's also used for unittest the HTTP implementation. """ from tests.utils import asyncio_patch -from tests.api.base import server, loop, port_manager +from tests.api.base import server, loop from gns3server.version import __version__ diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index 365f86a7..cc2f47f5 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -18,7 +18,7 @@ from tests.api.base import server, loop from tests.utils import asyncio_patch from gns3server import modules - +from unittest.mock import patch @asyncio_patch('gns3server.modules.VPCS.create_vm', return_value=84) def test_vpcs_create(server): diff --git a/tests/modules/vpcs/test_vpcs_device.py b/tests/modules/vpcs/test_vpcs_device.py index 4844e820..57eb374d 100644 --- a/tests/modules/vpcs/test_vpcs_device.py +++ b/tests/modules/vpcs/test_vpcs_device.py @@ -20,44 +20,52 @@ import asyncio from tests.utils import asyncio_patch #Move loop to util -from tests.api.base import loop, port_manager +from tests.api.base import loop from asyncio.subprocess import Process from unittest.mock import patch, MagicMock from gns3server.modules.vpcs.vpcs_device import VPCSDevice from gns3server.modules.vpcs.vpcs_error import VPCSError +from gns3server.modules.vpcs import VPCS +from gns3server.modules.port_manager import PortManager + +@pytest.fixture(scope="module") +def manager(): + m = VPCS.instance() + m.port_manager = PortManager("127.0.0.1", False) + return m @patch("subprocess.check_output", return_value="Welcome to Virtual PC Simulator, version 0.6".encode("utf-8")) -def test_vm(tmpdir, port_manager): - vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir)) +def test_vm(tmpdir, manager): + vm = VPCSDevice("test", 42, manager, working_dir=str(tmpdir)) assert vm.name == "test" assert vm.id == 42 @patch("subprocess.check_output", return_value="Welcome to Virtual PC Simulator, version 0.1".encode("utf-8")) -def test_vm_invalid_vpcs_version(tmpdir, port_manager): +def test_vm_invalid_vpcs_version(tmpdir, manager): with pytest.raises(VPCSError): - vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir)) + vm = VPCSDevice("test", 42, manager, working_dir=str(tmpdir)) assert vm.name == "test" assert vm.id == 42 @patch("gns3server.config.Config.get_section_config", return_value = {"path": "/bin/test_fake"}) -def test_vm_invalid_vpcs_path(tmpdir, port_manager): +def test_vm_invalid_vpcs_path(tmpdir, manager): with pytest.raises(VPCSError): - vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir)) + vm = VPCSDevice("test", 42, manager, working_dir=str(tmpdir)) assert vm.name == "test" assert vm.id == 42 -def test_start(tmpdir, loop, port_manager): +def test_start(tmpdir, loop, manager): with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): - vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir)) + vm = VPCSDevice("test", 42, manager, working_dir=str(tmpdir)) nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) loop.run_until_complete(asyncio.async(vm.start())) assert vm.is_running() == True -def test_stop(tmpdir, loop, port_manager): +def test_stop(tmpdir, loop, manager): process = MagicMock() with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): - vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir)) + vm = VPCSDevice("test", 42, manager, working_dir=str(tmpdir)) nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) loop.run_until_complete(asyncio.async(vm.start())) @@ -66,26 +74,26 @@ def test_stop(tmpdir, loop, port_manager): assert vm.is_running() == False process.terminate.assert_called_with() -def test_add_nio_binding_udp(port_manager, tmpdir): - vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir)) +def test_add_nio_binding_udp(tmpdir, manager): + vm = VPCSDevice("test", 42, manager, working_dir=str(tmpdir)) nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) assert nio.lport == 4242 -def test_add_nio_binding_tap(port_manager, tmpdir): - vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir)) +def test_add_nio_binding_tap(tmpdir, manager): + vm = VPCSDevice("test", 42, manager, working_dir=str(tmpdir)) with patch("gns3server.modules.vpcs.vpcs_device.has_privileged_access", return_value=True): nio = vm.port_add_nio_binding(0, {"type": "nio_tap", "tap_device": "test"}) assert nio.tap_device == "test" -def test_add_nio_binding_tap_no_privileged_access(port_manager, tmpdir): - vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir)) +def test_add_nio_binding_tap_no_privileged_access(tmpdir, manager): + vm = VPCSDevice("test", 42, manager, working_dir=str(tmpdir)) with patch("gns3server.modules.vpcs.vpcs_device.has_privileged_access", return_value=False): with pytest.raises(VPCSError): vm.port_add_nio_binding(0, {"type": "nio_tap", "tap_device": "test"}) assert vm._ethernet_adapter.ports[0] is not None -def test_port_remove_nio_binding(port_manager, tmpdir): - vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir)) +def test_port_remove_nio_binding(tmpdir, manager): + vm = VPCSDevice("test", 42, manager, working_dir=str(tmpdir)) nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) vm.port_remove_nio_binding(0) assert vm._ethernet_adapter.ports[0] == None From 7de95cd60a70ef4bd09f26fb63961cfa888e50e3 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 19 Jan 2015 11:28:51 +0100 Subject: [PATCH 034/485] Fix tests --- .../examples/delete_vpcsiddportsportidnio.txt | 15 +++++++++++ .../examples/post_vpcsiddportsportidnio.txt | 25 +++++++++++++++++++ tests/api/test_vpcs.py | 8 +++--- tests/modules/vpcs/test_vpcs_device.py | 4 +-- 4 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 docs/api/examples/delete_vpcsiddportsportidnio.txt create mode 100644 docs/api/examples/post_vpcsiddportsportidnio.txt diff --git a/docs/api/examples/delete_vpcsiddportsportidnio.txt b/docs/api/examples/delete_vpcsiddportsportidnio.txt new file mode 100644 index 00000000..6cb18d74 --- /dev/null +++ b/docs/api/examples/delete_vpcsiddportsportidnio.txt @@ -0,0 +1,15 @@ +curl -i -xDELETE 'http://localhost:8000/vpcs/{id:\d+}/ports/{port_id}/nio' + +DELETE /vpcs/{id:\d+}/ports/{port_id}/nio HTTP/1.1 + + + +HTTP/1.1 200 +CONNECTION: close +CONTENT-LENGTH: 2 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 aiohttp/0.13.1 +X-ROUTE: /vpcs/{id:\d+}/ports/{port_id}/nio + +{} diff --git a/docs/api/examples/post_vpcsiddportsportidnio.txt b/docs/api/examples/post_vpcsiddportsportidnio.txt new file mode 100644 index 00000000..771255e0 --- /dev/null +++ b/docs/api/examples/post_vpcsiddportsportidnio.txt @@ -0,0 +1,25 @@ +curl -i -xPOST 'http://localhost:8000/vpcs/{id:\d+}/ports/{port_id}/nio' -d '{"lport": 4242, "rhost": "127.0.0.1", "rport": 4343, "type": "nio_udp"}' + +POST /vpcs/{id:\d+}/ports/{port_id}/nio HTTP/1.1 +{ + "lport": 4242, + "rhost": "127.0.0.1", + "rport": 4343, + "type": "nio_udp" +} + + +HTTP/1.1 200 +CONNECTION: close +CONTENT-LENGTH: 89 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 aiohttp/0.13.1 +X-ROUTE: /vpcs/{id:\d+}/ports/{port_id}/nio + +{ + "lport": 4242, + "rhost": "127.0.0.1", + "rport": 4343, + "type": "nio_udp" +} diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index cc2f47f5..9dbbf94c 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -39,7 +39,7 @@ def test_vpcs_nio_create_udp(server): }, example=True) assert response.status == 200 - assert response.route == '/vpcs/{id}/ports/{port_id}/nio' + assert response.route == '/vpcs/{id:\d+}/ports/{port_id}/nio' assert response.json['type'] == 'nio_udp' @patch("gns3server.modules.vpcs.vpcs_device.has_privileged_access", return_value=True) @@ -50,7 +50,7 @@ def test_vpcs_nio_create_tap(mock, server): 'tap_device': 'test', }) assert response.status == 200 - assert response.route == '/vpcs/{vpcs_id}/ports/{port_id}/nio' + assert response.route == '/vpcs/{id:\d+}/ports/{port_id}/nio' assert response.json['type'] == 'nio_tap' def test_vpcs_delete_nio(server): @@ -62,8 +62,8 @@ def test_vpcs_delete_nio(server): 'rhost': '127.0.0.1' }, ) - response = server.delete('/vpcs/{}/ports/0/nio'.format(vm.json["vpcs_id"]), example=True) + response = server.delete('/vpcs/{}/ports/0/nio'.format(vm.json["id"]), example=True) assert response.status == 200 - assert response.route == '/vpcs/{id}/ports/{port_id}/nio' + assert response.route == '/vpcs/{id:\d+}/ports/{port_id}/nio' diff --git a/tests/modules/vpcs/test_vpcs_device.py b/tests/modules/vpcs/test_vpcs_device.py index 57eb374d..3d532f1a 100644 --- a/tests/modules/vpcs/test_vpcs_device.py +++ b/tests/modules/vpcs/test_vpcs_device.py @@ -90,10 +90,10 @@ def test_add_nio_binding_tap_no_privileged_access(tmpdir, manager): with patch("gns3server.modules.vpcs.vpcs_device.has_privileged_access", return_value=False): with pytest.raises(VPCSError): vm.port_add_nio_binding(0, {"type": "nio_tap", "tap_device": "test"}) - assert vm._ethernet_adapter.ports[0] is not None + assert vm._ethernet_adapter.ports[0] is None def test_port_remove_nio_binding(tmpdir, manager): vm = VPCSDevice("test", 42, manager, working_dir=str(tmpdir)) nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) vm.port_remove_nio_binding(0) - assert vm._ethernet_adapter.ports[0] == None + assert vm._ethernet_adapter.ports[0] is None From 9e5a2fcc42ad972abc8bd0080b41c8af4654eb1c Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 19 Jan 2015 11:37:24 +0100 Subject: [PATCH 035/485] Skip .tox directory during tests --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 50b5bb1b..f698cd6f 100644 --- a/tox.ini +++ b/tox.ini @@ -9,5 +9,5 @@ deps = -rdev-requirements.txt ignore = E501 [pytest] -norecursedirs = old_tests +norecursedirs = old_tests .tox From 2c3b0061a293ef73326a5acd47d56dae0b3ed089 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 19 Jan 2015 11:51:39 +0100 Subject: [PATCH 036/485] Cleanup travis build --- .travis.yml | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index d9bf305c..51c4f333 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,19 +1,14 @@ language: python - -env: - - TOX_ENV=py33 - - TOX_ENV=py34 - -before_install: - - sudo add-apt-repository ppa:gns3/ppa -y - - sudo apt-get update -q +python: + - "3.3" + - "3.4" install: - - pip install tox - - sudo apt-get install vpcs dynamips + - python setup.py install + - pip install -rdev-requirements.txt script: - - tox -e $TOX_ENV + - py.test -v -s tests #branches: # only: From f0880c4a37f09323067d8ed37977152a98e09ad7 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 19 Jan 2015 13:47:20 +0100 Subject: [PATCH 037/485] Drop queue codes because it's too specific --- gns3server/modules/base_vm.py | 51 +++++++------------------- gns3server/modules/vpcs/vpcs_device.py | 4 -- 2 files changed, 13 insertions(+), 42 deletions(-) diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index 6f7dd0e9..17aa103b 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -28,14 +28,12 @@ class BaseVM: def __init__(self, name, identifier, manager): - self._loop = asyncio.get_event_loop() - self._queue = asyncio.Queue() self._name = name self._id = identifier self._created = asyncio.Future() self._manager = manager self._config = Config.instance() - self._worker = asyncio.async(self._run()) + asyncio.async(self._create()) log.info("{type} device {name} [id={id}] has been created".format(type=self.__class__.__name__, name=self._name, id=self._id)) @@ -74,31 +72,13 @@ class BaseVM: @asyncio.coroutine def _create(self): """ - Called when the run loop start + Called when the run module is created and ready to receive + commands. It's asynchronous. """ - - raise NotImplementedError - - @asyncio.coroutine - def _run(self, timeout=60): - - try: - yield from self._create() - self._created.set_result(True) - except VMError as e: - self._created.set_exception(e) - return - - while True: - future, command = yield from self._queue.get() - try: - try: - yield from asyncio.wait_for(self._execute(command), timeout=timeout) - except asyncio.TimeoutError: - raise VMError("{} has timed out after {} seconds!".format(command, timeout)) - future.set_result(True) - except Exception as e: - future.set_exception(e) + self._created.set_result(True) + log.info("{type} device {name} [id={id}] has been created".format(type=self.__class__.__name__, + name=self._name, + id=self._id)) def wait_for_creation(self): return self._created @@ -111,17 +91,12 @@ class BaseVM: raise NotImplementedError - def put(self, *args): - """ - Add to the processing queue of the VM - :returns: future + @asyncio.coroutine + def stop(self): + """ + Starts the VM process. """ - future = asyncio.Future() - try: - args.insert(0, future) - self._queue.put_nowait(args) - except asyncio.qeues.QueueFull: - raise VMError("Queue is full") - return future + raise NotImplementedError + diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py index e93454ba..06ca28de 100644 --- a/gns3server/modules/vpcs/vpcs_device.py +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -173,10 +173,6 @@ class VPCSDevice(BaseVM): except (OSError, subprocess.SubprocessError) as e: raise VPCSError("Error while looking for the VPCS version: {}".format(e)) - @asyncio.coroutine - def _create(self): - pass - @asyncio.coroutine def start(self): """ From a06d935ef4513cbd873131b1ef7b10e0f2e59bbb Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 19 Jan 2015 14:21:08 +0100 Subject: [PATCH 038/485] Drop tornado --- gns3server/main.py | 24 ------------------- old_tests/test_version_handler.py | 40 ------------------------------- 2 files changed, 64 deletions(-) delete mode 100644 old_tests/test_version_handler.py diff --git a/gns3server/main.py b/gns3server/main.py index 71aaae90..0f669c58 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -21,26 +21,12 @@ import datetime import sys 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("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) - - def locale_check(): """ Checks if this application runs with a correct locale (i.e. supports UTF-8 encoding) and attempt to fix @@ -86,16 +72,6 @@ def main(): """ #TODO: migrate command line options to argparse (don't forget the quiet mode). - try: - tornado.options.parse_command_line() - except (tornado.options.Error, ValueError): - tornado.options.print_help() - raise SystemExit - - from tornado.options import options - if options.version: - print(__version__) - raise SystemExit current_year = datetime.date.today().year diff --git a/old_tests/test_version_handler.py b/old_tests/test_version_handler.py deleted file mode 100644 index 0c8c75d9..00000000 --- a/old_tests/test_version_handler.py +++ /dev/null @@ -1,40 +0,0 @@ -from tornado.testing import AsyncHTTPTestCase -from tornado.escape import json_decode -from gns3server.server import VersionHandler -from gns3server.version import __version__ -import tornado.web - -""" -Tests for the web server version handler -""" - - -class TestVersionHandler(AsyncHTTPTestCase): - - URL = "/version" - - def get_app(self): - - return tornado.web.Application([(self.URL, VersionHandler)]) - - def test_endpoint(self): - """ - Tests if the response HTTP code is 200 (success) - """ - - self.http_client.fetch(self.get_url(self.URL), self.stop) - response = self.wait() - assert response.code == 200 - - def test_received_version(self): - """ - Tests if the returned content type is JSON and - if the received version is the same as the server - """ - - self.http_client.fetch(self.get_url(self.URL), self.stop) - response = self.wait() - assert response.headers['Content-Type'].startswith('application/json') - assert response.body - body = json_decode(response.body) - assert body['version'] == __version__ From a9a09cc0bc33b60f22344f470e280fc65792a26a Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 19 Jan 2015 15:05:44 +0100 Subject: [PATCH 039/485] Temporaru drop old tornado logging --- gns3server/main.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/gns3server/main.py b/gns3server/main.py index 0f669c58..88411826 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -75,13 +75,23 @@ def main(): current_year = datetime.date.today().year - user_log = logging.getLogger('user_facing') - if not options.quiet: - # Send user facing messages to stdout. - stream_handler = logging.StreamHandler(sys.stdout) - stream_handler.addFilter(logging.Filter(name='user_facing')) - user_log.addHandler(stream_handler) - user_log.propagate = False + + # TODO: Renable the test when we will have command line + # user_log = logging.getLogger('user_facing') + # if not options.quiet: + # # Send user facing messages to stdout. + # stream_handler = logging.StreamHandler(sys.stdout) + # stream_handler.addFilter(logging.Filter(name='user_facing')) + # user_log.addHandler(stream_handler) + # user_log.propagate = False + # END OLD LOG CODE + root_log = logging.getLogger() + root_log.setLevel(logging.DEBUG) + console_log = logging.StreamHandler(sys.stdout) + console_log.setLevel(logging.DEBUG) + root_log.addHandler(console_log) + user_log = root_log + # FIXME END Temporary user_log.info("GNS3 server version {}".format(__version__)) user_log.info("Copyright (c) 2007-{} GNS3 Technologies Inc.".format(current_year)) @@ -104,7 +114,9 @@ def main(): log.critical("the current working directory doesn't exist") return - server = Server(options.host, options.port, options.console_bind_to_any) + # TODO: Renable console_bind_to_any when we will have command line parsing + #server = Server(options.host, options.port, options.console_bind_to_any) + server = Server("127.0.0.1", 8000, False) server.run() if __name__ == '__main__': From 240d83411c37432464bd30cf16459d0b8b31b63b Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 19 Jan 2015 16:23:41 +0100 Subject: [PATCH 040/485] Create a project entity --- docs/api/examples/post_project.txt | 20 ++++++++++ gns3server/handlers/__init__.py | 2 +- gns3server/handlers/project_handler.py | 34 +++++++++++++++++ gns3server/modules/project.py | 53 ++++++++++++++++++++++++++ gns3server/schemas/project.py | 39 +++++++++++++++++++ tests/api/test_project.py | 43 +++++++++++++++++++++ tests/modules/test_project.py | 41 ++++++++++++++++++++ 7 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 docs/api/examples/post_project.txt create mode 100644 gns3server/handlers/project_handler.py create mode 100644 gns3server/modules/project.py create mode 100644 gns3server/schemas/project.py create mode 100644 tests/api/test_project.py create mode 100644 tests/modules/test_project.py diff --git a/docs/api/examples/post_project.txt b/docs/api/examples/post_project.txt new file mode 100644 index 00000000..6312959f --- /dev/null +++ b/docs/api/examples/post_project.txt @@ -0,0 +1,20 @@ +curl -i -xPOST 'http://localhost:8000/project' -d '{"location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-240/test_create_project_with_dir0"}' + +POST /project HTTP/1.1 +{ + "location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-240/test_create_project_with_dir0" +} + + +HTTP/1.1 200 +CONNECTION: close +CONTENT-LENGTH: 171 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 aiohttp/0.13.1 +X-ROUTE: /project + +{ + "location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-240/test_create_project_with_dir0", + "uuid": "16c371ee-728c-4bb3-8062-26b9313bd67d" +} diff --git a/gns3server/handlers/__init__.py b/gns3server/handlers/__init__.py index 60c6268d..965f59e6 100644 --- a/gns3server/handlers/__init__.py +++ b/gns3server/handlers/__init__.py @@ -1 +1 @@ -__all__ = ['version_handler', 'vpcs_handler'] +__all__ = ['version_handler', 'vpcs_handler', 'project_handler'] diff --git a/gns3server/handlers/project_handler.py b/gns3server/handlers/project_handler.py new file mode 100644 index 00000000..6fbaa4b0 --- /dev/null +++ b/gns3server/handlers/project_handler.py @@ -0,0 +1,34 @@ +# -*- 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 ..schemas.project import PROJECT_OBJECT_SCHEMA +from ..modules.project import Project +from aiohttp.web import HTTPConflict + + +class ProjectHandler: + @classmethod + @Route.post( + r"/project", + description="Create a project on the server", + output=PROJECT_OBJECT_SCHEMA, + input=PROJECT_OBJECT_SCHEMA) + def create_project(request, response): + p = Project(location = request.json.get("location"), + uuid = request.json.get("uuid")) + response.json(p) diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py new file mode 100644 index 00000000..9d74d480 --- /dev/null +++ b/gns3server/modules/project.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python +# -*- 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 os +import tempfile +from uuid import uuid4 + + +class Project: + """ + A project contains a list of VM. + In theory VM are isolated project/project. + """ + + """ + :param uuid: Force project uuid (None by default auto generate an UUID) + :param location: Parent path of the project. (None should create a tmp directory) + """ + def __init__(self, uuid = None, location = None): + if uuid is None: + self.uuid = str(uuid4()) + else: + self.uuid = uuid + + self.location = location + if location is None: + self.location = tempfile.mkdtemp() + + self.path = os.path.join(self.location, self.uuid) + if os.path.exists(self.path) is False: + os.mkdir(self.path) + os.mkdir(os.path.join(self.path, 'files')) + + def __json__(self): + return { + "uuid": self.uuid, + "location": self.location + } diff --git a/gns3server/schemas/project.py b/gns3server/schemas/project.py new file mode 100644 index 00000000..bf1e07b2 --- /dev/null +++ b/gns3server/schemas/project.py @@ -0,0 +1,39 @@ +# -*- 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 . + + +PROJECT_OBJECT_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to create a new Project instance", + "type": "object", + "properties": { + "location": { + "description": "Base directory where the project should be created on remote server", + "type": "string", + "minLength": 1 + }, + "uuid": { + "description": "Project UUID", + "type": "string", + "minLength": 36, + "maxLength": 36, + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + }, + }, + "additionalProperties": False +} + diff --git a/tests/api/test_project.py b/tests/api/test_project.py new file mode 100644 index 00000000..69067f29 --- /dev/null +++ b/tests/api/test_project.py @@ -0,0 +1,43 @@ +# -*- 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 . + +""" +This test suite check /project endpoint +""" + + +from tests.utils import asyncio_patch +from tests.api.base import server, loop +from gns3server.version import __version__ + + +def test_create_project_with_dir(server, tmpdir): + response = server.post('/project', {"location": str(tmpdir)}, example=True) + assert response.status == 200 + assert response.json['location'] == str(tmpdir) + +def test_create_project_without_dir(server): + query = {} + response = server.post('/project', query) + assert response.status == 200 + assert response.json['uuid'] is not None + +def test_create_project_with_uuid(server): + query = {'uuid': '00010203-0405-0607-0809-0a0b0c0d0e0f'} + response = server.post('/project', query) + assert response.status == 200 + assert response.json['uuid'] is not None diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py new file mode 100644 index 00000000..07245afd --- /dev/null +++ b/tests/modules/test_project.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# -*- 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 gns3server.modules.project import Project +import os + +def test_affect_uuid(): + p = Project() + assert len(p.uuid) == 36 + + p = Project(uuid = '00010203-0405-0607-0809-0a0b0c0d0e0f') + assert p.uuid == '00010203-0405-0607-0809-0a0b0c0d0e0f' + +def test_path(tmpdir): + p = Project(location = str(tmpdir)) + assert p.path == os.path.join(str(tmpdir), p.uuid) + assert os.path.exists(os.path.join(str(tmpdir), p.uuid)) + assert os.path.exists(os.path.join(str(tmpdir), p.uuid, 'files')) + +def test_temporary_path(): + p = Project() + assert os.path.exists(p.path) + +def test_json(tmpdir): + p = Project() + assert p.__json__() == {"location": p.location, "uuid": p.uuid} From f0094cc0d00eb9d332d96292cbb20a5520d39434 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 19 Jan 2015 17:07:32 +0100 Subject: [PATCH 041/485] Project Manager --- gns3server/modules/project_manager.py | 63 +++++++++++++++++++++++++++ tests/modules/test_project.py | 2 +- tests/modules/test_project_manager.py | 32 ++++++++++++++ 3 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 gns3server/modules/project_manager.py create mode 100644 tests/modules/test_project_manager.py diff --git a/gns3server/modules/project_manager.py b/gns3server/modules/project_manager.py new file mode 100644 index 00000000..0ff089bc --- /dev/null +++ b/gns3server/modules/project_manager.py @@ -0,0 +1,63 @@ +# -*- 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 aiohttp +from .project import Project + + +class ProjectManager: + """ + This singleton, keep track of available projects. + """ + + def __init__(self): + self._projects = {} + + @classmethod + def instance(cls): + """ + Singleton to return only one instance of BaseManager. + + :returns: instance of Manager + """ + + if not hasattr(cls, "_instance"): + cls._instance = cls() + return cls._instance + + def get_project(self, project_id): + """ + Returns a Project instance. + + :param project_id: Project identifier + + :returns: Project instance + """ + + if project_id not in self._projects: + raise aiohttp.web.HTTPNotFound(text="Project UUID {} doesn't exist".format(project_id)) + return self._projects[project_id] + + def create_project(self, **kwargs): + """ + Create a project and keep a references to it in project manager. + + See documentation of Project for arguments + """ + project = Project(**kwargs) + self._projects[project.uuid] = project + return project diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py index 07245afd..2d6ea0b3 100644 --- a/tests/modules/test_project.py +++ b/tests/modules/test_project.py @@ -23,7 +23,7 @@ def test_affect_uuid(): p = Project() assert len(p.uuid) == 36 - p = Project(uuid = '00010203-0405-0607-0809-0a0b0c0d0e0f') + p = Project(uuid='00010203-0405-0607-0809-0a0b0c0d0e0f') assert p.uuid == '00010203-0405-0607-0809-0a0b0c0d0e0f' def test_path(tmpdir): diff --git a/tests/modules/test_project_manager.py b/tests/modules/test_project_manager.py new file mode 100644 index 00000000..64009928 --- /dev/null +++ b/tests/modules/test_project_manager.py @@ -0,0 +1,32 @@ +# -*- 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 aiohttp +import pytest +from gns3server.modules.project_manager import ProjectManager + + +def test_create_project(): + pm = ProjectManager.instance() + project = pm.create_project(uuid='00010203-0405-0607-0809-0a0b0c0d0e0f') + assert project == pm.get_project('00010203-0405-0607-0809-0a0b0c0d0e0f') + +def test_project_not_found(): + pm = ProjectManager.instance() + with pytest.raises(aiohttp.web.HTTPNotFound): + pm.get_project('00010203-0405-0607-0809-000000000000') + From ed973dbcf2c3a94274e94eeebeb591c4d48f62a0 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 19 Jan 2015 17:12:36 +0100 Subject: [PATCH 042/485] Project handler use ProjectManager --- docs/api/examples/post_project.txt | 8 ++++---- gns3server/handlers/project_handler.py | 9 ++++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/api/examples/post_project.txt b/docs/api/examples/post_project.txt index 6312959f..ab6d97fc 100644 --- a/docs/api/examples/post_project.txt +++ b/docs/api/examples/post_project.txt @@ -1,8 +1,8 @@ -curl -i -xPOST 'http://localhost:8000/project' -d '{"location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-240/test_create_project_with_dir0"}' +curl -i -xPOST 'http://localhost:8000/project' -d '{"location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-253/test_create_project_with_dir0"}' POST /project HTTP/1.1 { - "location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-240/test_create_project_with_dir0" + "location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-253/test_create_project_with_dir0" } @@ -15,6 +15,6 @@ SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /project { - "location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-240/test_create_project_with_dir0", - "uuid": "16c371ee-728c-4bb3-8062-26b9313bd67d" + "location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-253/test_create_project_with_dir0", + "uuid": "a8984692-a820-45de-8d4d-fc006b29072a" } diff --git a/gns3server/handlers/project_handler.py b/gns3server/handlers/project_handler.py index 6fbaa4b0..77a4b835 100644 --- a/gns3server/handlers/project_handler.py +++ b/gns3server/handlers/project_handler.py @@ -17,7 +17,7 @@ from ..web.route import Route from ..schemas.project import PROJECT_OBJECT_SCHEMA -from ..modules.project import Project +from ..modules.project_manager import ProjectManager from aiohttp.web import HTTPConflict @@ -29,6 +29,9 @@ class ProjectHandler: output=PROJECT_OBJECT_SCHEMA, input=PROJECT_OBJECT_SCHEMA) def create_project(request, response): - p = Project(location = request.json.get("location"), - uuid = request.json.get("uuid")) + pm = ProjectManager.instance() + p = pm.create_project( + location = request.json.get("location"), + uuid = request.json.get("uuid") + ) response.json(p) From 78015b800e89bc564dc8adf63a4f1737e46a6372 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 19 Jan 2015 17:50:09 +0100 Subject: [PATCH 043/485] Install vpcs on travis --- .travis.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.travis.yml b/.travis.yml index 51c4f333..59cbebed 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,11 @@ python: - "3.3" - "3.4" +before_install: + - sudo add-apt-repository ppa:gns3/ppa -y + - sudo apt-get update -q + - sudo apt-get install vpcs dynamips + install: - python setup.py install - pip install -rdev-requirements.txt From 345b471c4732868aa4a4c0f0077b015d0dfb3f6a Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 19 Jan 2015 17:58:01 +0100 Subject: [PATCH 044/485] Drop unused code --- gns3server/modules/deadman/__init__.py | 164 ------------------------- 1 file changed, 164 deletions(-) delete mode 100644 gns3server/modules/deadman/__init__.py diff --git a/gns3server/modules/deadman/__init__.py b/gns3server/modules/deadman/__init__.py deleted file mode 100644 index 3ea22783..00000000 --- a/gns3server/modules/deadman/__init__.py +++ /dev/null @@ -1,164 +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 . - -""" -DeadMan server module. -""" - -import os -import time -import subprocess - -from gns3server.modules import IModule -from gns3server.config import Config - - -import logging -log = logging.getLogger(__name__) - -class DeadMan(IModule): - """ - DeadMan module. - - :param name: module name - :param args: arguments for the module - :param kwargs: named arguments for the module - """ - - def __init__(self, name, *args, **kwargs): - config = Config.instance() - - # a new process start when calling IModule - IModule.__init__(self, name, *args, **kwargs) - self._host = kwargs["host"] - self._projects_dir = kwargs["projects_dir"] - self._tempdir = kwargs["temp_dir"] - self._working_dir = self._projects_dir - self._heartbeat_file = "%s/heartbeat_file_for_gnsdms" % ( - self._tempdir) - - if 'heartbeat_file' in kwargs: - self._heartbeat_file = kwargs['heartbeat_file'] - - self._is_enabled = False - try: - cloud_config = Config.instance().get_section_config("CLOUD_SERVER") - instance_id = cloud_config["instance_id"] - cloud_user_name = cloud_config["cloud_user_name"] - cloud_api_key = cloud_config["cloud_api_key"] - self._is_enabled = True - except KeyError: - log.critical("Missing cloud.conf - disabling Deadman Switch") - - self._deadman_process = None - self.heartbeat() - self.start() - - def _start_deadman_process(self): - """ - Start a subprocess and return the object - """ - - #gnsserver gets configuration options from cloud.conf. This is where - #the client adds specific cloud information. - #gns3dms also reads in cloud.conf. That is why we don't need to specific - #all the command line arguments here. - - cmd = [] - cmd.append("gns3dms") - cmd.append("--file") - cmd.append("%s" % (self._heartbeat_file)) - cmd.append("--background") - cmd.append("--debug") - log.info("Deadman: Running command: %s"%(cmd)) - - process = subprocess.Popen(cmd, stderr=subprocess.STDOUT, shell=False) - return process - - def _stop_deadman_process(self): - """ - Start a subprocess and return the object - """ - - cmd = [] - cmd.append("gns3dms") - cmd.append("-k") - log.info("Deadman: Running command: %s"%(cmd)) - - process = subprocess.Popen(cmd, shell=False) - return process - - - def stop(self, signum=None): - """ - Properly stops the module. - - :param signum: signal number (if called by the signal handler) - """ - - if self._deadman_process == None: - log.info("Deadman: Can't stop, is not currently running") - - log.debug("Deadman: Stopping process") - - self._deadman_process = self._stop_deadman_process() - self._deadman_process = None - #Jerry or Jeremy why do we do this? Won't this stop the I/O loop for - #for everyone? - IModule.stop(self, signum) # this will stop the I/O loop - - def start(self, request=None): - """ - Start the deadman process on the server - """ - - if self._is_enabled: - self._deadman_process = self._start_deadman_process() - log.debug("Deadman: Process is starting") - - @IModule.route("deadman.reset") - def reset(self, request=None): - """ - Resets the module (JSON-RPC notification). - - :param request: JSON request (not used) - """ - - self.stop() - self.start() - - log.info("Deadman: Module has been reset") - - - @IModule.route("deadman.heartbeat") - def heartbeat(self, request=None): - """ - Update a file on the server that the deadman switch will monitor - """ - - now = time.time() - - with open(self._heartbeat_file, 'w') as heartbeat_file: - heartbeat_file.write(str(now)) - heartbeat_file.close() - - log.debug("Deadman: heartbeat_file updated: %s %s" % ( - self._heartbeat_file, - now, - )) - - self.start() \ No newline at end of file From fe22576ae20e2dd70efb6ddf518a368fa61adab2 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 19 Jan 2015 14:43:35 -0700 Subject: [PATCH 045/485] Some quick cleaning. --- gns3server/modules/attic.py | 1 - gns3server/modules/base_manager.py | 8 ++++--- gns3server/modules/port_manager.py | 4 +++- gns3server/modules/project.py | 33 +++++++++++++++----------- gns3server/modules/project_manager.py | 7 +++--- gns3server/modules/vpcs/vpcs_device.py | 8 +++---- 6 files changed, 35 insertions(+), 26 deletions(-) diff --git a/gns3server/modules/attic.py b/gns3server/modules/attic.py index b059b532..be09b57a 100644 --- a/gns3server/modules/attic.py +++ b/gns3server/modules/attic.py @@ -24,7 +24,6 @@ import os import struct import socket import stat -import errno import time import logging diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 263cd1c3..42fb1576 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -29,14 +29,16 @@ class BaseManager: """ def __init__(self): + self._vms = {} + self._port_manager = None @classmethod def instance(cls): """ Singleton to return only one instance of BaseManager. - :returns: instance of Manager + :returns: instance of BaseManager """ if not hasattr(cls, "_instance") or cls._instance is None: @@ -55,11 +57,11 @@ class BaseManager: @port_manager.setter def port_manager(self, new_port_manager): - self._port_manager = new_port_manager + self._port_manager = new_port_manager @classmethod - @asyncio.coroutine + @asyncio.coroutine # FIXME: why coroutine? def destroy(cls): cls._instance = None diff --git a/gns3server/modules/port_manager.py b/gns3server/modules/port_manager.py index 6bbb01bc..b0188802 100644 --- a/gns3server/modules/port_manager.py +++ b/gns3server/modules/port_manager.py @@ -19,6 +19,7 @@ import socket import ipaddress import asyncio + class PortManager: """ :param host: IP address to bind for console connections @@ -55,8 +56,9 @@ class PortManager: return cls._instance @classmethod - @asyncio.coroutine + @asyncio.coroutine # FIXME: why coroutine? def destroy(cls): + cls._instance = None @property diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index 9d74d480..289b18a2 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright (C) 2015 GNS3 Technologies Inc. @@ -25,29 +24,35 @@ class Project: """ A project contains a list of VM. In theory VM are isolated project/project. - """ - """ :param uuid: Force project uuid (None by default auto generate an UUID) :param location: Parent path of the project. (None should create a tmp directory) """ - def __init__(self, uuid = None, location = None): + + def __init__(self, uuid=None, location=None): + if uuid is None: - self.uuid = str(uuid4()) + self._uuid = str(uuid4()) else: - self.uuid = uuid + self._uuid = uuid - self.location = location + self._location = location if location is None: - self.location = tempfile.mkdtemp() + self._location = tempfile.mkdtemp() - self.path = os.path.join(self.location, self.uuid) - if os.path.exists(self.path) is False: - os.mkdir(self.path) - os.mkdir(os.path.join(self.path, 'files')) + self._path = os.path.join(self._location, self._uuid) + if os.path.exists(self._path) is False: + os.mkdir(self._path) + os.mkdir(os.path.join(self._path, 'files')) + + @property + def uuid(self): + + return self._uuid def __json__(self): + return { - "uuid": self.uuid, - "location": self.location + "uuid": self._uuid, + "location": self._location } diff --git a/gns3server/modules/project_manager.py b/gns3server/modules/project_manager.py index 0ff089bc..98dab785 100644 --- a/gns3server/modules/project_manager.py +++ b/gns3server/modules/project_manager.py @@ -21,7 +21,7 @@ from .project import Project class ProjectManager: """ - This singleton, keep track of available projects. + This singleton keeps track of available projects. """ def __init__(self): @@ -30,9 +30,9 @@ class ProjectManager: @classmethod def instance(cls): """ - Singleton to return only one instance of BaseManager. + Singleton to return only one instance of ProjectManager. - :returns: instance of Manager + :returns: instance of ProjectManager """ if not hasattr(cls, "_instance"): @@ -58,6 +58,7 @@ class ProjectManager: See documentation of Project for arguments """ + project = Project(**kwargs) self._projects[project.uuid] = project return project diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py index 06ca28de..9d36c305 100644 --- a/gns3server/modules/vpcs/vpcs_device.py +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -193,10 +193,10 @@ class VPCSDevice(BaseVM): flags = subprocess.CREATE_NEW_PROCESS_GROUP with open(self._vpcs_stdout_file, "w") as fd: self._process = yield from asyncio.create_subprocess_exec(*self._command, - stdout=fd, - stderr=subprocess.STDOUT, - cwd=self._working_dir, - creationflags=flags) + stdout=fd, + stderr=subprocess.STDOUT, + cwd=self._working_dir, + creationflags=flags) log.info("VPCS instance {} started PID={}".format(self._id, self._process.pid)) self._started = True except (OSError, subprocess.SubprocessError) as e: From 7fff25a9a92403f333711520c258cd29fb184094 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 19 Jan 2015 18:30:57 -0700 Subject: [PATCH 046/485] UUID support for VMs. Basic VirtualBox support (create, start and stop). Some refactoring for BaseVM class. Updated CURL command in tests. --- gns3server/handlers/virtualbox_handler.py | 80 ++++ gns3server/handlers/vpcs_handler.py | 37 +- gns3server/modules/__init__.py | 3 +- gns3server/modules/base_manager.py | 56 +-- gns3server/modules/base_vm.py | 65 ++- gns3server/modules/project.py | 12 +- .../modules/virtualbox/virtualbox_vm.py | 184 +++++---- gns3server/modules/vpcs/vpcs_device.py | 56 ++- gns3server/schemas/virtualbox.py | 381 +----------------- gns3server/schemas/vpcs.py | 15 +- gns3server/server.py | 1 + tests/api/base.py | 5 +- tests/api/test_virtualbox.py | 41 ++ tests/api/test_vpcs.py | 66 ++- tests/conftest.py | 2 +- 15 files changed, 396 insertions(+), 608 deletions(-) create mode 100644 gns3server/handlers/virtualbox_handler.py create mode 100644 tests/api/test_virtualbox.py diff --git a/gns3server/handlers/virtualbox_handler.py b/gns3server/handlers/virtualbox_handler.py new file mode 100644 index 00000000..2b8714dd --- /dev/null +++ b/gns3server/handlers/virtualbox_handler.py @@ -0,0 +1,80 @@ +# -*- 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 ..schemas.virtualbox import VBOX_CREATE_SCHEMA +from ..schemas.virtualbox import VBOX_OBJECT_SCHEMA +from ..modules.virtualbox import VirtualBox + + +class VirtualBoxHandler: + """ + API entry points for VirtualBox. + """ + + @classmethod + @Route.post( + r"/virtualbox", + status_codes={ + 201: "VirtualBox VM instance created", + 409: "Conflict" + }, + description="Create a new VirtualBox VM instance", + input=VBOX_CREATE_SCHEMA, + output=VBOX_OBJECT_SCHEMA) + def create(request, response): + + vbox_manager = VirtualBox.instance() + vm = yield from vbox_manager.create_vm(request.json["name"], request.json.get("uuid")) + response.json({"name": vm.name, + "uuid": vm.uuid}) + + @classmethod + @Route.post( + r"/virtualbox/{uuid}/start", + parameters={ + "uuid": "VirtualBox VM instance UUID" + }, + status_codes={ + 204: "VirtualBox VM instance started", + 400: "Invalid VirtualBox VM instance UUID", + 404: "VirtualBox VM instance doesn't exist" + }, + description="Start a VirtualBox VM instance") + def create(request, response): + + vbox_manager = VirtualBox.instance() + yield from vbox_manager.start_vm(request.match_info["uuid"]) + response.json({}) + + @classmethod + @Route.post( + r"/virtualbox/{uuid}/stop", + parameters={ + "uuid": "VirtualBox VM instance UUID" + }, + status_codes={ + 204: "VirtualBox VM instance stopped", + 400: "Invalid VirtualBox VM instance UUID", + 404: "VirtualBox VM instance doesn't exist" + }, + description="Stop a VirtualBox VM instance") + def create(request, response): + + vbox_manager = VirtualBox.instance() + yield from vbox_manager.stop_vm(request.match_info["uuid"]) + response.json({}) diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index 4719f0b0..28e72331 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -22,7 +22,7 @@ from ..schemas.vpcs import VPCS_NIO_SCHEMA from ..modules.vpcs import VPCS -class VPCSHandler(object): +class VPCSHandler: """ API entry points for VPCS. """ @@ -40,53 +40,56 @@ class VPCSHandler(object): def create(request, response): vpcs = VPCS.instance() - vm = yield from vpcs.create_vm(request.json["name"]) + vm = yield from vpcs.create_vm(request.json["name"], request.json.get("uuid")) response.json({"name": vm.name, - "id": vm.id, + "uuid": vm.uuid, "console": vm.console}) @classmethod @Route.post( - r"/vpcs/{id:\d+}/start", + r"/vpcs/{uuid}/start", parameters={ - "id": "VPCS instance ID" + "uuid": "VPCS instance UUID" }, status_codes={ 204: "VPCS instance started", + 400: "Invalid VPCS instance UUID", 404: "VPCS instance doesn't exist" }, description="Start a VPCS instance") def create(request, response): vpcs_manager = VPCS.instance() - yield from vpcs_manager.start_vm(int(request.match_info["id"])) + yield from vpcs_manager.start_vm(request.match_info["uuid"]) response.json({}) @classmethod @Route.post( - r"/vpcs/{id:\d+}/stop", + r"/vpcs/{uuid}/stop", parameters={ - "id": "VPCS instance ID" + "uuid": "VPCS instance UUID" }, status_codes={ 204: "VPCS instance stopped", + 400: "Invalid VPCS instance UUID", 404: "VPCS instance doesn't exist" }, description="Stop a VPCS instance") def create(request, response): vpcs_manager = VPCS.instance() - yield from vpcs_manager.stop_vm(int(request.match_info["id"])) + yield from vpcs_manager.stop_vm(request.match_info["uuid"]) response.json({}) @Route.post( - r"/vpcs/{id:\d+}/ports/{port_id}/nio", + r"/vpcs/{uuid}/ports/{port_id}/nio", parameters={ - "id": "VPCS instance ID", + "uuid": "VPCS instance UUID", "port_id": "Id of the port where the nio should be add" }, status_codes={ 201: "NIO created", + 400: "Invalid VPCS instance UUID", 404: "VPCS instance doesn't exist" }, description="Add a NIO to a VPCS", @@ -95,26 +98,26 @@ class VPCSHandler(object): def create_nio(request, response): vpcs_manager = VPCS.instance() - vm = vpcs_manager.get_vm(int(request.match_info["id"])) + vm = vpcs_manager.get_vm(request.match_info["uuid"]) nio = vm.port_add_nio_binding(int(request.match_info["port_id"]), request.json) - response.json(nio) @classmethod @Route.delete( - r"/vpcs/{id:\d+}/ports/{port_id}/nio", + r"/vpcs/{uuid}/ports/{port_id}/nio", parameters={ - "id": "VPCS instance ID", - "port_id": "Id of the port where the nio should be remove" + "uuid": "VPCS instance UUID", + "port_id": "ID of the port where the nio should be removed" }, status_codes={ 200: "NIO deleted", + 400: "Invalid VPCS instance UUID", 404: "VPCS instance doesn't exist" }, description="Remove a NIO from a VPCS") def delete_nio(request, response): vpcs_manager = VPCS.instance() - vm = vpcs_manager.get_vm(int(request.match_info["id"])) + vm = vpcs_manager.get_vm(request.match_info["uuid"]) nio = vm.port_remove_nio_binding(int(request.match_info["port_id"])) response.json({}) diff --git a/gns3server/modules/__init__.py b/gns3server/modules/__init__.py index 5fc0b897..43b95792 100644 --- a/gns3server/modules/__init__.py +++ b/gns3server/modules/__init__.py @@ -17,8 +17,9 @@ import sys from .vpcs import VPCS +from .virtualbox import VirtualBox -MODULES = [VPCS] +MODULES = [VPCS, VirtualBox] #if sys.platform.startswith("linux"): # # IOU runs only on Linux diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 42fb1576..71f78d2b 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -19,7 +19,7 @@ import asyncio import aiohttp -from .vm_error import VMError +from uuid import UUID, uuid4 class BaseManager: @@ -63,44 +63,52 @@ class BaseManager: @classmethod @asyncio.coroutine # FIXME: why coroutine? def destroy(cls): + cls._instance = None - def get_vm(self, vm_id): + def get_vm(self, uuid): """ Returns a VM instance. - :param vm_id: VM identifier + :param uuid: VM UUID :returns: VM instance """ - if vm_id not in self._vms: - raise aiohttp.web.HTTPNotFound(text="ID {} doesn't exist".format(vm_id)) - return self._vms[vm_id] + try: + UUID(uuid, version=4) + except ValueError: + raise aiohttp.web.HTTPBadRequest(text="{} is not a valid UUID".format(uuid)) + + if uuid not in self._vms: + raise aiohttp.web.HTTPNotFound(text="UUID {} doesn't exist".format(uuid)) + return self._vms[uuid] @asyncio.coroutine - def create_vm(self, vmname, identifier=None): - if not identifier: - for i in range(1, 1024): - if i not in self._vms: - identifier = i - break - if identifier == 0: - raise VMError("Maximum number of VM instances reached") - else: - if identifier in self._vms: - raise VMError("VM identifier {} is already used by another VM instance".format(identifier)) - vm = self._VM_CLASS(vmname, identifier, self) - yield from vm.wait_for_creation() - self._vms[vm.id] = vm + def create_vm(self, name, uuid=None): + + #TODO: support for old projects with normal IDs. + + #TODO: supports specific args: pass kwargs to VM_CLASS? + + if not uuid: + uuid = str(uuid4()) + + vm = self._VM_CLASS(name, uuid, self) + future = vm.create() + if isinstance(future, asyncio.Future): + yield from future + self._vms[vm.uuid] = vm return vm @asyncio.coroutine - def start_vm(self, vm_id): - vm = self.get_vm(vm_id) + def start_vm(self, uuid): + + vm = self.get_vm(uuid) yield from vm.start() @asyncio.coroutine - def stop_vm(self, vm_id): - vm = self.get_vm(vm_id) + def stop_vm(self, uuid): + + vm = self.get_vm(uuid) yield from vm.stop() diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index 17aa103b..181faa2d 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -15,9 +15,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - -import asyncio -from .vm_error import VMError from ..config import Config import logging @@ -26,64 +23,62 @@ log = logging.getLogger(__name__) class BaseVM: - def __init__(self, name, identifier, manager): + def __init__(self, name, uuid, manager): self._name = name - self._id = identifier - self._created = asyncio.Future() + self._uuid = uuid self._manager = manager self._config = Config.instance() - asyncio.async(self._create()) - log.info("{type} device {name} [id={id}] has been created".format(type=self.__class__.__name__, - name=self._name, - id=self._id)) #TODO: When delete release console ports - @property - def id(self): + def name(self): """ - Returns the unique ID for this VM. + Returns the name for this VM. - :returns: id (integer) + :returns: name """ - return self._id + return self._name + + @name.setter + def name(self, new_name): + """ + Sets the name of this VM. + + :param new_name: name + """ + + self._name = new_name @property - def name(self): + def uuid(self): """ - Returns the name for this VM. + Returns the UUID for this VM. - :returns: name (string) + :returns: uuid (string) """ - return self._name + return self._uuid - @asyncio.coroutine - def _execute(self, command): + @property + def manager(self): """ - Called when we receive an event. + Returns the manager for this VM. + + :returns: instance of manager """ - raise NotImplementedError + return self._manager - @asyncio.coroutine - def _create(self): + def create(self): """ - Called when the run module is created and ready to receive - commands. It's asynchronous. + Creates the VM. """ - self._created.set_result(True) - log.info("{type} device {name} [id={id}] has been created".format(type=self.__class__.__name__, - name=self._name, - id=self._id)) - def wait_for_creation(self): - return self._created + return - @asyncio.coroutine def start(self): """ Starts the VM process. @@ -91,8 +86,6 @@ class BaseVM: raise NotImplementedError - - @asyncio.coroutine def stop(self): """ Starts the VM process. diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index 289b18a2..9be2f740 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -43,13 +43,23 @@ class Project: self._path = os.path.join(self._location, self._uuid) if os.path.exists(self._path) is False: os.mkdir(self._path) - os.mkdir(os.path.join(self._path, 'files')) + os.mkdir(os.path.join(self._path, "files")) @property def uuid(self): return self._uuid + @property + def location(self): + + return self._location + + @property + def path(self): + + return self._path + def __json__(self): return { diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index efebb34a..12dfb934 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -28,11 +28,13 @@ import tempfile import json import socket import time +import asyncio from .virtualbox_error import VirtualBoxError from ..adapters.ethernet_adapter import EthernetAdapter from ..attic import find_unused_port from .telnet_server import TelnetServer +from ..base_vm import BaseVM if sys.platform.startswith('win'): import msvcrt @@ -42,55 +44,32 @@ import logging log = logging.getLogger(__name__) -class VirtualBoxVM(object): +class VirtualBoxVM(BaseVM): """ VirtualBox VM implementation. - - :param vboxmanage_path: path to the VBoxManage tool - :param name: name of this VirtualBox VM - :param vmname: name of this VirtualBox VM in VirtualBox itself - :param linked_clone: flag if a linked clone must be created - :param working_dir: path to a working directory - :param vbox_id: VirtalBox VM instance ID - :param console: TCP console port - :param console_host: IP address to bind for console connections - :param console_start_port_range: TCP console port range start - :param console_end_port_range: TCP console port range end """ _instances = [] _allocated_console_ports = [] - def __init__(self, - vboxmanage_path, - vbox_user, - name, - vmname, - linked_clone, - working_dir, - vbox_id=None, - console=None, - console_host="0.0.0.0", - console_start_port_range=4512, - console_end_port_range=5000): - - if not vbox_id: - self._id = 0 - for identifier in range(1, 1024): - if identifier not in self._instances: - self._id = identifier - self._instances.append(self._id) - break - - if self._id == 0: - raise VirtualBoxError("Maximum number of VirtualBox VM instances reached") + def __init__(self, name, uuid, manager): + + super().__init__(name, uuid, manager) + + self._system_properties = {} + + #FIXME: harcoded values + if sys.platform.startswith("win"): + self._vboxmanage_path = r"C:\Program Files\Oracle\VirtualBox\VBoxManage.exe" else: - if vbox_id in self._instances: - raise VirtualBoxError("VirtualBox identifier {} is already used by another VirtualBox VM instance".format(vbox_id)) - self._id = vbox_id - self._instances.append(self._id) + self._vboxmanage_path = "/usr/bin/vboxmanage" + + self._queue = asyncio.Queue() + self._created = asyncio.Future() + self._worker = asyncio.async(self._run()) + + return - self._name = name self._linked_clone = linked_clone self._working_dir = None self._command = [] @@ -158,66 +137,99 @@ class VirtualBoxVM(object): log.info("VirtualBox VM {name} [id={id}] has been created".format(name=self._name, id=self._id)) - def defaults(self): - """ - Returns all the default attribute values for this VirtualBox VM. + @asyncio.coroutine + def _execute(self, subcommand, args, timeout=60): - :returns: default values (dictionary) - """ + command = [self._vboxmanage_path, "--nologo", subcommand] + command.extend(args) + try: + process = yield from asyncio.create_subprocess_exec(*command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) + except (OSError, subprocess.SubprocessError) as e: + raise VirtualBoxError("Could not execute VBoxManage: {}".format(e)) - vbox_defaults = {"name": self._name, - "vmname": self._vmname, - "adapters": self.adapters, - "adapter_start_index": self._adapter_start_index, - "adapter_type": "Intel PRO/1000 MT Desktop (82540EM)", - "console": self._console, - "enable_remote_console": self._enable_remote_console, - "headless": self._headless} + try: + stdout_data, stderr_data = yield from asyncio.wait_for(process.communicate(), timeout=timeout) + except asyncio.TimeoutError: + raise VirtualBoxError("VBoxManage has timed out after {} seconds!".format(timeout)) - return vbox_defaults + if process.returncode: + # only the first line of the output is useful + vboxmanage_error = stderr_data.decode("utf-8", errors="ignore").splitlines()[0] + raise VirtualBoxError(vboxmanage_error) - @property - def id(self): - """ - Returns the unique ID for this VirtualBox VM. + return stdout_data.decode("utf-8", errors="ignore").splitlines() - :returns: id (integer) - """ + @asyncio.coroutine + def _get_system_properties(self): + + properties = yield from self._execute("list", ["systemproperties"]) + for prop in properties: + try: + name, value = prop.split(':', 1) + except ValueError: + continue + self._system_properties[name.strip()] = value.strip() - return self._id + @asyncio.coroutine + def _run(self): - @classmethod - def reset(cls): - """ - Resets allocated instance list. - """ + try: + yield from self._get_system_properties() + self._created.set_result(True) + except VirtualBoxError as e: + self._created.set_exception(e) + return + + while True: + future, subcommand, args = yield from self._queue.get() + try: + yield from self._execute(subcommand, args) + future.set_result(True) + except VirtualBoxError as e: + future.set_exception(e) - cls._instances.clear() - cls._allocated_console_ports.clear() + def create(self): - @property - def name(self): - """ - Returns the name of this VirtualBox VM. + return self._created - :returns: name - """ + def _put(self, item): - return self._name + try: + self._queue.put_nowait(item) + except asyncio.qeues.QueueFull: + raise VirtualBoxError("Queue is full") - @name.setter - def name(self, new_name): + def start(self): + + args = [self._name] + future = asyncio.Future() + self._put((future, "startvm", args)) + return future + + def stop(self): + + args = [self._name, "poweroff"] + future = asyncio.Future() + self._put((future, "controlvm", args)) + return future + + def defaults(self): """ - Sets the name of this VirtualBox VM. + Returns all the default attribute values for this VirtualBox VM. - :param new_name: name + :returns: default values (dictionary) """ - log.info("VirtualBox VM {name} [id={id}]: renamed to {new_name}".format(name=self._name, - id=self._id, - new_name=new_name)) + vbox_defaults = {"name": self._name, + "vmname": self._vmname, + "adapters": self.adapters, + "adapter_start_index": self._adapter_start_index, + "adapter_type": "Intel PRO/1000 MT Desktop (82540EM)", + "console": self._console, + "enable_remote_console": self._enable_remote_console, + "headless": self._headless} - self._name = new_name + return vbox_defaults @property def working_dir(self): @@ -540,7 +552,7 @@ class VirtualBoxVM(object): id=self._id, adapter_type=adapter_type)) - def _execute(self, subcommand, args, timeout=60): + def _old_execute(self, subcommand, args, timeout=60): """ Executes a command with VBoxManage. @@ -831,7 +843,7 @@ class VirtualBoxVM(object): self._serial_pipe.close() self._serial_pipe = None - def start(self): + def old_start(self): """ Starts this VirtualBox VM. """ @@ -864,7 +876,7 @@ class VirtualBoxVM(object): if self._enable_remote_console: self._start_remote_console() - def stop(self): + def old_stop(self): """ Stops this VirtualBox VM. """ diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py index 9d36c305..eed301c3 100644 --- a/gns3server/modules/vpcs/vpcs_device.py +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -47,14 +47,14 @@ class VPCSDevice(BaseVM): VPCS device implementation. :param name: name of this VPCS device - :param vpcs_id: VPCS instance ID + :param uuid: VPCS instance UUID :param manager: parent VM Manager :param working_dir: path to a working directory :param console: TCP console port """ - def __init__(self, name, vpcs_id, manager, working_dir=None, console=None): + def __init__(self, name, uuid, manager, working_dir=None, console=None): - super().__init__(name, vpcs_id, manager) + super().__init__(name, uuid, manager) # TODO: Hardcodded for testing #self._working_dir = working_dir @@ -120,17 +120,8 @@ class VPCSDevice(BaseVM): return self._console - @property - def name(self): - """ - Returns the name of this VPCS device. - - :returns: name - """ - - return self._name - - @name.setter + #FIXME: correct way to subclass a property? + @BaseVM.name.setter def name(self, new_name): """ Sets the name of this VPCS device. @@ -151,10 +142,10 @@ class VPCSDevice(BaseVM): except OSError as e: raise VPCSError("Could not amend the configuration {}: {}".format(config_path, e)) - log.info("VPCS {name} [id={id}]: renamed to {new_name}".format(name=self._name, - id=self._id, - new_name=new_name)) - self._name = new_name + log.info("VPCS {name} [{uuid}]: renamed to {new_name}".format(name=self._name, + uuid=self.uuid, + new_name=new_name)) + BaseVM.name = new_name def _check_vpcs_version(self): """ @@ -197,7 +188,7 @@ class VPCSDevice(BaseVM): stderr=subprocess.STDOUT, cwd=self._working_dir, creationflags=flags) - log.info("VPCS instance {} started PID={}".format(self._id, self._process.pid)) + log.info("VPCS instance {} started PID={}".format(self.name, self._process.pid)) self._started = True except (OSError, subprocess.SubprocessError) as e: vpcs_stdout = self.read_vpcs_stdout() @@ -212,7 +203,7 @@ class VPCSDevice(BaseVM): # stop the VPCS process if self.is_running(): - log.info("stopping VPCS instance {} PID={}".format(self._id, self._process.pid)) + log.info("stopping VPCS instance {} PID={}".format(self.name, self._process.pid)) if sys.platform.startswith("win32"): self._process.send_signal(signal.CTRL_BREAK_EVENT) else: @@ -283,10 +274,10 @@ class VPCSDevice(BaseVM): self._ethernet_adapter.add_nio(port_id, nio) - log.info("VPCS {name} [id={id}]: {nio} added to port {port_id}".format(name=self._name, - id=self._id, - nio=nio, - port_id=port_id)) + log.info("VPCS {name} {uuid}]: {nio} added to port {port_id}".format(name=self._name, + uuid=self.uuid, + nio=nio, + port_id=port_id)) return nio def port_remove_nio_binding(self, port_id): @@ -304,10 +295,10 @@ class VPCSDevice(BaseVM): nio = self._ethernet_adapter.get_nio(port_id) self._ethernet_adapter.remove_nio(port_id) - log.info("VPCS {name} [id={id}]: {nio} removed from port {port_id}".format(name=self._name, - id=self._id, - nio=nio, - port_id=port_id)) + log.info("VPCS {name} [{uuid}]: {nio} removed from port {port_id}".format(name=self._name, + uuid=self.uuid, + nio=nio, + port_id=port_id)) return nio def _build_command(self): @@ -364,7 +355,8 @@ class VPCSDevice(BaseVM): command.extend(["-e"]) command.extend(["-d", nio.tap_device]) - command.extend(["-m", str(self._id)]) # the unique ID is used to set the MAC address offset + #FIXME: find workaround + #command.extend(["-m", str(self._id)]) # the unique ID is used to set the MAC address offset command.extend(["-i", "1"]) # option to start only one VPC instance command.extend(["-F"]) # option to avoid the daemonization of VPCS if self._script_file: @@ -390,6 +382,6 @@ class VPCSDevice(BaseVM): """ self._script_file = script_file - log.info("VPCS {name} [id={id}]: script_file set to {config}".format(name=self._name, - id=self._id, - config=self._script_file)) + log.info("VPCS {name} [{uuid}]: script_file set to {config}".format(name=self._name, + uuid=self.uuid, + config=self._script_file)) diff --git a/gns3server/schemas/virtualbox.py b/gns3server/schemas/virtualbox.py index 67c0568c..1dea7163 100644 --- a/gns3server/schemas/virtualbox.py +++ b/gns3server/schemas/virtualbox.py @@ -36,69 +36,37 @@ VBOX_CREATE_SCHEMA = { "type": "boolean" }, "vbox_id": { - "description": "VirtualBox VM instance ID", + "description": "VirtualBox VM instance ID (for project created before GNS3 1.3)", "type": "integer" }, - "console": { - "description": "console TCP port", - "minimum": 1, - "maximum": 65535, - "type": "integer" + "uuid": { + "description": "VirtualBox VM instance UUID", + "type": "string", + "minLength": 36, + "maxLength": 36, + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" }, }, "additionalProperties": False, "required": ["name", "vmname"], } -VBOX_DELETE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to delete a VirtualBox VM instance", - "type": "object", - "properties": { - "id": { - "description": "VirtualBox VM instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -VBOX_UPDATE_SCHEMA = { +VBOX_OBJECT_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to update a VirtualBox VM instance", + "description": "VirtualBox VM instance", "type": "object", "properties": { - "id": { - "description": "VirtualBox VM instance ID", - "type": "integer" - }, "name": { "description": "VirtualBox VM instance name", "type": "string", "minLength": 1, }, - "vmname": { - "description": "VirtualBox VM name (in VirtualBox itself)", + "uuid": { + "description": "VirtualBox VM instance UUID", "type": "string", - "minLength": 1, - }, - "adapters": { - "description": "number of adapters", - "type": "integer", - "minimum": 1, - "maximum": 36, # maximum given by the ICH9 chipset in VirtualBox - }, - "adapter_start_index": { - "description": "adapter index from which to start using adapters", - "type": "integer", - "minimum": 0, - "maximum": 35, # maximum given by the ICH9 chipset in VirtualBox - }, - "adapter_type": { - "description": "VirtualBox adapter type", - "type": "string", - "minLength": 1, + "minLength": 36, + "maxLength": 36, + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" }, "console": { "description": "console TCP port", @@ -106,327 +74,8 @@ VBOX_UPDATE_SCHEMA = { "maximum": 65535, "type": "integer" }, - "enable_remote_console": { - "description": "enable the remote console", - "type": "boolean" - }, - "headless": { - "description": "headless mode", - "type": "boolean" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -VBOX_START_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to start a VirtualBox VM instance", - "type": "object", - "properties": { - "id": { - "description": "VirtualBox VM instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -VBOX_STOP_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to stop a VirtualBox VM instance", - "type": "object", - "properties": { - "id": { - "description": "VirtualBox VM instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -VBOX_SUSPEND_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to suspend a VirtualBox VM instance", - "type": "object", - "properties": { - "id": { - "description": "VirtualBox VM instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -VBOX_RELOAD_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to reload a VirtualBox VM instance", - "type": "object", - "properties": { - "id": { - "description": "VirtualBox VM instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -VBOX_ALLOCATE_UDP_PORT_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to allocate an UDP port for a VirtualBox VM instance", - "type": "object", - "properties": { - "id": { - "description": "VirtualBox VM instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the VirtualBox VM instance", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id", "port_id"] -} - -VBOX_ADD_NIO_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to add a NIO for a VirtualBox VM 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": "VirtualBox VM instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the VirtualBox VM instance", - "type": "integer" - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 0, - "maximum": 36 # maximum given by the ICH9 chipset in VirtualBox - }, - "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"] -} - - -VBOX_DELETE_NIO_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to delete a NIO for a VirtualBox VM instance", - "type": "object", - "properties": { - "id": { - "description": "VirtualBox VM instance ID", - "type": "integer" - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 0, - "maximum": 36 # maximum given by the ICH9 chipset in VirtualBox - }, - }, - "additionalProperties": False, - "required": ["id", "port"] -} - -VBOX_START_CAPTURE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to start a packet capture on a VirtualBox VM instance port", - "type": "object", - "properties": { - "id": { - "description": "VirtualBox VM instance ID", - "type": "integer" - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 0, - "maximum": 36 # maximum given by the ICH9 chipset in VirtualBox - }, - "port_id": { - "description": "Unique port identifier for the VirtualBox VM instance", - "type": "integer" - }, - "capture_file_name": { - "description": "Capture file name", - "type": "string", - "minLength": 1, - }, - }, - "additionalProperties": False, - "required": ["id", "port", "port_id", "capture_file_name"] -} - -VBOX_STOP_CAPTURE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to stop a packet capture on a VirtualBox VM instance port", - "type": "object", - "properties": { - "id": { - "description": "VirtualBox VM instance ID", - "type": "integer" - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 0, - "maximum": 36 # maximum given by the ICH9 chipset in VirtualBox - }, - "port_id": { - "description": "Unique port identifier for the VirtualBox VM instance", - "type": "integer" - }, }, "additionalProperties": False, - "required": ["id", "port", "port_id"] + "required": ["name", "uuid"] } diff --git a/gns3server/schemas/vpcs.py b/gns3server/schemas/vpcs.py index c4b7c71c..275320de 100644 --- a/gns3server/schemas/vpcs.py +++ b/gns3server/schemas/vpcs.py @@ -26,8 +26,8 @@ VPCS_CREATE_SCHEMA = { "type": "string", "minLength": 1, }, - "id": { - "description": "VPCS device instance ID", + "vpcs_id": { + "description": "VPCS device instance ID (for project created before GNS3 1.3)", "type": "integer" }, "uuid": { @@ -117,9 +117,12 @@ VPCS_OBJECT_SCHEMA = { "type": "string", "minLength": 1, }, - "id": { - "description": "VPCS device instance ID", - "type": "integer" + "uuid": { + "description": "VPCS device UUID", + "type": "string", + "minLength": 36, + "maxLength": 36, + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" }, "console": { "description": "console TCP port", @@ -129,6 +132,6 @@ VPCS_OBJECT_SCHEMA = { }, }, "additionalProperties": False, - "required": ["name", "id", "console"] + "required": ["name", "uuid", "console"] } diff --git a/gns3server/server.py b/gns3server/server.py index 0db04449..d476a003 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -35,6 +35,7 @@ from .modules.port_manager import PortManager #TODO: get rid of * have something generic to automatically import handlers so the routes can be found from gns3server.handlers import * +from gns3server.handlers.virtualbox_handler import VirtualBoxHandler import logging log = logging.getLogger(__name__) diff --git a/tests/api/base.py b/tests/api/base.py index 10a432a4..b63b7674 100644 --- a/tests/api/base.py +++ b/tests/api/base.py @@ -25,6 +25,7 @@ import pytest from aiohttp import web import aiohttp + from gns3server.web.route import Route #TODO: get rid of * from gns3server.handlers import * @@ -95,7 +96,7 @@ class Query: if path is None: return with open(self._example_file_path(method, path), 'w+') as f: - f.write("curl -i -x{} 'http://localhost:8000{}'".format(method, path)) + f.write("curl -i -X {} 'http://localhost:8000{}'".format(method, path)) if body: f.write(" -d '{}'".format(re.sub(r"\n", "", json.dumps(json.loads(body), sort_keys=True)))) f.write("\n\n") @@ -116,7 +117,7 @@ class Query: def _example_file_path(self, method, path): path = re.sub('[^a-z0-9]', '', path) - return "docs/api/examples/{}_{}.txt".format(method.lower(), path) + return "docs/api/examples/{}_{}.txt".format(method.lower(), path) # FIXME: cannot find path when running tests def _get_unused_port(): diff --git a/tests/api/test_virtualbox.py b/tests/api/test_virtualbox.py new file mode 100644 index 00000000..a73700f9 --- /dev/null +++ b/tests/api/test_virtualbox.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 . + +from tests.utils import asyncio_patch + + +@asyncio_patch("gns3server.modules.VirtualBox.create_vm", return_value="61d61bdd-aa7d-4912-817f-65a9eb54d3ab") +def test_vbox_create(server): + response = server.post("/virtualbox", {"name": "VM1"}, example=False) + assert response.status == 200 + assert response.route == "/virtualbox" + assert response.json["name"] == "VM1" + assert response.json["uuid"] == "61d61bdd-aa7d-4912-817f-65a9eb54d3ab" + + +@asyncio_patch("gns3server.modules.VirtualBox.start_vm", return_value=True) +def test_vbox_start(server): + response = server.post("/virtualbox/61d61bdd-aa7d-4912-817f-65a9eb54d3ab/start", {}, example=False) + assert response.status == 204 + assert response.route == "/virtualbox/61d61bdd-aa7d-4912-817f-65a9eb54d3ab/start" + + +@asyncio_patch("gns3server.modules.VirtualBox.stop_vm", return_value=True) +def test_vbox_stop(server): + response = server.post("/virtualbox/61d61bdd-aa7d-4912-817f-65a9eb54d3ab/stop", {}, example=False) + assert response.status == 204 + assert response.route == "/virtualbox/61d61bdd-aa7d-4912-817f-65a9eb54d3ab/stop" diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index 9dbbf94c..12244d3f 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -15,55 +15,49 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from tests.api.base import server, loop from tests.utils import asyncio_patch -from gns3server import modules from unittest.mock import patch -@asyncio_patch('gns3server.modules.VPCS.create_vm', return_value=84) + +@asyncio_patch("gns3server.modules.VPCS.create_vm", return_value="61d61bdd-aa7d-4912-817f-65a9eb54d3ab") def test_vpcs_create(server): - response = server.post('/vpcs', {'name': 'PC TEST 1'}, example=False) + response = server.post("/vpcs", {"name": "PC TEST 1"}, example=False) assert response.status == 200 - assert response.route == '/vpcs' - assert response.json['name'] == 'PC TEST 1' - assert response.json['id'] == 84 + assert response.route == "/vpcs" + assert response.json["name"] == "PC TEST 1" + assert response.json["uuid"] == "61d61bdd-aa7d-4912-817f-65a9eb54d3ab" +#FIXME def test_vpcs_nio_create_udp(server): - vm = server.post('/vpcs', {'name': 'PC TEST 1'}) - response = server.post('/vpcs/{}/ports/0/nio'.format(vm.json["id"]), { - 'type': 'nio_udp', - 'lport': 4242, - 'rport': 4343, - 'rhost': '127.0.0.1' - }, - example=True) + vm = server.post("/vpcs", {"name": "PC TEST 1"}) + response = server.post("/vpcs/{}/ports/0/nio".format(vm.json["uuid"]), {"type": "nio_udp", + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}, + example=True) assert response.status == 200 - assert response.route == '/vpcs/{id:\d+}/ports/{port_id}/nio' - assert response.json['type'] == 'nio_udp' + assert response.route == "/vpcs/{uuid}/ports/{port_id}/nio" + assert response.json["type"] == "nio_udp" + @patch("gns3server.modules.vpcs.vpcs_device.has_privileged_access", return_value=True) def test_vpcs_nio_create_tap(mock, server): - vm = server.post('/vpcs', {'name': 'PC TEST 1'}) - response = server.post('/vpcs/{}/ports/0/nio'.format(vm.json["id"]), { - 'type': 'nio_tap', - 'tap_device': 'test', - }) + vm = server.post("/vpcs", {"name": "PC TEST 1"}) + response = server.post("/vpcs/{}/ports/0/nio".format(vm.json["uuid"]), {"type": "nio_tap", + "tap_device": "test"}) assert response.status == 200 - assert response.route == '/vpcs/{id:\d+}/ports/{port_id}/nio' - assert response.json['type'] == 'nio_tap' + assert response.route == "/vpcs/{uuid}/ports/{port_id}/nio" + assert response.json["type"] == "nio_tap" + +#FIXME def test_vpcs_delete_nio(server): - vm = server.post('/vpcs', {'name': 'PC TEST 1'}) - response = server.post('/vpcs/{}/ports/0/nio'.format(vm.json["id"]), { - 'type': 'nio_udp', - 'lport': 4242, - 'rport': 4343, - 'rhost': '127.0.0.1' - }, - ) - response = server.delete('/vpcs/{}/ports/0/nio'.format(vm.json["id"]), example=True) + vm = server.post("/vpcs", {"name": "PC TEST 1"}) + response = server.post("/vpcs/{}/ports/0/nio".format(vm.json["uuid"]), {"type": "nio_udp", + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}) + response = server.delete("/vpcs/{}/ports/0/nio".format(vm.json["uuid"]), example=True) assert response.status == 200 - assert response.route == '/vpcs/{id:\d+}/ports/{port_id}/nio' - - + assert response.route == "/vpcs/{uuid}/ports/{port_id}/nio" diff --git a/tests/conftest.py b/tests/conftest.py index a7ca89ef..3c62baca 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,6 @@ def server(request): cwd = os.path.dirname(os.path.abspath(__file__)) server_script = os.path.join(cwd, "../gns3server/main.py") process = subprocess.Popen([sys.executable, server_script, "--port=8000"]) - time.sleep(1) # give some time for the process to start + #time.sleep(1) # give some time for the process to start request.addfinalizer(process.terminate) return process From 927e6b540d6a606fdf2df04bc545ae4bc789f19d Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 20 Jan 2015 09:58:58 +0100 Subject: [PATCH 047/485] Fix tests --- docs/api/examples/get_version.txt | 2 +- docs/api/examples/post_project.txt | 8 ++++---- docs/api/examples/post_version.txt | 2 +- tests/api/test_vpcs.py | 1 + tests/modules/vpcs/test_vpcs_device.py | 24 ++++++++++++------------ 5 files changed, 19 insertions(+), 18 deletions(-) diff --git a/docs/api/examples/get_version.txt b/docs/api/examples/get_version.txt index 3e711fce..59bdb128 100644 --- a/docs/api/examples/get_version.txt +++ b/docs/api/examples/get_version.txt @@ -1,4 +1,4 @@ -curl -i -xGET 'http://localhost:8000/version' +curl -i -X GET 'http://localhost:8000/version' GET /version HTTP/1.1 diff --git a/docs/api/examples/post_project.txt b/docs/api/examples/post_project.txt index ab6d97fc..e68380af 100644 --- a/docs/api/examples/post_project.txt +++ b/docs/api/examples/post_project.txt @@ -1,8 +1,8 @@ -curl -i -xPOST 'http://localhost:8000/project' -d '{"location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-253/test_create_project_with_dir0"}' +curl -i -X POST 'http://localhost:8000/project' -d '{"location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-262/test_create_project_with_dir0"}' POST /project HTTP/1.1 { - "location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-253/test_create_project_with_dir0" + "location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-262/test_create_project_with_dir0" } @@ -15,6 +15,6 @@ SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /project { - "location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-253/test_create_project_with_dir0", - "uuid": "a8984692-a820-45de-8d4d-fc006b29072a" + "location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-262/test_create_project_with_dir0", + "uuid": "66821e79-aa05-4490-8c26-ffb7aeddc0d2" } diff --git a/docs/api/examples/post_version.txt b/docs/api/examples/post_version.txt index 90ab4d81..45ef1069 100644 --- a/docs/api/examples/post_version.txt +++ b/docs/api/examples/post_version.txt @@ -1,4 +1,4 @@ -curl -i -xPOST 'http://localhost:8000/version' -d '{"version": "1.3.dev1"}' +curl -i -X POST 'http://localhost:8000/version' -d '{"version": "1.3.dev1"}' POST /version HTTP/1.1 { diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index 12244d3f..a63a6f9a 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from tests.api.base import server, loop from tests.utils import asyncio_patch from unittest.mock import patch diff --git a/tests/modules/vpcs/test_vpcs_device.py b/tests/modules/vpcs/test_vpcs_device.py index 3d532f1a..1b32e2ca 100644 --- a/tests/modules/vpcs/test_vpcs_device.py +++ b/tests/modules/vpcs/test_vpcs_device.py @@ -36,27 +36,27 @@ def manager(): @patch("subprocess.check_output", return_value="Welcome to Virtual PC Simulator, version 0.6".encode("utf-8")) def test_vm(tmpdir, manager): - vm = VPCSDevice("test", 42, manager, working_dir=str(tmpdir)) + vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", manager, working_dir=str(tmpdir)) assert vm.name == "test" - assert vm.id == 42 + assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0f" @patch("subprocess.check_output", return_value="Welcome to Virtual PC Simulator, version 0.1".encode("utf-8")) def test_vm_invalid_vpcs_version(tmpdir, manager): with pytest.raises(VPCSError): - vm = VPCSDevice("test", 42, manager, working_dir=str(tmpdir)) + vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", manager, working_dir=str(tmpdir)) assert vm.name == "test" - assert vm.id == 42 + assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0f" @patch("gns3server.config.Config.get_section_config", return_value = {"path": "/bin/test_fake"}) def test_vm_invalid_vpcs_path(tmpdir, manager): with pytest.raises(VPCSError): - vm = VPCSDevice("test", 42, manager, working_dir=str(tmpdir)) + vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", manager, working_dir=str(tmpdir)) assert vm.name == "test" - assert vm.id == 42 + assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0f" def test_start(tmpdir, loop, manager): with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): - vm = VPCSDevice("test", 42, manager, working_dir=str(tmpdir)) + vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", manager, working_dir=str(tmpdir)) nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) loop.run_until_complete(asyncio.async(vm.start())) @@ -65,7 +65,7 @@ def test_start(tmpdir, loop, manager): def test_stop(tmpdir, loop, manager): process = MagicMock() with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): - vm = VPCSDevice("test", 42, manager, working_dir=str(tmpdir)) + vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", manager, working_dir=str(tmpdir)) nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) loop.run_until_complete(asyncio.async(vm.start())) @@ -75,25 +75,25 @@ def test_stop(tmpdir, loop, manager): process.terminate.assert_called_with() def test_add_nio_binding_udp(tmpdir, manager): - vm = VPCSDevice("test", 42, manager, working_dir=str(tmpdir)) + vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", manager, working_dir=str(tmpdir)) nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) assert nio.lport == 4242 def test_add_nio_binding_tap(tmpdir, manager): - vm = VPCSDevice("test", 42, manager, working_dir=str(tmpdir)) + vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", manager, working_dir=str(tmpdir)) with patch("gns3server.modules.vpcs.vpcs_device.has_privileged_access", return_value=True): nio = vm.port_add_nio_binding(0, {"type": "nio_tap", "tap_device": "test"}) assert nio.tap_device == "test" def test_add_nio_binding_tap_no_privileged_access(tmpdir, manager): - vm = VPCSDevice("test", 42, manager, working_dir=str(tmpdir)) + vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", manager, working_dir=str(tmpdir)) with patch("gns3server.modules.vpcs.vpcs_device.has_privileged_access", return_value=False): with pytest.raises(VPCSError): vm.port_add_nio_binding(0, {"type": "nio_tap", "tap_device": "test"}) assert vm._ethernet_adapter.ports[0] is None def test_port_remove_nio_binding(tmpdir, manager): - vm = VPCSDevice("test", 42, manager, working_dir=str(tmpdir)) + vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", manager, working_dir=str(tmpdir)) nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) vm.port_remove_nio_binding(0) assert vm._ethernet_adapter.ports[0] is None From 0695e75e774f63e730f6aefec29d665768c220ea Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 20 Jan 2015 12:46:15 +0100 Subject: [PATCH 048/485] Fix tests --- .../delete_vpcsuuidportsportidnio.txt | 15 +++++++ docs/api/examples/post_project.txt | 8 ++-- .../examples/post_vpcsuuidportsportidnio.txt | 25 +++++++++++ gns3server/handlers/vpcs_handler.py | 5 ++- gns3server/modules/base_manager.py | 15 +++++-- gns3server/modules/base_vm.py | 8 +++- gns3server/modules/project.py | 1 + gns3server/modules/project_manager.py | 2 + gns3server/modules/vpcs/vpcs_device.py | 12 +++--- gns3server/schemas/vpcs.py | 18 +++++++- old_tests/vpcs/test_vpcs_device.py | 33 --------------- tests/api/base.py | 7 ++++ tests/api/test_vpcs.py | 32 ++++++++------ tests/modules/vpcs/test_vpcs_device.py | 42 ++++++++++--------- 14 files changed, 139 insertions(+), 84 deletions(-) create mode 100644 docs/api/examples/delete_vpcsuuidportsportidnio.txt create mode 100644 docs/api/examples/post_vpcsuuidportsportidnio.txt delete mode 100644 old_tests/vpcs/test_vpcs_device.py diff --git a/docs/api/examples/delete_vpcsuuidportsportidnio.txt b/docs/api/examples/delete_vpcsuuidportsportidnio.txt new file mode 100644 index 00000000..64bbd964 --- /dev/null +++ b/docs/api/examples/delete_vpcsuuidportsportidnio.txt @@ -0,0 +1,15 @@ +curl -i -X DELETE 'http://localhost:8000/vpcs/{uuid}/ports/{port_id}/nio' + +DELETE /vpcs/{uuid}/ports/{port_id}/nio HTTP/1.1 + + + +HTTP/1.1 200 +CONNECTION: close +CONTENT-LENGTH: 2 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 aiohttp/0.13.1 +X-ROUTE: /vpcs/{uuid}/ports/{port_id}/nio + +{} diff --git a/docs/api/examples/post_project.txt b/docs/api/examples/post_project.txt index e68380af..6d61c41f 100644 --- a/docs/api/examples/post_project.txt +++ b/docs/api/examples/post_project.txt @@ -1,8 +1,8 @@ -curl -i -X POST 'http://localhost:8000/project' -d '{"location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-262/test_create_project_with_dir0"}' +curl -i -X POST 'http://localhost:8000/project' -d '{"location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-301/test_create_project_with_dir0"}' POST /project HTTP/1.1 { - "location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-262/test_create_project_with_dir0" + "location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-301/test_create_project_with_dir0" } @@ -15,6 +15,6 @@ SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /project { - "location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-262/test_create_project_with_dir0", - "uuid": "66821e79-aa05-4490-8c26-ffb7aeddc0d2" + "location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-301/test_create_project_with_dir0", + "uuid": "d4d66c3f-439b-4ae9-972b-59040189c995" } diff --git a/docs/api/examples/post_vpcsuuidportsportidnio.txt b/docs/api/examples/post_vpcsuuidportsportidnio.txt new file mode 100644 index 00000000..f29d6532 --- /dev/null +++ b/docs/api/examples/post_vpcsuuidportsportidnio.txt @@ -0,0 +1,25 @@ +curl -i -X POST 'http://localhost:8000/vpcs/{uuid}/ports/{port_id}/nio' -d '{"lport": 4242, "rhost": "127.0.0.1", "rport": 4343, "type": "nio_udp"}' + +POST /vpcs/{uuid}/ports/{port_id}/nio HTTP/1.1 +{ + "lport": 4242, + "rhost": "127.0.0.1", + "rport": 4343, + "type": "nio_udp" +} + + +HTTP/1.1 200 +CONNECTION: close +CONTENT-LENGTH: 89 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 aiohttp/0.13.1 +X-ROUTE: /vpcs/{uuid}/ports/{port_id}/nio + +{ + "lport": 4242, + "rhost": "127.0.0.1", + "rport": 4343, + "type": "nio_udp" +} diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index 28e72331..8c729123 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -40,10 +40,11 @@ class VPCSHandler: def create(request, response): vpcs = VPCS.instance() - vm = yield from vpcs.create_vm(request.json["name"], request.json.get("uuid")) + vm = yield from vpcs.create_vm(request.json["name"], request.json["project_uuid"], uuid = request.json.get("uuid")) response.json({"name": vm.name, "uuid": vm.uuid, - "console": vm.console}) + "console": vm.console, + "project_uuid": vm.project.uuid}) @classmethod @Route.post( diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 71f78d2b..926a8578 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -20,6 +20,7 @@ import asyncio import aiohttp from uuid import UUID, uuid4 +from .project_manager import ProjectManager class BaseManager: @@ -85,16 +86,24 @@ class BaseManager: return self._vms[uuid] @asyncio.coroutine - def create_vm(self, name, uuid=None): + def create_vm(self, name, project_identifier, uuid=None): + """ + Create a new VM + + :param name VM name + :param project_identifier UUID of Project + :param uuid Force UUID force VM + """ + project = ProjectManager.instance().get_project(project_identifier) - #TODO: support for old projects with normal IDs. + #TODO: support for old projects VM with normal IDs. #TODO: supports specific args: pass kwargs to VM_CLASS? if not uuid: uuid = str(uuid4()) - vm = self._VM_CLASS(name, uuid, self) + vm = self._VM_CLASS(name, uuid, project, self) future = vm.create() if isinstance(future, asyncio.Future): yield from future diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index 181faa2d..e075ec3e 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -23,15 +23,21 @@ log = logging.getLogger(__name__) class BaseVM: - def __init__(self, name, uuid, manager): + def __init__(self, name, uuid, project, manager): self._name = name self._uuid = uuid + self._project = project self._manager = manager self._config = Config.instance() #TODO: When delete release console ports + @property + def project(self): + """Return VM current project""" + return self._project + @property def name(self): """ diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index 9be2f740..fa427027 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -34,6 +34,7 @@ class Project: if uuid is None: self._uuid = str(uuid4()) else: + assert len(uuid) == 36 self._uuid = uuid self._location = location diff --git a/gns3server/modules/project_manager.py b/gns3server/modules/project_manager.py index 98dab785..f2a75e4a 100644 --- a/gns3server/modules/project_manager.py +++ b/gns3server/modules/project_manager.py @@ -48,6 +48,8 @@ class ProjectManager: :returns: Project instance """ + assert len(project_id) == 36 + if project_id not in self._projects: raise aiohttp.web.HTTPNotFound(text="Project UUID {} doesn't exist".format(project_id)) return self._projects[project_id] diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py index eed301c3..0b720176 100644 --- a/gns3server/modules/vpcs/vpcs_device.py +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -48,22 +48,22 @@ class VPCSDevice(BaseVM): :param name: name of this VPCS device :param uuid: VPCS instance UUID + :param project: Project instance :param manager: parent VM Manager :param working_dir: path to a working directory :param console: TCP console port """ - def __init__(self, name, uuid, manager, working_dir=None, console=None): + def __init__(self, name, uuid, project, manager, working_dir=None, console=None): - super().__init__(name, uuid, manager) - - # TODO: Hardcodded for testing - #self._working_dir = working_dir - self._working_dir = "/tmp" + super().__init__(name, uuid, project, manager) self._path = self._config.get_section_config("VPCS").get("path", "vpcs") self._console = console + #TODO: remove working_dir + self._working_dir = "/tmp" + self._command = [] self._process = None self._vpcs_stdout_file = "" diff --git a/gns3server/schemas/vpcs.py b/gns3server/schemas/vpcs.py index 275320de..d2486ce4 100644 --- a/gns3server/schemas/vpcs.py +++ b/gns3server/schemas/vpcs.py @@ -37,6 +37,13 @@ VPCS_CREATE_SCHEMA = { "maxLength": 36, "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" }, + "project_uuid": { + "description": "Project UUID", + "type": "string", + "minLength": 36, + "maxLength": 36, + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + }, "console": { "description": "console TCP port", "minimum": 1, @@ -45,7 +52,7 @@ VPCS_CREATE_SCHEMA = { }, }, "additionalProperties": False, - "required": ["name"] + "required": ["name", "project_uuid"] } @@ -130,8 +137,15 @@ VPCS_OBJECT_SCHEMA = { "maximum": 65535, "type": "integer" }, + "project_uuid": { + "description": "Project UUID", + "type": "string", + "minLength": 36, + "maxLength": 36, + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + } }, "additionalProperties": False, - "required": ["name", "uuid", "console"] + "required": ["name", "uuid", "console", "project_uuid"] } diff --git a/old_tests/vpcs/test_vpcs_device.py b/old_tests/vpcs/test_vpcs_device.py deleted file mode 100644 index 781166b4..00000000 --- a/old_tests/vpcs/test_vpcs_device.py +++ /dev/null @@ -1,33 +0,0 @@ -from gns3server.modules.vpcs import VPCSDevice -import os -import pytest - - -@pytest.fixture(scope="session") -def vpcs(request): - - if os.path.isfile("/usr/bin/vpcs"): - vpcs_path = "/usr/bin/vpcs" - else: - cwd = os.path.dirname(os.path.abspath(__file__)) - vpcs_path = os.path.join(cwd, "vpcs") - vpcs_device = VPCSDevice("VPCS1", vpcs_path, "/tmp") - vpcs_device.port_add_nio_binding(0, 'nio_tap:tap0') - vpcs_device.start() - request.addfinalizer(vpcs_device.delete) - return vpcs_device - - -def test_vpcs_is_started(vpcs): - - print(vpcs.command()) - assert vpcs.id == 1 # we should have only one VPCS running! - assert vpcs.is_running() - - -def test_vpcs_restart(vpcs): - - vpcs.stop() - assert not vpcs.is_running() - vpcs.start() - assert vpcs.is_running() diff --git a/tests/api/base.py b/tests/api/base.py index b63b7674..5f5981d1 100644 --- a/tests/api/base.py +++ b/tests/api/base.py @@ -31,6 +31,7 @@ from gns3server.web.route import Route from gns3server.handlers import * from gns3server.modules import MODULES from gns3server.modules.port_manager import PortManager +from gns3server.modules.project_manager import ProjectManager class Query: @@ -162,3 +163,9 @@ def server(request, loop): srv.wait_closed() request.addfinalizer(tear_down) return Query(loop, host=host, port=port) + + +@pytest.fixture(scope="module") +def project(): + return ProjectManager.instance().create_project(uuid = "a1e920ca-338a-4e9f-b363-aa607b09dd80") + diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index a63a6f9a..3e23071e 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -15,24 +15,32 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from tests.api.base import server, loop +import pytest +from tests.api.base import server, loop, project from tests.utils import asyncio_patch from unittest.mock import patch +@pytest.fixture(scope="module") +def vm(server, project): + response = server.post("/vpcs", {"name": "PC TEST 1", "project_uuid": project.uuid}) + assert response.status == 200 + return response.json + + @asyncio_patch("gns3server.modules.VPCS.create_vm", return_value="61d61bdd-aa7d-4912-817f-65a9eb54d3ab") -def test_vpcs_create(server): - response = server.post("/vpcs", {"name": "PC TEST 1"}, example=False) +def test_vpcs_create(server, project): + response = server.post("/vpcs", {"name": "PC TEST 1", "project_uuid": project.uuid}, example=True) assert response.status == 200 assert response.route == "/vpcs" assert response.json["name"] == "PC TEST 1" assert response.json["uuid"] == "61d61bdd-aa7d-4912-817f-65a9eb54d3ab" + assert response.json["project_uuid"] == "61d61bdd-aa7d-4912-817f-65a9eb54d3ab" #FIXME -def test_vpcs_nio_create_udp(server): - vm = server.post("/vpcs", {"name": "PC TEST 1"}) - response = server.post("/vpcs/{}/ports/0/nio".format(vm.json["uuid"]), {"type": "nio_udp", +def test_vpcs_nio_create_udp(server, vm): + response = server.post("/vpcs/{}/ports/0/nio".format(vm["uuid"]), {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"}, @@ -43,9 +51,8 @@ def test_vpcs_nio_create_udp(server): @patch("gns3server.modules.vpcs.vpcs_device.has_privileged_access", return_value=True) -def test_vpcs_nio_create_tap(mock, server): - vm = server.post("/vpcs", {"name": "PC TEST 1"}) - response = server.post("/vpcs/{}/ports/0/nio".format(vm.json["uuid"]), {"type": "nio_tap", +def test_vpcs_nio_create_tap(mock, server, vm): + response = server.post("/vpcs/{}/ports/0/nio".format(vm["uuid"]), {"type": "nio_tap", "tap_device": "test"}) assert response.status == 200 assert response.route == "/vpcs/{uuid}/ports/{port_id}/nio" @@ -53,12 +60,11 @@ def test_vpcs_nio_create_tap(mock, server): #FIXME -def test_vpcs_delete_nio(server): - vm = server.post("/vpcs", {"name": "PC TEST 1"}) - response = server.post("/vpcs/{}/ports/0/nio".format(vm.json["uuid"]), {"type": "nio_udp", +def test_vpcs_delete_nio(server, vm): + response = server.post("/vpcs/{}/ports/0/nio".format(vm["uuid"]), {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"}) - response = server.delete("/vpcs/{}/ports/0/nio".format(vm.json["uuid"]), example=True) + response = server.delete("/vpcs/{}/ports/0/nio".format(vm["uuid"]), example=True) assert response.status == 200 assert response.route == "/vpcs/{uuid}/ports/{port_id}/nio" diff --git a/tests/modules/vpcs/test_vpcs_device.py b/tests/modules/vpcs/test_vpcs_device.py index 1b32e2ca..08a2b04d 100644 --- a/tests/modules/vpcs/test_vpcs_device.py +++ b/tests/modules/vpcs/test_vpcs_device.py @@ -19,8 +19,8 @@ import pytest import asyncio from tests.utils import asyncio_patch -#Move loop to util -from tests.api.base import loop +#TODO: Move loop to util +from tests.api.base import loop, project from asyncio.subprocess import Process from unittest.mock import patch, MagicMock from gns3server.modules.vpcs.vpcs_device import VPCSDevice @@ -28,44 +28,46 @@ from gns3server.modules.vpcs.vpcs_error import VPCSError from gns3server.modules.vpcs import VPCS from gns3server.modules.port_manager import PortManager + @pytest.fixture(scope="module") def manager(): m = VPCS.instance() m.port_manager = PortManager("127.0.0.1", False) return m + @patch("subprocess.check_output", return_value="Welcome to Virtual PC Simulator, version 0.6".encode("utf-8")) -def test_vm(tmpdir, manager): - vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", manager, working_dir=str(tmpdir)) +def test_vm(manager): + vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) assert vm.name == "test" assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0f" @patch("subprocess.check_output", return_value="Welcome to Virtual PC Simulator, version 0.1".encode("utf-8")) -def test_vm_invalid_vpcs_version(tmpdir, manager): +def test_vm_invalid_vpcs_version(project, manager): with pytest.raises(VPCSError): - vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", manager, working_dir=str(tmpdir)) + vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) assert vm.name == "test" assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0f" @patch("gns3server.config.Config.get_section_config", return_value = {"path": "/bin/test_fake"}) -def test_vm_invalid_vpcs_path(tmpdir, manager): +def test_vm_invalid_vpcs_path(project, manager): with pytest.raises(VPCSError): - vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", manager, working_dir=str(tmpdir)) + vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) assert vm.name == "test" assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0f" -def test_start(tmpdir, loop, manager): +def test_start(project, loop, manager): with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): - vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", manager, working_dir=str(tmpdir)) + vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) loop.run_until_complete(asyncio.async(vm.start())) assert vm.is_running() == True -def test_stop(tmpdir, loop, manager): +def test_stop(project, loop, manager): process = MagicMock() with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): - vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", manager, working_dir=str(tmpdir)) + vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) loop.run_until_complete(asyncio.async(vm.start())) @@ -74,26 +76,26 @@ def test_stop(tmpdir, loop, manager): assert vm.is_running() == False process.terminate.assert_called_with() -def test_add_nio_binding_udp(tmpdir, manager): - vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", manager, working_dir=str(tmpdir)) +def test_add_nio_binding_udp(manager): + vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) assert nio.lport == 4242 -def test_add_nio_binding_tap(tmpdir, manager): - vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", manager, working_dir=str(tmpdir)) +def test_add_nio_binding_tap(project, manager): + vm = VPCSDevice("test", 42, project, manager) with patch("gns3server.modules.vpcs.vpcs_device.has_privileged_access", return_value=True): nio = vm.port_add_nio_binding(0, {"type": "nio_tap", "tap_device": "test"}) assert nio.tap_device == "test" -def test_add_nio_binding_tap_no_privileged_access(tmpdir, manager): - vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", manager, working_dir=str(tmpdir)) +def test_add_nio_binding_tap_no_privileged_access(manager): + vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) with patch("gns3server.modules.vpcs.vpcs_device.has_privileged_access", return_value=False): with pytest.raises(VPCSError): vm.port_add_nio_binding(0, {"type": "nio_tap", "tap_device": "test"}) assert vm._ethernet_adapter.ports[0] is None -def test_port_remove_nio_binding(tmpdir, manager): - vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", manager, working_dir=str(tmpdir)) +def test_port_remove_nio_binding(manager): + vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) vm.port_remove_nio_binding(0) assert vm._ethernet_adapter.ports[0] is None From 68d0e5f42d8dfd7ea8b7c5842be24e11fa588e8c Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 20 Jan 2015 13:04:20 +0100 Subject: [PATCH 049/485] PEP8 --- gns3server/handlers/file_upload_handler.py | 2 +- gns3server/handlers/project_handler.py | 4 ++-- gns3server/handlers/vpcs_handler.py | 4 ++-- gns3server/modules/__init__.py | 5 ----- gns3server/modules/base_manager.py | 4 ++-- gns3server/modules/base_vm.py | 3 +-- gns3server/modules/vpcs/vpcs_device.py | 15 +++++++-------- gns3server/schemas/project.py | 1 - gns3server/schemas/virtualbox.py | 1 - gns3server/schemas/vpcs.py | 1 - gns3server/web/response.py | 1 + tests/api/base.py | 7 ++++--- tests/api/test_project.py | 2 ++ tests/api/test_vpcs.py | 16 +++++++--------- tests/conftest.py | 19 ------------------- tests/modules/test_project.py | 8 ++++++-- tests/modules/test_project_manager.py | 2 +- tests/modules/vpcs/test_vpcs_device.py | 18 +++++++++++++----- 18 files changed, 49 insertions(+), 64 deletions(-) delete mode 100644 tests/conftest.py diff --git a/gns3server/handlers/file_upload_handler.py b/gns3server/handlers/file_upload_handler.py index 78e45844..d4e33200 100644 --- a/gns3server/handlers/file_upload_handler.py +++ b/gns3server/handlers/file_upload_handler.py @@ -19,7 +19,7 @@ Simple file upload & listing handler. """ -#TODO: file upload with aiohttp +# TODO: file upload with aiohttp import os diff --git a/gns3server/handlers/project_handler.py b/gns3server/handlers/project_handler.py index 77a4b835..a3a7bf35 100644 --- a/gns3server/handlers/project_handler.py +++ b/gns3server/handlers/project_handler.py @@ -31,7 +31,7 @@ class ProjectHandler: def create_project(request, response): pm = ProjectManager.instance() p = pm.create_project( - location = request.json.get("location"), - uuid = request.json.get("uuid") + location=request.json.get("location"), + uuid=request.json.get("uuid") ) response.json(p) diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index 8c729123..e18d40fa 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -40,11 +40,11 @@ class VPCSHandler: def create(request, response): vpcs = VPCS.instance() - vm = yield from vpcs.create_vm(request.json["name"], request.json["project_uuid"], uuid = request.json.get("uuid")) + vm = yield from vpcs.create_vm(request.json["name"], request.json["project_uuid"], uuid=request.json.get("uuid")) response.json({"name": vm.name, "uuid": vm.uuid, "console": vm.console, - "project_uuid": vm.project.uuid}) + "project_uuid": vm.project.uuid}) @classmethod @Route.post( diff --git a/gns3server/modules/__init__.py b/gns3server/modules/__init__.py index 43b95792..5127dd83 100644 --- a/gns3server/modules/__init__.py +++ b/gns3server/modules/__init__.py @@ -20,8 +20,3 @@ from .vpcs import VPCS from .virtualbox import VirtualBox MODULES = [VPCS, VirtualBox] - -#if sys.platform.startswith("linux"): -# # IOU runs only on Linux -# from .iou import IOU -# MODULES.append(IOU) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 926a8578..3f211c98 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -96,9 +96,9 @@ class BaseManager: """ project = ProjectManager.instance().get_project(project_identifier) - #TODO: support for old projects VM with normal IDs. + # TODO: support for old projects VM with normal IDs. - #TODO: supports specific args: pass kwargs to VM_CLASS? + # TODO: supports specific args: pass kwargs to VM_CLASS? if not uuid: uuid = str(uuid4()) diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index e075ec3e..ee232526 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -31,7 +31,7 @@ class BaseVM: self._manager = manager self._config = Config.instance() - #TODO: When delete release console ports + # TODO: When delete release console ports @property def project(self): @@ -98,4 +98,3 @@ class BaseVM: """ raise NotImplementedError - diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py index 0b720176..44b09682 100644 --- a/gns3server/modules/vpcs/vpcs_device.py +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -61,7 +61,7 @@ class VPCSDevice(BaseVM): self._console = console - #TODO: remove working_dir + # TODO: remove working_dir self._working_dir = "/tmp" self._command = [] @@ -120,7 +120,7 @@ class VPCSDevice(BaseVM): return self._console - #FIXME: correct way to subclass a property? + # FIXME: correct way to subclass a property? @BaseVM.name.setter def name(self, new_name): """ @@ -151,7 +151,7 @@ class VPCSDevice(BaseVM): """ Checks if the VPCS executable version is >= 0.5b1. """ - #TODO: should be async + # TODO: should be async try: output = subprocess.check_output([self._path, "-v"], cwd=self._working_dir) match = re.search("Welcome to Virtual PC Simulator, version ([0-9a-z\.]+)", output.decode("utf-8")) @@ -219,7 +219,7 @@ class VPCSDevice(BaseVM): Reads the standard output of the VPCS process. Only use when the process has been stopped or has crashed. """ - #TODO: should be async + # TODO: should be async output = "" if self._vpcs_stdout_file: try: @@ -258,7 +258,7 @@ class VPCSDevice(BaseVM): rhost = nio_settings["rhost"] rport = nio_settings["rport"] try: - #TODO: handle IPv6 + # TODO: handle IPv6 with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: sock.connect((rhost, rport)) except OSError as e: @@ -272,7 +272,6 @@ class VPCSDevice(BaseVM): if not nio: raise VPCSError("Requested NIO does not exist or is not supported: {}".format(nio_settings["type"])) - self._ethernet_adapter.add_nio(port_id, nio) log.info("VPCS {name} {uuid}]: {nio} added to port {port_id}".format(name=self._name, uuid=self.uuid, @@ -355,8 +354,8 @@ class VPCSDevice(BaseVM): command.extend(["-e"]) command.extend(["-d", nio.tap_device]) - #FIXME: find workaround - #command.extend(["-m", str(self._id)]) # the unique ID is used to set the MAC address offset + # FIXME: find workaround + # command.extend(["-m", str(self._id)]) # the unique ID is used to set the MAC address offset command.extend(["-i", "1"]) # option to start only one VPC instance command.extend(["-F"]) # option to avoid the daemonization of VPCS if self._script_file: diff --git a/gns3server/schemas/project.py b/gns3server/schemas/project.py index bf1e07b2..1e363ec0 100644 --- a/gns3server/schemas/project.py +++ b/gns3server/schemas/project.py @@ -36,4 +36,3 @@ PROJECT_OBJECT_SCHEMA = { }, "additionalProperties": False } - diff --git a/gns3server/schemas/virtualbox.py b/gns3server/schemas/virtualbox.py index 1dea7163..d7e9e4be 100644 --- a/gns3server/schemas/virtualbox.py +++ b/gns3server/schemas/virtualbox.py @@ -78,4 +78,3 @@ VBOX_OBJECT_SCHEMA = { "additionalProperties": False, "required": ["name", "uuid"] } - diff --git a/gns3server/schemas/vpcs.py b/gns3server/schemas/vpcs.py index d2486ce4..8a2834ae 100644 --- a/gns3server/schemas/vpcs.py +++ b/gns3server/schemas/vpcs.py @@ -148,4 +148,3 @@ VPCS_OBJECT_SCHEMA = { "additionalProperties": False, "required": ["name", "uuid", "console", "project_uuid"] } - diff --git a/gns3server/web/response.py b/gns3server/web/response.py index d829e725..e5cd6912 100644 --- a/gns3server/web/response.py +++ b/gns3server/web/response.py @@ -22,6 +22,7 @@ import logging log = logging.getLogger(__name__) + class Response(aiohttp.web.Response): def __init__(self, route=None, output_schema=None, headers={}, **kwargs): diff --git a/tests/api/base.py b/tests/api/base.py index 5f5981d1..85d465e9 100644 --- a/tests/api/base.py +++ b/tests/api/base.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . + """Base code use for all API tests""" import json @@ -27,7 +28,7 @@ import aiohttp from gns3server.web.route import Route -#TODO: get rid of * +# TODO: get rid of * from gns3server.handlers import * from gns3server.modules import MODULES from gns3server.modules.port_manager import PortManager @@ -143,6 +144,7 @@ def loop(request): request.addfinalizer(tear_down) return loop + @pytest.fixture(scope="session") def server(request, loop): port = _get_unused_port() @@ -167,5 +169,4 @@ def server(request, loop): @pytest.fixture(scope="module") def project(): - return ProjectManager.instance().create_project(uuid = "a1e920ca-338a-4e9f-b363-aa607b09dd80") - + return ProjectManager.instance().create_project(uuid="a1e920ca-338a-4e9f-b363-aa607b09dd80") diff --git a/tests/api/test_project.py b/tests/api/test_project.py index 69067f29..13aeeabf 100644 --- a/tests/api/test_project.py +++ b/tests/api/test_project.py @@ -30,12 +30,14 @@ def test_create_project_with_dir(server, tmpdir): assert response.status == 200 assert response.json['location'] == str(tmpdir) + def test_create_project_without_dir(server): query = {} response = server.post('/project', query) assert response.status == 200 assert response.json['uuid'] is not None + def test_create_project_with_uuid(server): query = {'uuid': '00010203-0405-0607-0809-0a0b0c0d0e0f'} response = server.post('/project', query) diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index 3e23071e..04d01c63 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -38,12 +38,11 @@ def test_vpcs_create(server, project): assert response.json["project_uuid"] == "61d61bdd-aa7d-4912-817f-65a9eb54d3ab" -#FIXME def test_vpcs_nio_create_udp(server, vm): response = server.post("/vpcs/{}/ports/0/nio".format(vm["uuid"]), {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"}, + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}, example=True) assert response.status == 200 assert response.route == "/vpcs/{uuid}/ports/{port_id}/nio" @@ -53,18 +52,17 @@ def test_vpcs_nio_create_udp(server, vm): @patch("gns3server.modules.vpcs.vpcs_device.has_privileged_access", return_value=True) def test_vpcs_nio_create_tap(mock, server, vm): response = server.post("/vpcs/{}/ports/0/nio".format(vm["uuid"]), {"type": "nio_tap", - "tap_device": "test"}) + "tap_device": "test"}) assert response.status == 200 assert response.route == "/vpcs/{uuid}/ports/{port_id}/nio" assert response.json["type"] == "nio_tap" -#FIXME def test_vpcs_delete_nio(server, vm): response = server.post("/vpcs/{}/ports/0/nio".format(vm["uuid"]), {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"}) + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}) response = server.delete("/vpcs/{}/ports/0/nio".format(vm["uuid"]), example=True) assert response.status == 200 assert response.route == "/vpcs/{uuid}/ports/{port_id}/nio" diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 3c62baca..00000000 --- a/tests/conftest.py +++ /dev/null @@ -1,19 +0,0 @@ -import sys -import os -import pytest -import subprocess -import time - - -@pytest.fixture(scope="session", autouse=True) -def server(request): - """ - Starts GNS3 server for all the tests. - """ - - cwd = os.path.dirname(os.path.abspath(__file__)) - server_script = os.path.join(cwd, "../gns3server/main.py") - process = subprocess.Popen([sys.executable, server_script, "--port=8000"]) - #time.sleep(1) # give some time for the process to start - request.addfinalizer(process.terminate) - return process diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py index 2d6ea0b3..8ecbee42 100644 --- a/tests/modules/test_project.py +++ b/tests/modules/test_project.py @@ -16,8 +16,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from gns3server.modules.project import Project import os +from gns3server.modules.project import Project + def test_affect_uuid(): p = Project() @@ -26,16 +27,19 @@ def test_affect_uuid(): p = Project(uuid='00010203-0405-0607-0809-0a0b0c0d0e0f') assert p.uuid == '00010203-0405-0607-0809-0a0b0c0d0e0f' + def test_path(tmpdir): - p = Project(location = str(tmpdir)) + p = Project(location=str(tmpdir)) assert p.path == os.path.join(str(tmpdir), p.uuid) assert os.path.exists(os.path.join(str(tmpdir), p.uuid)) assert os.path.exists(os.path.join(str(tmpdir), p.uuid, 'files')) + def test_temporary_path(): p = Project() assert os.path.exists(p.path) + def test_json(tmpdir): p = Project() assert p.__json__() == {"location": p.location, "uuid": p.uuid} diff --git a/tests/modules/test_project_manager.py b/tests/modules/test_project_manager.py index 64009928..9276c6a2 100644 --- a/tests/modules/test_project_manager.py +++ b/tests/modules/test_project_manager.py @@ -25,8 +25,8 @@ def test_create_project(): project = pm.create_project(uuid='00010203-0405-0607-0809-0a0b0c0d0e0f') assert project == pm.get_project('00010203-0405-0607-0809-0a0b0c0d0e0f') + def test_project_not_found(): pm = ProjectManager.instance() with pytest.raises(aiohttp.web.HTTPNotFound): pm.get_project('00010203-0405-0607-0809-000000000000') - diff --git a/tests/modules/vpcs/test_vpcs_device.py b/tests/modules/vpcs/test_vpcs_device.py index 08a2b04d..2fe1aa26 100644 --- a/tests/modules/vpcs/test_vpcs_device.py +++ b/tests/modules/vpcs/test_vpcs_device.py @@ -19,7 +19,7 @@ import pytest import asyncio from tests.utils import asyncio_patch -#TODO: Move loop to util +# TODO: Move loop to util from tests.api.base import loop, project from asyncio.subprocess import Process from unittest.mock import patch, MagicMock @@ -42,6 +42,7 @@ def test_vm(manager): assert vm.name == "test" assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0f" + @patch("subprocess.check_output", return_value="Welcome to Virtual PC Simulator, version 0.1".encode("utf-8")) def test_vm_invalid_vpcs_version(project, manager): with pytest.raises(VPCSError): @@ -49,20 +50,23 @@ def test_vm_invalid_vpcs_version(project, manager): assert vm.name == "test" assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0f" -@patch("gns3server.config.Config.get_section_config", return_value = {"path": "/bin/test_fake"}) + +@patch("gns3server.config.Config.get_section_config", return_value={"path": "/bin/test_fake"}) def test_vm_invalid_vpcs_path(project, manager): with pytest.raises(VPCSError): vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) assert vm.name == "test" assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0f" + def test_start(project, loop, manager): with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) loop.run_until_complete(asyncio.async(vm.start())) - assert vm.is_running() == True + assert vm.is_running() + def test_stop(project, loop, manager): process = MagicMock() @@ -71,22 +75,25 @@ def test_stop(project, loop, manager): nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) loop.run_until_complete(asyncio.async(vm.start())) - assert vm.is_running() == True + assert vm.is_running() loop.run_until_complete(asyncio.async(vm.stop())) - assert vm.is_running() == False + assert vm.is_running() is False process.terminate.assert_called_with() + def test_add_nio_binding_udp(manager): vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) assert nio.lport == 4242 + def test_add_nio_binding_tap(project, manager): vm = VPCSDevice("test", 42, project, manager) with patch("gns3server.modules.vpcs.vpcs_device.has_privileged_access", return_value=True): nio = vm.port_add_nio_binding(0, {"type": "nio_tap", "tap_device": "test"}) assert nio.tap_device == "test" + def test_add_nio_binding_tap_no_privileged_access(manager): vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) with patch("gns3server.modules.vpcs.vpcs_device.has_privileged_access", return_value=False): @@ -94,6 +101,7 @@ def test_add_nio_binding_tap_no_privileged_access(manager): vm.port_add_nio_binding(0, {"type": "nio_tap", "tap_device": "test"}) assert vm._ethernet_adapter.ports[0] is None + def test_port_remove_nio_binding(manager): vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) From 7f185663d1942febbb8505631f624a69b6609055 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 20 Jan 2015 13:12:26 +0100 Subject: [PATCH 050/485] VPCS Device => VPCS VM --- docs/api/examples/post_project.txt | 8 +- gns3server/modules/old_vpcs/__init__.py | 652 ------------------ .../modules/old_vpcs/adapters/__init__.py | 0 .../modules/old_vpcs/adapters/adapter.py | 104 --- .../old_vpcs/adapters/ethernet_adapter.py | 31 - gns3server/modules/old_vpcs/nios/__init__.py | 0 gns3server/modules/old_vpcs/nios/nio_tap.py | 46 -- gns3server/modules/old_vpcs/nios/nio_udp.py | 72 -- gns3server/modules/old_vpcs/schemas.py | 347 ---------- gns3server/modules/old_vpcs/vpcs_device.py | 557 --------------- gns3server/modules/old_vpcs/vpcs_error.py | 39 -- gns3server/modules/vpcs/__init__.py | 4 +- .../vpcs/{vpcs_device.py => vpcs_vm.py} | 24 +- tests/api/test_vpcs.py | 2 +- .../{test_vpcs_device.py => test_vpcs_vm.py} | 24 +- 15 files changed, 31 insertions(+), 1879 deletions(-) delete mode 100644 gns3server/modules/old_vpcs/__init__.py delete mode 100644 gns3server/modules/old_vpcs/adapters/__init__.py delete mode 100644 gns3server/modules/old_vpcs/adapters/adapter.py delete mode 100644 gns3server/modules/old_vpcs/adapters/ethernet_adapter.py delete mode 100644 gns3server/modules/old_vpcs/nios/__init__.py delete mode 100644 gns3server/modules/old_vpcs/nios/nio_tap.py delete mode 100644 gns3server/modules/old_vpcs/nios/nio_udp.py delete mode 100644 gns3server/modules/old_vpcs/schemas.py delete mode 100644 gns3server/modules/old_vpcs/vpcs_device.py delete mode 100644 gns3server/modules/old_vpcs/vpcs_error.py rename gns3server/modules/vpcs/{vpcs_device.py => vpcs_vm.py} (96%) rename tests/modules/vpcs/{test_vpcs_device.py => test_vpcs_vm.py} (80%) diff --git a/docs/api/examples/post_project.txt b/docs/api/examples/post_project.txt index 6d61c41f..3a99e4e6 100644 --- a/docs/api/examples/post_project.txt +++ b/docs/api/examples/post_project.txt @@ -1,8 +1,8 @@ -curl -i -X POST 'http://localhost:8000/project' -d '{"location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-301/test_create_project_with_dir0"}' +curl -i -X POST 'http://localhost:8000/project' -d '{"location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-308/test_create_project_with_dir0"}' POST /project HTTP/1.1 { - "location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-301/test_create_project_with_dir0" + "location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-308/test_create_project_with_dir0" } @@ -15,6 +15,6 @@ SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /project { - "location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-301/test_create_project_with_dir0", - "uuid": "d4d66c3f-439b-4ae9-972b-59040189c995" + "location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-308/test_create_project_with_dir0", + "uuid": "7b9efb50-4909-4dc2-bb61-0bf443874c4c" } diff --git a/gns3server/modules/old_vpcs/__init__.py b/gns3server/modules/old_vpcs/__init__.py deleted file mode 100644 index aa0f216e..00000000 --- a/gns3server/modules/old_vpcs/__init__.py +++ /dev/null @@ -1,652 +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 . - -""" -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/old_vpcs/adapters/__init__.py b/gns3server/modules/old_vpcs/adapters/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/gns3server/modules/old_vpcs/adapters/adapter.py b/gns3server/modules/old_vpcs/adapters/adapter.py deleted file mode 100644 index cf439427..00000000 --- a/gns3server/modules/old_vpcs/adapters/adapter.py +++ /dev/null @@ -1,104 +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 . - - -class Adapter(object): - """ - Base class for adapters. - - :param interfaces: number of interfaces supported by this adapter. - """ - - def __init__(self, interfaces=1): - - self._interfaces = interfaces - - self._ports = {} - for port_id in range(0, interfaces): - self._ports[port_id] = None - - def removable(self): - """ - Returns True if the adapter can be removed from a slot - and False if not. - - :returns: boolean - """ - - return True - - def port_exists(self, port_id): - """ - Checks if a port exists on this adapter. - - :returns: True is the port exists, - False otherwise. - """ - - if port_id in self._ports: - return True - return False - - def add_nio(self, port_id, nio): - """ - Adds a NIO to a port on this adapter. - - :param port_id: port ID (integer) - :param nio: NIO instance - """ - - self._ports[port_id] = nio - - def remove_nio(self, port_id): - """ - Removes a NIO from a port on this adapter. - - :param port_id: port ID (integer) - """ - - self._ports[port_id] = None - - def get_nio(self, port_id): - """ - Returns the NIO assigned to a port. - - :params port_id: port ID (integer) - - :returns: NIO instance - """ - - return self._ports[port_id] - - @property - def ports(self): - """ - Returns port to NIO mapping - - :returns: dictionary port -> NIO - """ - - return self._ports - - @property - def interfaces(self): - """ - Returns the number of interfaces supported by this adapter. - - :returns: number of interfaces - """ - - return self._interfaces diff --git a/gns3server/modules/old_vpcs/adapters/ethernet_adapter.py b/gns3server/modules/old_vpcs/adapters/ethernet_adapter.py deleted file mode 100644 index bbca7f40..00000000 --- a/gns3server/modules/old_vpcs/adapters/ethernet_adapter.py +++ /dev/null @@ -1,31 +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 . - -from .adapter import Adapter - - -class EthernetAdapter(Adapter): - """ - VPCS Ethernet adapter. - """ - - def __init__(self): - Adapter.__init__(self, interfaces=1) - - def __str__(self): - - return "VPCS Ethernet adapter" diff --git a/gns3server/modules/old_vpcs/nios/__init__.py b/gns3server/modules/old_vpcs/nios/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/gns3server/modules/old_vpcs/nios/nio_tap.py b/gns3server/modules/old_vpcs/nios/nio_tap.py deleted file mode 100644 index 4c3ed6b2..00000000 --- a/gns3server/modules/old_vpcs/nios/nio_tap.py +++ /dev/null @@ -1,46 +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 . - -""" -Interface for TAP NIOs (UNIX based OSes only). -""" - - -class NIO_TAP(object): - """ - TAP NIO. - - :param tap_device: TAP device name (e.g. tap0) - """ - - def __init__(self, tap_device): - - self._tap_device = tap_device - - @property - def tap_device(self): - """ - Returns the TAP device used by this NIO. - - :returns: the TAP device name - """ - - return self._tap_device - - def __str__(self): - - return "NIO TAP" diff --git a/gns3server/modules/old_vpcs/nios/nio_udp.py b/gns3server/modules/old_vpcs/nios/nio_udp.py deleted file mode 100644 index 0527f675..00000000 --- a/gns3server/modules/old_vpcs/nios/nio_udp.py +++ /dev/null @@ -1,72 +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 . - -""" -Interface for UDP NIOs. -""" - - -class NIO_UDP(object): - """ - UDP NIO. - - :param lport: local port number - :param rhost: remote address/host - :param rport: remote port number - """ - - _instance_count = 0 - - def __init__(self, lport, rhost, rport): - - self._lport = lport - self._rhost = rhost - self._rport = rport - - @property - def lport(self): - """ - Returns the local port - - :returns: local port number - """ - - return self._lport - - @property - def rhost(self): - """ - Returns the remote host - - :returns: remote address/host - """ - - return self._rhost - - @property - def rport(self): - """ - Returns the remote port - - :returns: remote port number - """ - - return self._rport - - def __str__(self): - - return "NIO UDP" diff --git a/gns3server/modules/old_vpcs/schemas.py b/gns3server/modules/old_vpcs/schemas.py deleted file mode 100644 index 6556895b..00000000 --- a/gns3server/modules/old_vpcs/schemas.py +++ /dev/null @@ -1,347 +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 . - - -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_DELETE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to delete a VPCS instance", - "type": "object", - "properties": { - "id": { - "description": "VPCS device instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -VPCS_UPDATE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to update a VPCS instance", - "type": "object", - "properties": { - "id": { - "description": "VPCS device instance ID", - "type": "integer" - }, - "name": { - "description": "VPCS device name", - "type": "string", - "minLength": 1, - }, - "console": { - "description": "console TCP port", - "minimum": 1, - "maximum": 65535, - "type": "integer" - }, - "script_file": { - "description": "Path to the VPCS script file file", - "type": "string", - "minLength": 1, - }, - "script_file_base64": { - "description": "Script file base64 encoded", - "type": "string" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -VPCS_START_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to start a VPCS instance", - "type": "object", - "properties": { - "id": { - "description": "VPCS device instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -VPCS_STOP_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to stop a VPCS instance", - "type": "object", - "properties": { - "id": { - "description": "VPCS device instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -VPCS_RELOAD_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to reload a VPCS instance", - "type": "object", - "properties": { - "id": { - "description": "VPCS device instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -VPCS_ALLOCATE_UDP_PORT_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to allocate an UDP port for a VPCS instance", - "type": "object", - "properties": { - "id": { - "description": "VPCS device instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the VPCS instance", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id", "port_id"] -} - -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_DELETE_NIO_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to delete a NIO for a VPCS instance", - "type": "object", - "properties": { - "id": { - "description": "VPCS device instance ID", - "type": "integer" - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 0, - "maximum": 0 - }, - }, - "additionalProperties": False, - "required": ["id", "port"] -} - -VPCS_EXPORT_CONFIG_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to export the script file of a VPCS instance", - "type": "object", - "properties": { - "id": { - "description": "VPCS device instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} diff --git a/gns3server/modules/old_vpcs/vpcs_device.py b/gns3server/modules/old_vpcs/vpcs_device.py deleted file mode 100644 index 65664f39..00000000 --- a/gns3server/modules/old_vpcs/vpcs_device.py +++ /dev/null @@ -1,557 +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 . - -""" -VPCS device management (creates command line, processes, files etc.) in -order to run an VPCS instance. -""" - -import os -import sys -import subprocess -import signal -import shutil -import re - -from pkg_resources import parse_version -from .vpcs_error import VPCSError -from .adapters.ethernet_adapter import EthernetAdapter -from .nios.nio_udp import NIO_UDP -from .nios.nio_tap import NIO_TAP -from ..attic import find_unused_port - -import logging -log = logging.getLogger(__name__) - - -class VPCSDevice(object): - """ - VPCS device implementation. - - :param name: name of this VPCS device - :param path: path to VPCS executable - :param working_dir: path to a working directory - :param vpcs_id: VPCS instance ID - :param console: TCP console port - :param console_host: IP address to bind for console connections - :param console_start_port_range: TCP console port range start - :param console_end_port_range: TCP console port range end - """ - - _instances = [] - _allocated_console_ports = [] - - def __init__(self, - name, - path, - working_dir, - vpcs_id=None, - console=None, - console_host="0.0.0.0", - console_start_port_range=4512, - console_end_port_range=5000): - - - if not vpcs_id: - # find an instance identifier is none is provided (1 <= id <= 255) - # This 255 limit is due to a restriction on the number of possible - # MAC addresses given in VPCS using the -m option - self._id = 0 - for identifier in range(1, 256): - if identifier not in self._instances: - self._id = identifier - self._instances.append(self._id) - break - - if self._id == 0: - raise VPCSError("Maximum number of VPCS instances reached") - else: - if vpcs_id in self._instances: - raise VPCSError("VPCS identifier {} is already used by another VPCS device".format(vpcs_id)) - self._id = vpcs_id - self._instances.append(self._id) - - self._name = name - self._path = path - self._console = console - self._working_dir = None - self._console_host = console_host - self._command = [] - self._process = None - self._vpcs_stdout_file = "" - self._started = False - self._console_start_port_range = console_start_port_range - self._console_end_port_range = console_end_port_range - - # VPCS settings - self._script_file = "" - self._ethernet_adapter = EthernetAdapter() # one adapter with 1 Ethernet interface - - working_dir_path = os.path.join(working_dir, "vpcs", "pc-{}".format(self._id)) - - if vpcs_id and not os.path.isdir(working_dir_path): - raise VPCSError("Working directory {} doesn't exist".format(working_dir_path)) - - # create the device own working directory - self.working_dir = working_dir_path - - if not self._console: - # allocate a console port - try: - self._console = find_unused_port(self._console_start_port_range, - self._console_end_port_range, - self._console_host, - ignore_ports=self._allocated_console_ports) - except Exception as e: - raise VPCSError(e) - - if self._console in self._allocated_console_ports: - raise VPCSError("Console port {} is already used by another VPCS device".format(console)) - self._allocated_console_ports.append(self._console) - - log.info("VPCS device {name} [id={id}] has been created".format(name=self._name, - id=self._id)) - - def defaults(self): - """ - Returns all the default attribute values for VPCS. - - :returns: default values (dictionary) - """ - - vpcs_defaults = {"name": self._name, - "script_file": self._script_file, - "console": self._console} - - return vpcs_defaults - - @property - def id(self): - """ - Returns the unique ID for this VPCS device. - - :returns: id (integer) - """ - - return self._id - - @classmethod - def reset(cls): - """ - Resets allocated instance list. - """ - - cls._instances.clear() - cls._allocated_console_ports.clear() - - @property - def name(self): - """ - Returns the name of this VPCS device. - - :returns: name - """ - - return self._name - - @name.setter - def name(self, new_name): - """ - Sets the name of this VPCS device. - - :param new_name: name - """ - - if self._script_file: - # update the startup.vpc - config_path = os.path.join(self._working_dir, "startup.vpc") - if os.path.isfile(config_path): - try: - with open(config_path, "r+", errors="replace") as f: - old_config = f.read() - new_config = old_config.replace(self._name, new_name) - f.seek(0) - f.write(new_config) - except OSError as e: - raise VPCSError("Could not amend the configuration {}: {}".format(config_path, e)) - - log.info("VPCS {name} [id={id}]: renamed to {new_name}".format(name=self._name, - id=self._id, - new_name=new_name)) - self._name = new_name - - @property - def path(self): - """ - Returns the path to the VPCS executable. - - :returns: path to VPCS - """ - - return self._path - - @path.setter - def path(self, path): - """ - Sets the path to the VPCS executable. - - :param path: path to VPCS - """ - - self._path = path - log.info("VPCS {name} [id={id}]: path changed to {path}".format(name=self._name, - id=self._id, - path=path)) - - @property - def working_dir(self): - """ - Returns current working directory - - :returns: path to the working directory - """ - - return self._working_dir - - @working_dir.setter - def working_dir(self, working_dir): - """ - Sets the working directory for VPCS. - - :param working_dir: path to the working directory - """ - - try: - os.makedirs(working_dir) - except FileExistsError: - pass - except OSError as e: - raise VPCSError("Could not create working directory {}: {}".format(working_dir, e)) - - self._working_dir = working_dir - log.info("VPCS {name} [id={id}]: working directory changed to {wd}".format(name=self._name, - id=self._id, - wd=self._working_dir)) - - @property - def console(self): - """ - Returns the TCP console port. - - :returns: console port (integer) - """ - - return self._console - - @console.setter - def console(self, console): - """ - Sets the TCP console port. - - :param console: console port (integer) - """ - - if console in self._allocated_console_ports: - raise VPCSError("Console port {} is already used by another VPCS device".format(console)) - - self._allocated_console_ports.remove(self._console) - self._console = console - self._allocated_console_ports.append(self._console) - log.info("VPCS {name} [id={id}]: console port set to {port}".format(name=self._name, - id=self._id, - port=console)) - - def command(self): - """ - Returns the VPCS command line. - - :returns: VPCS command line (string) - """ - - return " ".join(self._build_command()) - - def delete(self): - """ - Deletes this VPCS device. - """ - - self.stop() - if self._id in self._instances: - self._instances.remove(self._id) - - if self.console and self.console in self._allocated_console_ports: - self._allocated_console_ports.remove(self.console) - - log.info("VPCS device {name} [id={id}] has been deleted".format(name=self._name, - id=self._id)) - - def clean_delete(self): - """ - Deletes this VPCS device & all files (configs, logs etc.) - """ - - self.stop() - if self._id in self._instances: - self._instances.remove(self._id) - - if self.console: - self._allocated_console_ports.remove(self.console) - - try: - shutil.rmtree(self._working_dir) - except OSError as e: - log.error("could not delete VPCS device {name} [id={id}]: {error}".format(name=self._name, - id=self._id, - error=e)) - return - - log.info("VPCS device {name} [id={id}] has been deleted (including associated files)".format(name=self._name, - id=self._id)) - - @property - def started(self): - """ - Returns either this VPCS device has been started or not. - - :returns: boolean - """ - - return self._started - - def _check_vpcs_version(self): - """ - Checks if the VPCS executable version is >= 0.5b1. - """ - - try: - output = subprocess.check_output([self._path, "-v"], cwd=self._working_dir) - match = re.search("Welcome to Virtual PC Simulator, version ([0-9a-z\.]+)", output.decode("utf-8")) - if match: - version = match.group(1) - if parse_version(version) < parse_version("0.5b1"): - raise VPCSError("VPCS executable version must be >= 0.5b1") - else: - raise VPCSError("Could not determine the VPCS version for {}".format(self._path)) - except (OSError, subprocess.SubprocessError) as e: - raise VPCSError("Error while looking for the VPCS version: {}".format(e)) - - def start(self): - """ - Starts the VPCS process. - """ - - if not self.is_running(): - - if not self._path: - raise VPCSError("No path to a VPCS executable has been set") - - if not os.path.isfile(self._path): - raise VPCSError("VPCS program '{}' is not accessible".format(self._path)) - - if not os.access(self._path, os.X_OK): - raise VPCSError("VPCS program '{}' is not executable".format(self._path)) - - self._check_vpcs_version() - - if not self._ethernet_adapter.get_nio(0): - raise VPCSError("This VPCS instance must be connected in order to start") - - self._command = self._build_command() - try: - log.info("starting VPCS: {}".format(self._command)) - self._vpcs_stdout_file = os.path.join(self._working_dir, "vpcs.log") - log.info("logging to {}".format(self._vpcs_stdout_file)) - flags = 0 - if sys.platform.startswith("win32"): - flags = subprocess.CREATE_NEW_PROCESS_GROUP - with open(self._vpcs_stdout_file, "w") as fd: - self._process = subprocess.Popen(self._command, - stdout=fd, - stderr=subprocess.STDOUT, - cwd=self._working_dir, - creationflags=flags) - log.info("VPCS instance {} started PID={}".format(self._id, self._process.pid)) - self._started = True - except (OSError, subprocess.SubprocessError) as e: - vpcs_stdout = self.read_vpcs_stdout() - log.error("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout)) - raise VPCSError("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout)) - - def stop(self): - """ - Stops the VPCS process. - """ - - # stop the VPCS process - if self.is_running(): - log.info("stopping VPCS instance {} PID={}".format(self._id, self._process.pid)) - if sys.platform.startswith("win32"): - self._process.send_signal(signal.CTRL_BREAK_EVENT) - else: - self._process.terminate() - - self._process.wait() - - self._process = None - self._started = False - - def read_vpcs_stdout(self): - """ - Reads the standard output of the VPCS process. - Only use when the process has been stopped or has crashed. - """ - - output = "" - if self._vpcs_stdout_file: - try: - with open(self._vpcs_stdout_file, errors="replace") as file: - output = file.read() - except OSError as e: - log.warn("could not read {}: {}".format(self._vpcs_stdout_file, e)) - return output - - def is_running(self): - """ - Checks if the VPCS process is running - - :returns: True or False - """ - - if self._process and self._process.poll() is None: - return True - return False - - def port_add_nio_binding(self, port_id, nio): - """ - Adds a port NIO binding. - - :param port_id: port ID - :param nio: NIO instance to add to the slot/port - """ - - if not self._ethernet_adapter.port_exists(port_id): - raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter, - port_id=port_id)) - - self._ethernet_adapter.add_nio(port_id, nio) - log.info("VPCS {name} [id={id}]: {nio} added to port {port_id}".format(name=self._name, - id=self._id, - nio=nio, - port_id=port_id)) - - def port_remove_nio_binding(self, port_id): - """ - Removes a port NIO binding. - - :param port_id: port ID - - :returns: NIO instance - """ - - if not self._ethernet_adapter.port_exists(port_id): - raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter, - port_id=port_id)) - - nio = self._ethernet_adapter.get_nio(port_id) - self._ethernet_adapter.remove_nio(port_id) - log.info("VPCS {name} [id={id}]: {nio} removed from port {port_id}".format(name=self._name, - id=self._id, - nio=nio, - port_id=port_id)) - return nio - - def _build_command(self): - """ - Command to start the VPCS process. - (to be passed to subprocess.Popen()) - - VPCS command line: - usage: vpcs [options] [scriptfile] - Option: - -h print this help then exit - -v print version information then exit - - -i num number of vpc instances to start (default is 9) - -p port run as a daemon listening on the tcp 'port' - -m num start byte of ether address, default from 0 - -r file load and execute script file - compatible with older versions, DEPRECATED. - - -e tap mode, using /dev/tapx by default (linux only) - -u udp mode, default - - udp mode options: - -s port local udp base port, default from 20000 - -c port remote udp base port (dynamips udp port), default from 30000 - -t ip remote host IP, default 127.0.0.1 - - tap mode options: - -d device device name, works only when -i is set to 1 - - hypervisor mode option: - -H port run as the hypervisor listening on the tcp 'port' - - If no 'scriptfile' specified, vpcs will read and execute the file named - 'startup.vpc' if it exsits in the current directory. - - """ - - command = [self._path] - command.extend(["-p", str(self._console)]) # listen to console port - - nio = self._ethernet_adapter.get_nio(0) - if nio: - if isinstance(nio, NIO_UDP): - # UDP tunnel - command.extend(["-s", str(nio.lport)]) # source UDP port - command.extend(["-c", str(nio.rport)]) # destination UDP port - command.extend(["-t", nio.rhost]) # destination host - - elif isinstance(nio, NIO_TAP): - # TAP interface - command.extend(["-e"]) - command.extend(["-d", nio.tap_device]) - - command.extend(["-m", str(self._id)]) # the unique ID is used to set the MAC address offset - command.extend(["-i", "1"]) # option to start only one VPC instance - command.extend(["-F"]) # option to avoid the daemonization of VPCS - if self._script_file: - command.extend([self._script_file]) - return command - - @property - def script_file(self): - """ - Returns the script-file for this VPCS instance. - - :returns: path to script-file - """ - - return self._script_file - - @script_file.setter - def script_file(self, script_file): - """ - Sets the script-file for this VPCS instance. - - :param script_file: path to base-script-file - """ - - self._script_file = script_file - log.info("VPCS {name} [id={id}]: script_file set to {config}".format(name=self._name, - id=self._id, - config=self._script_file)) diff --git a/gns3server/modules/old_vpcs/vpcs_error.py b/gns3server/modules/old_vpcs/vpcs_error.py deleted file mode 100644 index 167129ba..00000000 --- a/gns3server/modules/old_vpcs/vpcs_error.py +++ /dev/null @@ -1,39 +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 . - -""" -Custom exceptions for VPCS module. -""" - - -class VPCSError(Exception): - - def __init__(self, message, original_exception=None): - - Exception.__init__(self, message) - if isinstance(message, Exception): - message = str(message) - self._message = message - self._original_exception = original_exception - - def __repr__(self): - - return self._message - - def __str__(self): - - return self._message diff --git a/gns3server/modules/vpcs/__init__.py b/gns3server/modules/vpcs/__init__.py index 52191f2f..6ad5d8cc 100644 --- a/gns3server/modules/vpcs/__init__.py +++ b/gns3server/modules/vpcs/__init__.py @@ -20,8 +20,8 @@ VPCS server module. """ from ..base_manager import BaseManager -from .vpcs_device import VPCSDevice +from .vpcs_vm import VPCSVM class VPCS(BaseManager): - _VM_CLASS = VPCSDevice + _VM_CLASS = VPCSVM diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_vm.py similarity index 96% rename from gns3server/modules/vpcs/vpcs_device.py rename to gns3server/modules/vpcs/vpcs_vm.py index 44b09682..a1ffdd57 100644 --- a/gns3server/modules/vpcs/vpcs_device.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -16,7 +16,7 @@ # along with this program. If not, see . """ -VPCS device management (creates command line, processes, files etc.) in +VPCS vm management (creates command line, processes, files etc.) in order to run an VPCS instance. """ @@ -42,11 +42,11 @@ import logging log = logging.getLogger(__name__) -class VPCSDevice(BaseVM): +class VPCSVM(BaseVM): """ - VPCS device implementation. + VPCS vm implementation. - :param name: name of this VPCS device + :param name: name of this VPCS vm :param uuid: VPCS instance UUID :param project: Project instance :param manager: parent VM Manager @@ -78,7 +78,7 @@ class VPCSDevice(BaseVM): # if vpcs_id and not os.path.isdir(working_dir_path): # raise VPCSError("Working directory {} doesn't exist".format(working_dir_path)) # - # # create the device own working directory + # # create the vm own working directory # self.working_dir = working_dir_path # try: @@ -113,7 +113,7 @@ class VPCSDevice(BaseVM): @property def console(self): """ - Returns the console port of this VPCS device. + Returns the console port of this VPCS vm. :returns: console port """ @@ -124,7 +124,7 @@ class VPCSDevice(BaseVM): @BaseVM.name.setter def name(self, new_name): """ - Sets the name of this VPCS device. + Sets the name of this VPCS vm. :param new_name: name """ @@ -265,10 +265,10 @@ class VPCSDevice(BaseVM): raise VPCSError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e)) nio = NIO_UDP(lport, rhost, rport) elif nio_settings["type"] == "nio_tap": - tap_device = nio_settings["tap_device"] + tap_vm = nio_settings["tap_device"] if not has_privileged_access(self._path): - raise VPCSError("{} has no privileged access to {}.".format(self._path, tap_device)) - nio = NIO_TAP(tap_device) + raise VPCSError("{} has no privileged access to {}.".format(self._path, tap_vm)) + nio = NIO_TAP(tap_vm) if not nio: raise VPCSError("Requested NIO does not exist or is not supported: {}".format(nio_settings["type"])) @@ -326,7 +326,7 @@ class VPCSDevice(BaseVM): -t ip remote host IP, default 127.0.0.1 tap mode options: - -d device device name, works only when -i is set to 1 + -d vm device name, works only when -i is set to 1 hypervisor mode option: -H port run as the hypervisor listening on the tcp 'port' @@ -352,7 +352,7 @@ class VPCSDevice(BaseVM): elif isinstance(nio, NIO_TAP): # TAP interface command.extend(["-e"]) - command.extend(["-d", nio.tap_device]) + command.extend(["-d", nio.tap_vm]) # FIXME: find workaround # command.extend(["-m", str(self._id)]) # the unique ID is used to set the MAC address offset diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index 04d01c63..205e74a0 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -49,7 +49,7 @@ def test_vpcs_nio_create_udp(server, vm): assert response.json["type"] == "nio_udp" -@patch("gns3server.modules.vpcs.vpcs_device.has_privileged_access", return_value=True) +@patch("gns3server.modules.vpcs.vpcs_vm.has_privileged_access", return_value=True) def test_vpcs_nio_create_tap(mock, server, vm): response = server.post("/vpcs/{}/ports/0/nio".format(vm["uuid"]), {"type": "nio_tap", "tap_device": "test"}) diff --git a/tests/modules/vpcs/test_vpcs_device.py b/tests/modules/vpcs/test_vpcs_vm.py similarity index 80% rename from tests/modules/vpcs/test_vpcs_device.py rename to tests/modules/vpcs/test_vpcs_vm.py index 2fe1aa26..57cf71d8 100644 --- a/tests/modules/vpcs/test_vpcs_device.py +++ b/tests/modules/vpcs/test_vpcs_vm.py @@ -23,7 +23,7 @@ from tests.utils import asyncio_patch from tests.api.base import loop, project from asyncio.subprocess import Process from unittest.mock import patch, MagicMock -from gns3server.modules.vpcs.vpcs_device import VPCSDevice +from gns3server.modules.vpcs.vpcs_vm import VPCSVM from gns3server.modules.vpcs.vpcs_error import VPCSError from gns3server.modules.vpcs import VPCS from gns3server.modules.port_manager import PortManager @@ -38,7 +38,7 @@ def manager(): @patch("subprocess.check_output", return_value="Welcome to Virtual PC Simulator, version 0.6".encode("utf-8")) def test_vm(manager): - vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) assert vm.name == "test" assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0f" @@ -46,7 +46,7 @@ def test_vm(manager): @patch("subprocess.check_output", return_value="Welcome to Virtual PC Simulator, version 0.1".encode("utf-8")) def test_vm_invalid_vpcs_version(project, manager): with pytest.raises(VPCSError): - vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) assert vm.name == "test" assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0f" @@ -54,14 +54,14 @@ def test_vm_invalid_vpcs_version(project, manager): @patch("gns3server.config.Config.get_section_config", return_value={"path": "/bin/test_fake"}) def test_vm_invalid_vpcs_path(project, manager): with pytest.raises(VPCSError): - vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) assert vm.name == "test" assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0f" def test_start(project, loop, manager): with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): - vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) loop.run_until_complete(asyncio.async(vm.start())) @@ -71,7 +71,7 @@ def test_start(project, loop, manager): def test_stop(project, loop, manager): process = MagicMock() with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): - vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) loop.run_until_complete(asyncio.async(vm.start())) @@ -82,28 +82,28 @@ def test_stop(project, loop, manager): def test_add_nio_binding_udp(manager): - vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) assert nio.lport == 4242 def test_add_nio_binding_tap(project, manager): - vm = VPCSDevice("test", 42, project, manager) - with patch("gns3server.modules.vpcs.vpcs_device.has_privileged_access", return_value=True): + vm = VPCSVM("test", 42, project, manager) + with patch("gns3server.modules.vpcs.vpcs_vm.has_privileged_access", return_value=True): nio = vm.port_add_nio_binding(0, {"type": "nio_tap", "tap_device": "test"}) assert nio.tap_device == "test" def test_add_nio_binding_tap_no_privileged_access(manager): - vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) - with patch("gns3server.modules.vpcs.vpcs_device.has_privileged_access", return_value=False): + vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + with patch("gns3server.modules.vpcs.vpcs_vm.has_privileged_access", return_value=False): with pytest.raises(VPCSError): vm.port_add_nio_binding(0, {"type": "nio_tap", "tap_device": "test"}) assert vm._ethernet_adapter.ports[0] is None def test_port_remove_nio_binding(manager): - vm = VPCSDevice("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) vm.port_remove_nio_binding(0) assert vm._ethernet_adapter.ports[0] is None From f5ed9fbcf189ac0d7989d23a90840fd58bbaefff Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 20 Jan 2015 13:24:00 +0100 Subject: [PATCH 051/485] PEP 8 clean thanks to auto pep8 --- cloud-image/create_image.py | 2 +- docs/conf.py | 4 +- gns3dms/cloud/base_cloud_ctrl.py | 1 - gns3dms/cloud/exceptions.py | 22 +++++++ gns3dms/main.py | 66 +++++++++---------- gns3dms/modules/__init__.py | 2 +- gns3dms/modules/daemon.py | 22 ++++--- gns3dms/modules/rackspace_cloud.py | 7 +- gns3server/config.py | 3 +- gns3server/handlers/auth_handler.py | 33 ++++++---- gns3server/handlers/file_upload_handler.py | 1 + gns3server/handlers/project_handler.py | 1 + gns3server/handlers/virtualbox_handler.py | 1 + gns3server/handlers/vpcs_handler.py | 1 + gns3server/main.py | 8 +-- gns3server/modules/adapters/adapter.py | 1 + .../modules/adapters/ethernet_adapter.py | 1 + gns3server/modules/base_manager.py | 1 + gns3server/modules/dynamips/__init__.py | 7 +- .../modules/dynamips/adapters/adapter.py | 1 + .../modules/dynamips/adapters/c1700_mb_1fe.py | 1 + .../dynamips/adapters/c1700_mb_wic1.py | 1 + .../modules/dynamips/adapters/c2600_mb_1e.py | 1 + .../modules/dynamips/adapters/c2600_mb_1fe.py | 1 + .../modules/dynamips/adapters/c2600_mb_2e.py | 1 + .../modules/dynamips/adapters/c2600_mb_2fe.py | 1 + .../modules/dynamips/adapters/c7200_io_2fe.py | 1 + .../modules/dynamips/adapters/c7200_io_fe.py | 1 + .../dynamips/adapters/c7200_io_ge_e.py | 1 + .../modules/dynamips/adapters/leopard_2fe.py | 1 + .../modules/dynamips/adapters/nm_16esw.py | 1 + gns3server/modules/dynamips/adapters/nm_1e.py | 1 + .../modules/dynamips/adapters/nm_1fe_tx.py | 1 + gns3server/modules/dynamips/adapters/nm_4e.py | 1 + gns3server/modules/dynamips/adapters/nm_4t.py | 1 + .../modules/dynamips/adapters/pa_2fe_tx.py | 1 + gns3server/modules/dynamips/adapters/pa_4e.py | 1 + gns3server/modules/dynamips/adapters/pa_4t.py | 1 + gns3server/modules/dynamips/adapters/pa_8e.py | 1 + gns3server/modules/dynamips/adapters/pa_8t.py | 1 + gns3server/modules/dynamips/adapters/pa_a1.py | 1 + .../modules/dynamips/adapters/pa_fe_tx.py | 1 + gns3server/modules/dynamips/adapters/pa_ge.py | 1 + .../modules/dynamips/adapters/pa_pos_oc3.py | 1 + .../modules/dynamips/adapters/wic_1enet.py | 1 + .../modules/dynamips/adapters/wic_1t.py | 1 + .../modules/dynamips/adapters/wic_2t.py | 1 + gns3server/modules/dynamips/backends/vm.py | 6 +- .../modules/dynamips/dynamips_hypervisor.py | 1 + gns3server/modules/dynamips/hypervisor.py | 1 + .../modules/dynamips/hypervisor_manager.py | 1 + gns3server/modules/dynamips/nios/nio.py | 1 + gns3server/modules/dynamips/nios/nio_fifo.py | 1 + .../dynamips/nios/nio_generic_ethernet.py | 1 + .../dynamips/nios/nio_linux_ethernet.py | 1 + gns3server/modules/dynamips/nios/nio_mcast.py | 3 +- gns3server/modules/dynamips/nios/nio_null.py | 1 + gns3server/modules/dynamips/nios/nio_tap.py | 1 + gns3server/modules/dynamips/nios/nio_udp.py | 1 + .../modules/dynamips/nios/nio_udp_auto.py | 1 + gns3server/modules/dynamips/nios/nio_unix.py | 1 + gns3server/modules/dynamips/nios/nio_vde.py | 1 + .../modules/dynamips/nodes/atm_bridge.py | 3 +- .../modules/dynamips/nodes/atm_switch.py | 1 + gns3server/modules/dynamips/nodes/bridge.py | 1 + gns3server/modules/dynamips/nodes/c1700.py | 1 + gns3server/modules/dynamips/nodes/c2600.py | 1 + gns3server/modules/dynamips/nodes/c2691.py | 1 + gns3server/modules/dynamips/nodes/c3600.py | 1 + gns3server/modules/dynamips/nodes/c3725.py | 1 + gns3server/modules/dynamips/nodes/c3745.py | 1 + gns3server/modules/dynamips/nodes/c7200.py | 7 +- .../modules/dynamips/nodes/ethernet_switch.py | 7 +- .../dynamips/nodes/frame_relay_switch.py | 1 + gns3server/modules/dynamips/nodes/hub.py | 1 + gns3server/modules/dynamips/nodes/router.py | 3 +- gns3server/modules/dynamips/schemas/ethsw.py | 30 ++++----- gns3server/modules/dynamips/schemas/vm.py | 2 +- gns3server/modules/iou/__init__.py | 3 +- gns3server/modules/iou/adapters/adapter.py | 1 + .../modules/iou/adapters/ethernet_adapter.py | 1 + .../modules/iou/adapters/serial_adapter.py | 1 + gns3server/modules/iou/iou_device.py | 1 + gns3server/modules/iou/ioucon.py | 33 +++++----- gns3server/modules/iou/nios/nio.py | 1 + .../modules/iou/nios/nio_generic_ethernet.py | 1 + gns3server/modules/iou/nios/nio_tap.py | 1 + gns3server/modules/iou/nios/nio_udp.py | 1 + gns3server/modules/nios/nio_tap.py | 1 + gns3server/modules/nios/nio_udp.py | 1 + gns3server/modules/port_manager.py | 1 + gns3server/modules/project.py | 1 + gns3server/modules/project_manager.py | 1 + gns3server/modules/qemu/__init__.py | 3 +- gns3server/modules/qemu/adapters/adapter.py | 1 + .../modules/qemu/adapters/ethernet_adapter.py | 1 + gns3server/modules/qemu/nios/nio.py | 1 + gns3server/modules/qemu/nios/nio_udp.py | 1 + gns3server/modules/qemu/qemu_vm.py | 45 +++++++------ .../modules/virtualbox/telnet_server.py | 54 +++++++-------- .../modules/virtualbox/virtualbox_vm.py | 8 +-- gns3server/modules/vpcs/vpcs_vm.py | 2 + gns3server/server.py | 10 +-- gns3server/start_server.py | 14 ++-- gns3server/version.py | 1 - gns3server/web/documentation.py | 2 + gns3server/web/response.py | 1 + gns3server/web/route.py | 1 + old_tests/dynamips/test_c1700.py | 2 +- old_tests/dynamips/test_c2600.py | 2 +- old_tests/dynamips/test_c2691.py | 2 +- old_tests/dynamips/test_c3600.py | 2 +- old_tests/dynamips/test_c3725.py | 2 +- old_tests/dynamips/test_c3745.py | 2 +- old_tests/dynamips/test_c7200.py | 4 +- old_tests/dynamips/test_hypervisor_manager.py | 2 +- old_tests/dynamips/test_nios.py | 2 +- old_tests/dynamips/test_router.py | 6 +- old_tests/dynamips/test_vmhandler.py | 26 ++++---- old_tests/test_jsonrpc.py | 1 + scripts/ws_client.py | 2 +- setup.py | 2 +- tests/api/base.py | 1 + tests/utils.py | 2 + 124 files changed, 340 insertions(+), 210 deletions(-) diff --git a/cloud-image/create_image.py b/cloud-image/create_image.py index b7b1fec1..b1021f34 100644 --- a/cloud-image/create_image.py +++ b/cloud-image/create_image.py @@ -103,7 +103,7 @@ def main(): instance.change_password(passwd) # wait for the password change to be processed. Continuing while # a password change is processing will cause image creation to fail. - sleep(POLL_SEC*6) + sleep(POLL_SEC * 6) env.host_string = str(instance.accessIPv4) env.user = "root" diff --git a/docs/conf.py b/docs/conf.py index 73bef3c9..7fe6d119 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -103,9 +103,9 @@ pygments_style = 'sphinx' # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' -#html_theme = 'nature' +# html_theme = 'nature' -#If uncommented it's turn off the default read the doc style +# If uncommented it's turn off the default read the doc style html_style = "/default.css" # Theme options are theme-specific and customize the look and feel of a theme diff --git a/gns3dms/cloud/base_cloud_ctrl.py b/gns3dms/cloud/base_cloud_ctrl.py index 236cdccc..0ad74af1 100644 --- a/gns3dms/cloud/base_cloud_ctrl.py +++ b/gns3dms/cloud/base_cloud_ctrl.py @@ -175,7 +175,6 @@ class BaseCloudCtrl(object): except Exception as e: log.error("list_instances returned an error: {}".format(e)) - def create_key_pair(self, name): """ Create and return a new Key Pair. """ diff --git a/gns3dms/cloud/exceptions.py b/gns3dms/cloud/exceptions.py index beeb598d..65d65f9f 100644 --- a/gns3dms/cloud/exceptions.py +++ b/gns3dms/cloud/exceptions.py @@ -1,45 +1,67 @@ """ Exception classes for CloudCtrl classes. """ + class ApiError(Exception): + """ Raised when the server returns 500 Compute Error. """ pass + class BadRequest(Exception): + """ Raised when the server returns 400 Bad Request. """ pass + class ComputeFault(Exception): + """ Raised when the server returns 400|500 Compute Fault. """ pass + class Forbidden(Exception): + """ Raised when the server returns 403 Forbidden. """ pass + class ItemNotFound(Exception): + """ Raised when the server returns 404 Not Found. """ pass + class KeyPairExists(Exception): + """ Raised when the server returns 409 Conflict Key pair exists. """ pass + class MethodNotAllowed(Exception): + """ Raised when the server returns 405 Method Not Allowed. """ pass + class OverLimit(Exception): + """ Raised when the server returns 413 Over Limit. """ pass + class ServerCapacityUnavailable(Exception): + """ Raised when the server returns 503 Server Capacity Uavailable. """ pass + class ServiceUnavailable(Exception): + """ Raised when the server returns 503 Service Unavailable. """ pass + class Unauthorized(Exception): + """ Raised when the server returns 401 Unauthorized. """ pass diff --git a/gns3dms/main.py b/gns3dms/main.py index 6cdad64e..d7d0fc30 100644 --- a/gns3dms/main.py +++ b/gns3dms/main.py @@ -41,7 +41,7 @@ from os.path import expanduser SCRIPT_NAME = os.path.basename(__file__) -#Is the full path when used as an import +# Is the full path when used as an import SCRIPT_PATH = os.path.dirname(__file__) if not SCRIPT_PATH: @@ -98,6 +98,8 @@ Options: """ % (SCRIPT_NAME) # Parse cmd line options + + def parse_cmd_line(argv): """ Parse command line arguments @@ -107,22 +109,22 @@ def parse_cmd_line(argv): short_args = "dvhk" long_args = ("debug", - "verbose", - "help", - "cloud_user_name=", - "cloud_api_key=", - "instance_id=", - "region=", - "dead_time=", - "init-wait=", - "check-interval=", - "file=", - "background", - ) + "verbose", + "help", + "cloud_user_name=", + "cloud_api_key=", + "instance_id=", + "region=", + "dead_time=", + "init-wait=", + "check-interval=", + "file=", + "background", + ) try: opts, extra_opts = getopt.getopt(argv[1:], short_args, long_args) except getopt.GetoptError as e: - print("Unrecognized command line option or missing required argument: %s" %(e)) + print("Unrecognized command line option or missing required argument: %s" % (e)) print(usage) sys.exit(2) @@ -133,7 +135,7 @@ def parse_cmd_line(argv): cmd_line_option_list["cloud_api_key"] = None cmd_line_option_list["instance_id"] = None cmd_line_option_list["region"] = None - cmd_line_option_list["dead_time"] = 60 * 60 #minutes + cmd_line_option_list["dead_time"] = 60 * 60 # minutes cmd_line_option_list["check-interval"] = None cmd_line_option_list["init-wait"] = 5 * 60 cmd_line_option_list["file"] = None @@ -146,8 +148,7 @@ def parse_cmd_line(argv): elif sys.platform == "osx": cmd_line_option_list['syslog'] = "/var/run/syslog" else: - cmd_line_option_list['syslog'] = ('localhost',514) - + cmd_line_option_list['syslog'] = ('localhost', 514) get_gns3secrets(cmd_line_option_list) cmd_line_option_list["dead_time"] = int(cmd_line_option_list["dead_time"]) @@ -181,7 +182,7 @@ def parse_cmd_line(argv): elif (opt in ("--background")): cmd_line_option_list["daemon"] = True - if cmd_line_option_list["shutdown"] == False: + if cmd_line_option_list["shutdown"] is False: if cmd_line_option_list["check-interval"] is None: cmd_line_option_list["check-interval"] = cmd_line_option_list["dead_time"] + 120 @@ -211,9 +212,9 @@ def parse_cmd_line(argv): print(usage) sys.exit(2) - return cmd_line_option_list + def get_gns3secrets(cmd_line_option_list): """ Load cloud credentials from .gns3secrets @@ -248,10 +249,10 @@ def set_logging(cmd_options): log_level = logging.INFO log_level_console = logging.WARNING - if cmd_options['verbose'] == True: + if cmd_options['verbose']: log_level_console = logging.INFO - if cmd_options['debug'] == True: + if cmd_options['debug']: log_level_console = logging.DEBUG log_level = logging.DEBUG @@ -275,6 +276,7 @@ def set_logging(cmd_options): return log + def send_shutdown(pid_file): """ Sends the daemon process a kill signal @@ -291,8 +293,9 @@ def send_shutdown(pid_file): def _get_file_age(filename): return datetime.datetime.fromtimestamp( - os.path.getmtime(filename) - ) + os.path.getmtime(filename) + ) + def monitor_loop(options): """ @@ -307,7 +310,7 @@ def monitor_loop(options): terminate_attempts = 0 - while options['shutdown'] == False: + while options['shutdown'] is False: log.debug("In monitor_loop for : %s" % ( datetime.datetime.now() - options['starttime']) ) @@ -320,15 +323,15 @@ def monitor_loop(options): if delta.seconds > options["dead_time"]: log.warning("Dead time exceeded, terminating instance ...") - #Terminate involves many layers of HTTP / API calls, lots of - #different errors types could occur here. + # Terminate involves many layers of HTTP / API calls, lots of + # different errors types could occur here. try: rksp = Rackspace(options) rksp.terminate() except Exception as e: log.critical("Exception during terminate: %s" % (e)) - terminate_attempts+=1 + terminate_attempts += 1 log.warning("Termination sent, attempt: %s" % (terminate_attempts)) time.sleep(600) else: @@ -372,14 +375,13 @@ def main(): for key, value in iter(sorted(options.items())): log.debug("%s : %s" % (key, value)) - log.debug("Checking file ....") - if os.path.isfile(options["file"]) == False: + if os.path.isfile(options["file"]) is False: log.critical("File does not exist!!!") sys.exit(1) test_acess = _get_file_age(options["file"]) - if type(test_acess) is not datetime.datetime: + if not isinstance(test_acess, datetime.datetime): log.critical("Can't get file modification time!!!") sys.exit(1) @@ -390,13 +392,11 @@ def main(): class MyDaemon(daemon.daemon): + def run(self): monitor_loop(self.options) - if __name__ == "__main__": result = main() sys.exit(result) - - diff --git a/gns3dms/modules/__init__.py b/gns3dms/modules/__init__.py index 885d6fa0..0950e877 100644 --- a/gns3dms/modules/__init__.py +++ b/gns3dms/modules/__init__.py @@ -21,4 +21,4 @@ # 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) \ No newline at end of file +# number has been incremented) diff --git a/gns3dms/modules/daemon.py b/gns3dms/modules/daemon.py index c7245335..cfc5539f 100644 --- a/gns3dms/modules/daemon.py +++ b/gns3dms/modules/daemon.py @@ -1,8 +1,14 @@ """Generic linux daemon base class for python 3.x.""" -import sys, os, time, atexit, signal +import sys +import os +import time +import atexit +import signal + class daemon: + """A generic daemon class. Usage: subclass the daemon class and override the run() method.""" @@ -54,7 +60,7 @@ class daemon: atexit.register(self.delpid) pid = str(os.getpid()) - with open(self.pidfile,'w+') as f: + with open(self.pidfile, 'w+') as f: f.write(pid + '\n') def delpid(self): @@ -74,7 +80,7 @@ class daemon: # Check for a pidfile to see if the daemon already runs try: - with open(self.pidfile,'r') as pf: + with open(self.pidfile, 'r') as pf: pid = int(pf.read().strip()) except IOError: @@ -101,20 +107,20 @@ class daemon: # Get the pid from the pidfile try: - with open(self.pidfile,'r') as pf: + with open(self.pidfile, 'r') as pf: pid = int(pf.read().strip()) except IOError: pid = None if not pid: message = "pidfile {0} does not exist. " + \ - "Daemon not running?\n" + "Daemon not running?\n" sys.stderr.write(message.format(self.pidfile)) - return # not an error in a restart + return # not an error in a restart # Try killing the daemon process try: - while 1: + while True: os.kill(pid, signal.SIGTERM) time.sleep(0.1) except OSError as err: @@ -123,7 +129,7 @@ class daemon: if os.path.exists(self.pidfile): os.remove(self.pidfile) else: - print (str(err.args)) + print(str(err.args)) sys.exit(1) def restart(self): diff --git a/gns3dms/modules/rackspace_cloud.py b/gns3dms/modules/rackspace_cloud.py index 487e6f9f..06f81046 100644 --- a/gns3dms/modules/rackspace_cloud.py +++ b/gns3dms/modules/rackspace_cloud.py @@ -23,7 +23,8 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -import os, sys +import os +import sys import json import logging import socket @@ -34,7 +35,9 @@ from gns3dms.cloud.rackspace_ctrl import RackspaceCtrl LOG_NAME = "gns3dms.rksp" log = logging.getLogger("%s" % (LOG_NAME)) + class Rackspace(object): + def __init__(self, options): self.username = options["cloud_user_name"] self.apikey = options["cloud_api_key"] @@ -54,7 +57,7 @@ class Rackspace(object): for region in self.rksp.list_regions(): log.debug("Rackspace regions: %s" % (region)) - + log.debug("Checking region: %s" % (self.region)) self.rksp.set_region(self.region) for server in self.rksp.list_instances(): diff --git a/gns3server/config.py b/gns3server/config.py index d851c49c..57d61180 100644 --- a/gns3server/config.py +++ b/gns3server/config.py @@ -30,6 +30,7 @@ CLOUD_SERVER = 'CLOUD_SERVER' class Config(object): + """ Configuration file management using configparser. """ @@ -117,7 +118,7 @@ class Config(object): :returns: configparser section """ - if not section in self._config: + if section is not in self._config: return self._config["DEFAULT"] return self._config[section] diff --git a/gns3server/handlers/auth_handler.py b/gns3server/handlers/auth_handler.py index 6db9f4ec..b479e533 100644 --- a/gns3server/handlers/auth_handler.py +++ b/gns3server/handlers/auth_handler.py @@ -27,32 +27,37 @@ import tornado.websocket import logging log = logging.getLogger(__name__) + class GNS3BaseHandler(tornado.web.RequestHandler): + def get_current_user(self): if 'required_user' not in self.settings: return "FakeUser" user = self.get_secure_cookie("user") if not user: - return None + return None if self.settings['required_user'] == user.decode("utf-8"): - return user + return user + class GNS3WebSocketBaseHandler(tornado.websocket.WebSocketHandler): + def get_current_user(self): if 'required_user' not in self.settings: return "FakeUser" user = self.get_secure_cookie("user") if not user: - return None + return None if self.settings['required_user'] == user.decode("utf-8"): - return user + return user class LoginHandler(tornado.web.RequestHandler): + def get(self): self.write('
' 'Name: ' @@ -61,10 +66,10 @@ class LoginHandler(tornado.web.RequestHandler): '
') try: - redirect_to = self.get_argument("next") - self.set_secure_cookie("login_success_redirect_to", redirect_to) + redirect_to = self.get_argument("next") + self.set_secure_cookie("login_success_redirect_to", redirect_to) except tornado.web.MissingArgumentError: - pass + pass def post(self): @@ -72,21 +77,21 @@ class LoginHandler(tornado.web.RequestHandler): password = self.get_argument("password") if self.settings['required_user'] == user and self.settings['required_pass'] == password: - self.set_secure_cookie("user", user) - auth_status = "successful" + self.set_secure_cookie("user", user) + auth_status = "successful" else: - self.set_secure_cookie("user", "None") - auth_status = "failure" + self.set_secure_cookie("user", "None") + auth_status = "failure" log.info("Authentication attempt {}: {}, {}".format(auth_status, user, password)) try: - redirect_to = self.get_secure_cookie("login_success_redirect_to") + redirect_to = self.get_secure_cookie("login_success_redirect_to") except tornado.web.MissingArgumentError: - redirect_to = "/" + redirect_to = "/" if redirect_to is None: self.write({'result': auth_status}) else: log.info('Redirecting to {}'.format(redirect_to)) - self.redirect(redirect_to) \ No newline at end of file + self.redirect(redirect_to) diff --git a/gns3server/handlers/file_upload_handler.py b/gns3server/handlers/file_upload_handler.py index d4e33200..7c8fd862 100644 --- a/gns3server/handlers/file_upload_handler.py +++ b/gns3server/handlers/file_upload_handler.py @@ -34,6 +34,7 @@ log = logging.getLogger(__name__) class FileUploadHandler(GNS3BaseHandler): + """ File upload handler. diff --git a/gns3server/handlers/project_handler.py b/gns3server/handlers/project_handler.py index a3a7bf35..79d1b68e 100644 --- a/gns3server/handlers/project_handler.py +++ b/gns3server/handlers/project_handler.py @@ -22,6 +22,7 @@ from aiohttp.web import HTTPConflict class ProjectHandler: + @classmethod @Route.post( r"/project", diff --git a/gns3server/handlers/virtualbox_handler.py b/gns3server/handlers/virtualbox_handler.py index 2b8714dd..01c20b3e 100644 --- a/gns3server/handlers/virtualbox_handler.py +++ b/gns3server/handlers/virtualbox_handler.py @@ -22,6 +22,7 @@ from ..modules.virtualbox import VirtualBox class VirtualBoxHandler: + """ API entry points for VirtualBox. """ diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index e18d40fa..3eb9b46d 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -23,6 +23,7 @@ from ..modules.vpcs import VPCS class VPCSHandler: + """ API entry points for VPCS. """ diff --git a/gns3server/main.py b/gns3server/main.py index 88411826..65993058 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -27,6 +27,7 @@ from gns3server.version import __version__ import logging log = logging.getLogger(__name__) + def locale_check(): """ Checks if this application runs with a correct locale (i.e. supports UTF-8 encoding) and attempt to fix @@ -71,11 +72,10 @@ def main(): Entry point for GNS3 server """ - #TODO: migrate command line options to argparse (don't forget the quiet mode). + # TODO: migrate command line options to argparse (don't forget the quiet mode). current_year = datetime.date.today().year - # TODO: Renable the test when we will have command line # user_log = logging.getLogger('user_facing') # if not options.quiet: @@ -95,7 +95,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 + # TODO: end todo # we only support Python 3 version >= 3.3 if sys.version_info < (3, 3): @@ -115,7 +115,7 @@ def main(): return # TODO: Renable console_bind_to_any when we will have command line parsing - #server = Server(options.host, options.port, options.console_bind_to_any) + # server = Server(options.host, options.port, options.console_bind_to_any) server = Server("127.0.0.1", 8000, False) server.run() diff --git a/gns3server/modules/adapters/adapter.py b/gns3server/modules/adapters/adapter.py index cf439427..ade660f9 100644 --- a/gns3server/modules/adapters/adapter.py +++ b/gns3server/modules/adapters/adapter.py @@ -17,6 +17,7 @@ class Adapter(object): + """ Base class for adapters. diff --git a/gns3server/modules/adapters/ethernet_adapter.py b/gns3server/modules/adapters/ethernet_adapter.py index bbca7f40..9d3ee003 100644 --- a/gns3server/modules/adapters/ethernet_adapter.py +++ b/gns3server/modules/adapters/ethernet_adapter.py @@ -19,6 +19,7 @@ from .adapter import Adapter class EthernetAdapter(Adapter): + """ VPCS Ethernet adapter. """ diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 3f211c98..c90a2bab 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -24,6 +24,7 @@ from .project_manager import ProjectManager class BaseManager: + """ Base class for all Manager. Responsible of management of a VM pool diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index b04aeaa5..26347094 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -95,6 +95,7 @@ log = logging.getLogger(__name__) class Dynamips(IModule): + """ Dynamips module. @@ -140,7 +141,7 @@ class Dynamips(IModule): self._console_host = dynamips_config.get("console_host", kwargs["console_host"]) if not sys.platform.startswith("win32"): - #FIXME: pickle issues Windows + # FIXME: pickle issues Windows self._callback = self.add_periodic_callback(self._check_hypervisors, 5000) self._callback.start() @@ -323,7 +324,7 @@ class Dynamips(IModule): log.debug("received request {}".format(request)) - #TODO: JSON schema validation + # TODO: JSON schema validation if not self._hypervisor_manager: if "path" in request: @@ -407,7 +408,7 @@ class Dynamips(IModule): rhost = request["nio"]["rhost"] rport = request["nio"]["rport"] try: - #TODO: handle IPv6 + # TODO: handle IPv6 with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: sock.connect((rhost, rport)) except OSError as e: diff --git a/gns3server/modules/dynamips/adapters/adapter.py b/gns3server/modules/dynamips/adapters/adapter.py index d963933e..40d82c7e 100644 --- a/gns3server/modules/dynamips/adapters/adapter.py +++ b/gns3server/modules/dynamips/adapters/adapter.py @@ -17,6 +17,7 @@ class Adapter(object): + """ Base class for adapters. diff --git a/gns3server/modules/dynamips/adapters/c1700_mb_1fe.py b/gns3server/modules/dynamips/adapters/c1700_mb_1fe.py index 3c67f3df..c94f551d 100644 --- a/gns3server/modules/dynamips/adapters/c1700_mb_1fe.py +++ b/gns3server/modules/dynamips/adapters/c1700_mb_1fe.py @@ -19,6 +19,7 @@ from .adapter import Adapter class C1700_MB_1FE(Adapter): + """ Integrated 1 port FastEthernet adapter for c1700 platform. """ diff --git a/gns3server/modules/dynamips/adapters/c1700_mb_wic1.py b/gns3server/modules/dynamips/adapters/c1700_mb_wic1.py index eca72358..9c6d2190 100644 --- a/gns3server/modules/dynamips/adapters/c1700_mb_wic1.py +++ b/gns3server/modules/dynamips/adapters/c1700_mb_wic1.py @@ -19,6 +19,7 @@ from .adapter import Adapter class C1700_MB_WIC1(Adapter): + """ Fake module to provide a placeholder for slot 1 interfaces when WICs are inserted into WIC slot 1. diff --git a/gns3server/modules/dynamips/adapters/c2600_mb_1e.py b/gns3server/modules/dynamips/adapters/c2600_mb_1e.py index 26fe5497..bebe7fa9 100644 --- a/gns3server/modules/dynamips/adapters/c2600_mb_1e.py +++ b/gns3server/modules/dynamips/adapters/c2600_mb_1e.py @@ -19,6 +19,7 @@ from .adapter import Adapter class C2600_MB_1E(Adapter): + """ Integrated 1 port Ethernet adapter for the c2600 platform. """ diff --git a/gns3server/modules/dynamips/adapters/c2600_mb_1fe.py b/gns3server/modules/dynamips/adapters/c2600_mb_1fe.py index 768d9c95..1ad294f2 100644 --- a/gns3server/modules/dynamips/adapters/c2600_mb_1fe.py +++ b/gns3server/modules/dynamips/adapters/c2600_mb_1fe.py @@ -19,6 +19,7 @@ from .adapter import Adapter class C2600_MB_1FE(Adapter): + """ Integrated 1 port FastEthernet adapter for the c2600 platform. """ diff --git a/gns3server/modules/dynamips/adapters/c2600_mb_2e.py b/gns3server/modules/dynamips/adapters/c2600_mb_2e.py index c2ca7442..1e42d5dd 100644 --- a/gns3server/modules/dynamips/adapters/c2600_mb_2e.py +++ b/gns3server/modules/dynamips/adapters/c2600_mb_2e.py @@ -19,6 +19,7 @@ from .adapter import Adapter class C2600_MB_2E(Adapter): + """ Integrated 2 port Ethernet adapter for the c2600 platform. """ diff --git a/gns3server/modules/dynamips/adapters/c2600_mb_2fe.py b/gns3server/modules/dynamips/adapters/c2600_mb_2fe.py index a7e6df14..dcd96581 100644 --- a/gns3server/modules/dynamips/adapters/c2600_mb_2fe.py +++ b/gns3server/modules/dynamips/adapters/c2600_mb_2fe.py @@ -19,6 +19,7 @@ from .adapter import Adapter class C2600_MB_2FE(Adapter): + """ Integrated 2 port FastEthernet adapter for the c2600 platform. """ diff --git a/gns3server/modules/dynamips/adapters/c7200_io_2fe.py b/gns3server/modules/dynamips/adapters/c7200_io_2fe.py index 0b8ae8a4..8b545e99 100644 --- a/gns3server/modules/dynamips/adapters/c7200_io_2fe.py +++ b/gns3server/modules/dynamips/adapters/c7200_io_2fe.py @@ -19,6 +19,7 @@ from .adapter import Adapter class C7200_IO_2FE(Adapter): + """ C7200-IO-2FE FastEthernet Input/Ouput controller. """ diff --git a/gns3server/modules/dynamips/adapters/c7200_io_fe.py b/gns3server/modules/dynamips/adapters/c7200_io_fe.py index 56e86cf1..784b154d 100644 --- a/gns3server/modules/dynamips/adapters/c7200_io_fe.py +++ b/gns3server/modules/dynamips/adapters/c7200_io_fe.py @@ -19,6 +19,7 @@ from .adapter import Adapter class C7200_IO_FE(Adapter): + """ C7200-IO-FE FastEthernet Input/Ouput controller. """ diff --git a/gns3server/modules/dynamips/adapters/c7200_io_ge_e.py b/gns3server/modules/dynamips/adapters/c7200_io_ge_e.py index 12ebaed6..f233dffd 100644 --- a/gns3server/modules/dynamips/adapters/c7200_io_ge_e.py +++ b/gns3server/modules/dynamips/adapters/c7200_io_ge_e.py @@ -19,6 +19,7 @@ from .adapter import Adapter class C7200_IO_GE_E(Adapter): + """ C7200-IO-GE-E GigabitEthernet Input/Ouput controller. """ diff --git a/gns3server/modules/dynamips/adapters/leopard_2fe.py b/gns3server/modules/dynamips/adapters/leopard_2fe.py index 0afa95c0..db6ad9c2 100644 --- a/gns3server/modules/dynamips/adapters/leopard_2fe.py +++ b/gns3server/modules/dynamips/adapters/leopard_2fe.py @@ -19,6 +19,7 @@ from .adapter import Adapter class Leopard_2FE(Adapter): + """ Integrated 2 port FastEthernet adapter for c3660 router. """ diff --git a/gns3server/modules/dynamips/adapters/nm_16esw.py b/gns3server/modules/dynamips/adapters/nm_16esw.py index fc3755cd..31e74565 100644 --- a/gns3server/modules/dynamips/adapters/nm_16esw.py +++ b/gns3server/modules/dynamips/adapters/nm_16esw.py @@ -19,6 +19,7 @@ from .adapter import Adapter class NM_16ESW(Adapter): + """ NM-16ESW FastEthernet network module. """ diff --git a/gns3server/modules/dynamips/adapters/nm_1e.py b/gns3server/modules/dynamips/adapters/nm_1e.py index ac200247..59ac5569 100644 --- a/gns3server/modules/dynamips/adapters/nm_1e.py +++ b/gns3server/modules/dynamips/adapters/nm_1e.py @@ -19,6 +19,7 @@ from .adapter import Adapter class NM_1E(Adapter): + """ NM-1E Ethernet network module. """ diff --git a/gns3server/modules/dynamips/adapters/nm_1fe_tx.py b/gns3server/modules/dynamips/adapters/nm_1fe_tx.py index 9723f703..26568306 100644 --- a/gns3server/modules/dynamips/adapters/nm_1fe_tx.py +++ b/gns3server/modules/dynamips/adapters/nm_1fe_tx.py @@ -19,6 +19,7 @@ from .adapter import Adapter class NM_1FE_TX(Adapter): + """ NM-1FE-TX FastEthernet network module. """ diff --git a/gns3server/modules/dynamips/adapters/nm_4e.py b/gns3server/modules/dynamips/adapters/nm_4e.py index ae6a51ed..086b04ee 100644 --- a/gns3server/modules/dynamips/adapters/nm_4e.py +++ b/gns3server/modules/dynamips/adapters/nm_4e.py @@ -19,6 +19,7 @@ from .adapter import Adapter class NM_4E(Adapter): + """ NM-4E Ethernet network module. """ diff --git a/gns3server/modules/dynamips/adapters/nm_4t.py b/gns3server/modules/dynamips/adapters/nm_4t.py index df6db299..77c3ecc8 100644 --- a/gns3server/modules/dynamips/adapters/nm_4t.py +++ b/gns3server/modules/dynamips/adapters/nm_4t.py @@ -19,6 +19,7 @@ from .adapter import Adapter class NM_4T(Adapter): + """ NM-4T Serial network module. """ diff --git a/gns3server/modules/dynamips/adapters/pa_2fe_tx.py b/gns3server/modules/dynamips/adapters/pa_2fe_tx.py index 8589ff2e..09b677f3 100644 --- a/gns3server/modules/dynamips/adapters/pa_2fe_tx.py +++ b/gns3server/modules/dynamips/adapters/pa_2fe_tx.py @@ -19,6 +19,7 @@ from .adapter import Adapter class PA_2FE_TX(Adapter): + """ PA-2FE-TX FastEthernet port adapter. """ diff --git a/gns3server/modules/dynamips/adapters/pa_4e.py b/gns3server/modules/dynamips/adapters/pa_4e.py index 32564992..d5981860 100644 --- a/gns3server/modules/dynamips/adapters/pa_4e.py +++ b/gns3server/modules/dynamips/adapters/pa_4e.py @@ -19,6 +19,7 @@ from .adapter import Adapter class PA_4E(Adapter): + """ PA-4E Ethernet port adapter. """ diff --git a/gns3server/modules/dynamips/adapters/pa_4t.py b/gns3server/modules/dynamips/adapters/pa_4t.py index 6a098a24..5a1393bc 100644 --- a/gns3server/modules/dynamips/adapters/pa_4t.py +++ b/gns3server/modules/dynamips/adapters/pa_4t.py @@ -19,6 +19,7 @@ from .adapter import Adapter class PA_4T(Adapter): + """ PA-4T+ Serial port adapter. """ diff --git a/gns3server/modules/dynamips/adapters/pa_8e.py b/gns3server/modules/dynamips/adapters/pa_8e.py index a6b5075f..96684055 100644 --- a/gns3server/modules/dynamips/adapters/pa_8e.py +++ b/gns3server/modules/dynamips/adapters/pa_8e.py @@ -19,6 +19,7 @@ from .adapter import Adapter class PA_8E(Adapter): + """ PA-8E Ethernet port adapter. """ diff --git a/gns3server/modules/dynamips/adapters/pa_8t.py b/gns3server/modules/dynamips/adapters/pa_8t.py index 600a5c29..723e026f 100644 --- a/gns3server/modules/dynamips/adapters/pa_8t.py +++ b/gns3server/modules/dynamips/adapters/pa_8t.py @@ -19,6 +19,7 @@ from .adapter import Adapter class PA_8T(Adapter): + """ PA-8T Serial port adapter. """ diff --git a/gns3server/modules/dynamips/adapters/pa_a1.py b/gns3server/modules/dynamips/adapters/pa_a1.py index 21d51f15..469d9ce4 100644 --- a/gns3server/modules/dynamips/adapters/pa_a1.py +++ b/gns3server/modules/dynamips/adapters/pa_a1.py @@ -19,6 +19,7 @@ from .adapter import Adapter class PA_A1(Adapter): + """ PA-A1 ATM port adapter. """ diff --git a/gns3server/modules/dynamips/adapters/pa_fe_tx.py b/gns3server/modules/dynamips/adapters/pa_fe_tx.py index 70ce8489..6434d2b4 100644 --- a/gns3server/modules/dynamips/adapters/pa_fe_tx.py +++ b/gns3server/modules/dynamips/adapters/pa_fe_tx.py @@ -19,6 +19,7 @@ from .adapter import Adapter class PA_FE_TX(Adapter): + """ PA-FE-TX FastEthernet port adapter. """ diff --git a/gns3server/modules/dynamips/adapters/pa_ge.py b/gns3server/modules/dynamips/adapters/pa_ge.py index f0287408..e466d905 100644 --- a/gns3server/modules/dynamips/adapters/pa_ge.py +++ b/gns3server/modules/dynamips/adapters/pa_ge.py @@ -19,6 +19,7 @@ from .adapter import Adapter class PA_GE(Adapter): + """ PA-GE GigabitEthernet port adapter. """ diff --git a/gns3server/modules/dynamips/adapters/pa_pos_oc3.py b/gns3server/modules/dynamips/adapters/pa_pos_oc3.py index b120de97..de0bc5d1 100644 --- a/gns3server/modules/dynamips/adapters/pa_pos_oc3.py +++ b/gns3server/modules/dynamips/adapters/pa_pos_oc3.py @@ -19,6 +19,7 @@ from .adapter import Adapter class PA_POS_OC3(Adapter): + """ PA-POS-OC3 port adapter. """ diff --git a/gns3server/modules/dynamips/adapters/wic_1enet.py b/gns3server/modules/dynamips/adapters/wic_1enet.py index dac79b6b..2d5e62b7 100644 --- a/gns3server/modules/dynamips/adapters/wic_1enet.py +++ b/gns3server/modules/dynamips/adapters/wic_1enet.py @@ -17,6 +17,7 @@ class WIC_1ENET(object): + """ WIC-1ENET Ethernet """ diff --git a/gns3server/modules/dynamips/adapters/wic_1t.py b/gns3server/modules/dynamips/adapters/wic_1t.py index 0f7cb3ad..2067246d 100644 --- a/gns3server/modules/dynamips/adapters/wic_1t.py +++ b/gns3server/modules/dynamips/adapters/wic_1t.py @@ -17,6 +17,7 @@ class WIC_1T(object): + """ WIC-1T Serial """ diff --git a/gns3server/modules/dynamips/adapters/wic_2t.py b/gns3server/modules/dynamips/adapters/wic_2t.py index 2bf2d565..b5af954e 100644 --- a/gns3server/modules/dynamips/adapters/wic_2t.py +++ b/gns3server/modules/dynamips/adapters/wic_2t.py @@ -17,6 +17,7 @@ class WIC_2T(object): + """ WIC-2T Serial """ diff --git a/gns3server/modules/dynamips/backends/vm.py b/gns3server/modules/dynamips/backends/vm.py index 8348a231..e40e79d6 100644 --- a/gns3server/modules/dynamips/backends/vm.py +++ b/gns3server/modules/dynamips/backends/vm.py @@ -466,7 +466,7 @@ class VM(object): adapter_name = value adapter = ADAPTER_MATRIX[adapter_name]() try: - if router.slots[slot_id] and type(router.slots[slot_id]) != type(adapter): + if router.slots[slot_id] and not isinstance(router.slots[slot_id], type(adapter)): router.slot_remove_binding(slot_id) router.slot_add_binding(slot_id, adapter) response[name] = value @@ -487,14 +487,14 @@ class VM(object): wic_name = value wic = WIC_MATRIX[wic_name]() try: - if router.slots[0].wics[wic_slot_id] and type(router.slots[0].wics[wic_slot_id]) != type(wic): + if router.slots[0].wics[wic_slot_id] and not isinstance(router.slots[0].wics[wic_slot_id], type(wic)): router.uninstall_wic(wic_slot_id) router.install_wic(wic_slot_id, wic) response[name] = value except DynamipsError as e: self.send_custom_error(str(e)) return - elif name.startswith("wic") and value == None: + elif name.startswith("wic") and value is None: wic_slot_id = int(name[-1]) if router.slots[0].wics and router.slots[0].wics[wic_slot_id]: try: diff --git a/gns3server/modules/dynamips/dynamips_hypervisor.py b/gns3server/modules/dynamips/dynamips_hypervisor.py index 7c93f775..1ac01ee1 100644 --- a/gns3server/modules/dynamips/dynamips_hypervisor.py +++ b/gns3server/modules/dynamips/dynamips_hypervisor.py @@ -30,6 +30,7 @@ log = logging.getLogger(__name__) class DynamipsHypervisor(object): + """ Creates a new connection to a Dynamips server (also called hypervisor) diff --git a/gns3server/modules/dynamips/hypervisor.py b/gns3server/modules/dynamips/hypervisor.py index 88c577d7..ffce2935 100644 --- a/gns3server/modules/dynamips/hypervisor.py +++ b/gns3server/modules/dynamips/hypervisor.py @@ -32,6 +32,7 @@ log = logging.getLogger(__name__) class Hypervisor(DynamipsHypervisor): + """ Hypervisor. diff --git a/gns3server/modules/dynamips/hypervisor_manager.py b/gns3server/modules/dynamips/hypervisor_manager.py index 3106d98c..a9be6ed0 100644 --- a/gns3server/modules/dynamips/hypervisor_manager.py +++ b/gns3server/modules/dynamips/hypervisor_manager.py @@ -34,6 +34,7 @@ log = logging.getLogger(__name__) class HypervisorManager(object): + """ Manages Dynamips hypervisors. diff --git a/gns3server/modules/dynamips/nios/nio.py b/gns3server/modules/dynamips/nios/nio.py index 1fd61bf9..3b66d54f 100644 --- a/gns3server/modules/dynamips/nios/nio.py +++ b/gns3server/modules/dynamips/nios/nio.py @@ -27,6 +27,7 @@ log = logging.getLogger(__name__) class NIO(object): + """ Base NIO class diff --git a/gns3server/modules/dynamips/nios/nio_fifo.py b/gns3server/modules/dynamips/nios/nio_fifo.py index a67f863d..c85b679d 100644 --- a/gns3server/modules/dynamips/nios/nio_fifo.py +++ b/gns3server/modules/dynamips/nios/nio_fifo.py @@ -26,6 +26,7 @@ log = logging.getLogger(__name__) class NIO_FIFO(NIO): + """ Dynamips FIFO NIO. diff --git a/gns3server/modules/dynamips/nios/nio_generic_ethernet.py b/gns3server/modules/dynamips/nios/nio_generic_ethernet.py index 58e6ec7f..2a8b1443 100644 --- a/gns3server/modules/dynamips/nios/nio_generic_ethernet.py +++ b/gns3server/modules/dynamips/nios/nio_generic_ethernet.py @@ -26,6 +26,7 @@ log = logging.getLogger(__name__) class NIO_GenericEthernet(NIO): + """ Dynamips generic Ethernet NIO. diff --git a/gns3server/modules/dynamips/nios/nio_linux_ethernet.py b/gns3server/modules/dynamips/nios/nio_linux_ethernet.py index a199c264..25988aa8 100644 --- a/gns3server/modules/dynamips/nios/nio_linux_ethernet.py +++ b/gns3server/modules/dynamips/nios/nio_linux_ethernet.py @@ -26,6 +26,7 @@ log = logging.getLogger(__name__) class NIO_LinuxEthernet(NIO): + """ Dynamips Linux Ethernet NIO. diff --git a/gns3server/modules/dynamips/nios/nio_mcast.py b/gns3server/modules/dynamips/nios/nio_mcast.py index 4d939d5a..bcd42670 100644 --- a/gns3server/modules/dynamips/nios/nio_mcast.py +++ b/gns3server/modules/dynamips/nios/nio_mcast.py @@ -26,6 +26,7 @@ log = logging.getLogger(__name__) class NIO_Mcast(NIO): + """ Dynamips Linux Ethernet NIO. @@ -103,5 +104,5 @@ class NIO_Mcast(NIO): """ self._hypervisor.send("nio set_mcast_ttl {name} {ttl}".format(name=self._name, - ttl=ttl)) + ttl=ttl)) self._ttl = ttl diff --git a/gns3server/modules/dynamips/nios/nio_null.py b/gns3server/modules/dynamips/nios/nio_null.py index b9350c07..1cde2a52 100644 --- a/gns3server/modules/dynamips/nios/nio_null.py +++ b/gns3server/modules/dynamips/nios/nio_null.py @@ -26,6 +26,7 @@ log = logging.getLogger(__name__) class NIO_Null(NIO): + """ Dynamips NULL NIO. diff --git a/gns3server/modules/dynamips/nios/nio_tap.py b/gns3server/modules/dynamips/nios/nio_tap.py index 9ee16abb..d24e9109 100644 --- a/gns3server/modules/dynamips/nios/nio_tap.py +++ b/gns3server/modules/dynamips/nios/nio_tap.py @@ -26,6 +26,7 @@ log = logging.getLogger(__name__) class NIO_TAP(NIO): + """ Dynamips TAP NIO. diff --git a/gns3server/modules/dynamips/nios/nio_udp.py b/gns3server/modules/dynamips/nios/nio_udp.py index bcfd9e4d..d9e2d294 100644 --- a/gns3server/modules/dynamips/nios/nio_udp.py +++ b/gns3server/modules/dynamips/nios/nio_udp.py @@ -26,6 +26,7 @@ log = logging.getLogger(__name__) class NIO_UDP(NIO): + """ Dynamips UDP NIO. diff --git a/gns3server/modules/dynamips/nios/nio_udp_auto.py b/gns3server/modules/dynamips/nios/nio_udp_auto.py index ccefce2b..03d290d6 100644 --- a/gns3server/modules/dynamips/nios/nio_udp_auto.py +++ b/gns3server/modules/dynamips/nios/nio_udp_auto.py @@ -26,6 +26,7 @@ log = logging.getLogger(__name__) class NIO_UDP_auto(NIO): + """ Dynamips auto UDP NIO. diff --git a/gns3server/modules/dynamips/nios/nio_unix.py b/gns3server/modules/dynamips/nios/nio_unix.py index f699eead..af100d2e 100644 --- a/gns3server/modules/dynamips/nios/nio_unix.py +++ b/gns3server/modules/dynamips/nios/nio_unix.py @@ -26,6 +26,7 @@ log = logging.getLogger(__name__) class NIO_UNIX(NIO): + """ Dynamips UNIX NIO. diff --git a/gns3server/modules/dynamips/nios/nio_vde.py b/gns3server/modules/dynamips/nios/nio_vde.py index 79af96d7..7157834f 100644 --- a/gns3server/modules/dynamips/nios/nio_vde.py +++ b/gns3server/modules/dynamips/nios/nio_vde.py @@ -26,6 +26,7 @@ log = logging.getLogger(__name__) class NIO_VDE(NIO): + """ Dynamips VDE NIO. diff --git a/gns3server/modules/dynamips/nodes/atm_bridge.py b/gns3server/modules/dynamips/nodes/atm_bridge.py index 10abe1b2..bfed0b78 100644 --- a/gns3server/modules/dynamips/nodes/atm_bridge.py +++ b/gns3server/modules/dynamips/nodes/atm_bridge.py @@ -24,6 +24,7 @@ from ..dynamips_error import DynamipsError class ATMBridge(object): + """ Dynamips bridge switch. @@ -33,7 +34,7 @@ class ATMBridge(object): def __init__(self, hypervisor, name): - #FIXME: instance tracking + # FIXME: instance tracking self._hypervisor = hypervisor self._name = '"' + name + '"' # put name into quotes to protect spaces self._hypervisor.send("atm_bridge create {}".format(self._name)) diff --git a/gns3server/modules/dynamips/nodes/atm_switch.py b/gns3server/modules/dynamips/nodes/atm_switch.py index 0c382c44..aa0dba3e 100644 --- a/gns3server/modules/dynamips/nodes/atm_switch.py +++ b/gns3server/modules/dynamips/nodes/atm_switch.py @@ -28,6 +28,7 @@ log = logging.getLogger(__name__) class ATMSwitch(object): + """ Dynamips ATM switch. diff --git a/gns3server/modules/dynamips/nodes/bridge.py b/gns3server/modules/dynamips/nodes/bridge.py index a87ba029..fcac17b9 100644 --- a/gns3server/modules/dynamips/nodes/bridge.py +++ b/gns3server/modules/dynamips/nodes/bridge.py @@ -22,6 +22,7 @@ http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L538 class Bridge(object): + """ Dynamips bridge. diff --git a/gns3server/modules/dynamips/nodes/c1700.py b/gns3server/modules/dynamips/nodes/c1700.py index 906abe3e..249ab508 100644 --- a/gns3server/modules/dynamips/nodes/c1700.py +++ b/gns3server/modules/dynamips/nodes/c1700.py @@ -29,6 +29,7 @@ log = logging.getLogger(__name__) class C1700(Router): + """ Dynamips c1700 router. diff --git a/gns3server/modules/dynamips/nodes/c2600.py b/gns3server/modules/dynamips/nodes/c2600.py index b5e46e89..083bbce6 100644 --- a/gns3server/modules/dynamips/nodes/c2600.py +++ b/gns3server/modules/dynamips/nodes/c2600.py @@ -31,6 +31,7 @@ log = logging.getLogger(__name__) class C2600(Router): + """ Dynamips c2600 router. diff --git a/gns3server/modules/dynamips/nodes/c2691.py b/gns3server/modules/dynamips/nodes/c2691.py index 0dc0ef28..fca62624 100644 --- a/gns3server/modules/dynamips/nodes/c2691.py +++ b/gns3server/modules/dynamips/nodes/c2691.py @@ -28,6 +28,7 @@ log = logging.getLogger(__name__) class C2691(Router): + """ Dynamips c2691 router. diff --git a/gns3server/modules/dynamips/nodes/c3600.py b/gns3server/modules/dynamips/nodes/c3600.py index 32e2bbe7..8b9e8966 100644 --- a/gns3server/modules/dynamips/nodes/c3600.py +++ b/gns3server/modules/dynamips/nodes/c3600.py @@ -28,6 +28,7 @@ log = logging.getLogger(__name__) class C3600(Router): + """ Dynamips c3600 router. diff --git a/gns3server/modules/dynamips/nodes/c3725.py b/gns3server/modules/dynamips/nodes/c3725.py index 9317a393..76ba4d9f 100644 --- a/gns3server/modules/dynamips/nodes/c3725.py +++ b/gns3server/modules/dynamips/nodes/c3725.py @@ -28,6 +28,7 @@ log = logging.getLogger(__name__) class C3725(Router): + """ Dynamips c3725 router. diff --git a/gns3server/modules/dynamips/nodes/c3745.py b/gns3server/modules/dynamips/nodes/c3745.py index 8002909a..0903b789 100644 --- a/gns3server/modules/dynamips/nodes/c3745.py +++ b/gns3server/modules/dynamips/nodes/c3745.py @@ -28,6 +28,7 @@ log = logging.getLogger(__name__) class C3745(Router): + """ Dynamips c3745 router. diff --git a/gns3server/modules/dynamips/nodes/c7200.py b/gns3server/modules/dynamips/nodes/c7200.py index 21ab4aa6..8ba10f8e 100644 --- a/gns3server/modules/dynamips/nodes/c7200.py +++ b/gns3server/modules/dynamips/nodes/c7200.py @@ -30,6 +30,7 @@ log = logging.getLogger(__name__) class C7200(Router): + """ Dynamips c7200 router (model is 7206). @@ -227,9 +228,9 @@ class C7200(Router): powered_on=power_supply)) log.info("router {name} [id={id}]: power supply {power_supply_id} state updated to {powered_on}".format(name=self._name, - id=self._id, - power_supply_id=power_supply_id, - powered_on=power_supply)) + id=self._id, + power_supply_id=power_supply_id, + powered_on=power_supply)) power_supply_id += 1 self._power_supplies = power_supplies diff --git a/gns3server/modules/dynamips/nodes/ethernet_switch.py b/gns3server/modules/dynamips/nodes/ethernet_switch.py index 45cc25c0..a88346dc 100644 --- a/gns3server/modules/dynamips/nodes/ethernet_switch.py +++ b/gns3server/modules/dynamips/nodes/ethernet_switch.py @@ -28,6 +28,7 @@ log = logging.getLogger(__name__) class EthernetSwitch(object): + """ Dynamips Ethernet switch. @@ -268,9 +269,9 @@ class EthernetSwitch(object): outer_vlan=outer_vlan)) log.info("Ethernet switch {name} [id={id}]: port {port} set as a QinQ port with outer VLAN {vlan_id}".format(name=self._name, - id=self._id, - port=port, - vlan_id=outer_vlan)) + id=self._id, + port=port, + vlan_id=outer_vlan)) self._mapping[port] = ("qinq", outer_vlan) def get_mac_addr_table(self): diff --git a/gns3server/modules/dynamips/nodes/frame_relay_switch.py b/gns3server/modules/dynamips/nodes/frame_relay_switch.py index 0b44fbea..8a309301 100644 --- a/gns3server/modules/dynamips/nodes/frame_relay_switch.py +++ b/gns3server/modules/dynamips/nodes/frame_relay_switch.py @@ -28,6 +28,7 @@ log = logging.getLogger(__name__) class FrameRelaySwitch(object): + """ Dynamips Frame Relay switch. diff --git a/gns3server/modules/dynamips/nodes/hub.py b/gns3server/modules/dynamips/nodes/hub.py index 6f7f0e59..18cedbe1 100644 --- a/gns3server/modules/dynamips/nodes/hub.py +++ b/gns3server/modules/dynamips/nodes/hub.py @@ -28,6 +28,7 @@ log = logging.getLogger(__name__) class Hub(Bridge): + """ Dynamips hub (based on Bridge) diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index d0d1aef3..18d8db8d 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -33,6 +33,7 @@ log = logging.getLogger(__name__) class Router(object): + """ Dynamips router implementation. @@ -575,7 +576,7 @@ class Router(object): try: reply = self._hypervisor.send("vm extract_config {}".format(self._name))[0].rsplit(' ', 2)[-2:] except IOError: - #for some reason Dynamips gets frozen when it does not find the magic number in the NVRAM file. + # for some reason Dynamips gets frozen when it does not find the magic number in the NVRAM file. return None, None startup_config = reply[0][1:-1] # get statup-config and remove single quotes private_config = reply[1][1:-1] # get private-config and remove single quotes diff --git a/gns3server/modules/dynamips/schemas/ethsw.py b/gns3server/modules/dynamips/schemas/ethsw.py index aeac7023..33559f28 100644 --- a/gns3server/modules/dynamips/schemas/ethsw.py +++ b/gns3server/modules/dynamips/schemas/ethsw.py @@ -44,7 +44,7 @@ ETHSW_DELETE_SCHEMA = { "required": ["id"] } -#TODO: ports {'1': {'vlan': 1, 'type': 'qinq'} +# TODO: ports {'1': {'vlan': 1, 'type': 'qinq'} ETHSW_UPDATE_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", "description": "Request validation to update an Ethernet switch instance", @@ -59,20 +59,20 @@ ETHSW_UPDATE_SCHEMA = { "type": "string", "minLength": 1, }, -# "ports": { -# "type": "object", -# "properties": { -# "type": { -# "description": "Port type", -# "enum": ["access", "dot1q", "qinq"], -# }, -# "vlan": { -# "description": "VLAN number", -# "type": "integer", -# "minimum": 1 -# }, -# }, -# }, + # "ports": { + # "type": "object", + # "properties": { + # "type": { + # "description": "Port type", + # "enum": ["access", "dot1q", "qinq"], + # }, + # "vlan": { + # "description": "VLAN number", + # "type": "integer", + # "minimum": 1 + # }, + # }, + # }, }, #"additionalProperties": False, "required": ["id"] diff --git a/gns3server/modules/dynamips/schemas/vm.py b/gns3server/modules/dynamips/schemas/vm.py index ae261ffa..adb380e4 100644 --- a/gns3server/modules/dynamips/schemas/vm.py +++ b/gns3server/modules/dynamips/schemas/vm.py @@ -147,7 +147,7 @@ VM_RELOAD_SCHEMA = { "required": ["id"] } -#TODO: improve platform specific properties (dependencies?) +# TODO: improve platform specific properties (dependencies?) VM_UPDATE_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", "description": "Request validation to update a VM instance", diff --git a/gns3server/modules/iou/__init__.py b/gns3server/modules/iou/__init__.py index 4a6ceec6..04c7e4c0 100644 --- a/gns3server/modules/iou/__init__.py +++ b/gns3server/modules/iou/__init__.py @@ -56,6 +56,7 @@ log = logging.getLogger(__name__) class IOU(IModule): + """ IOU module. @@ -635,7 +636,7 @@ class IOU(IModule): rhost = request["nio"]["rhost"] rport = request["nio"]["rport"] try: - #TODO: handle IPv6 + # TODO: handle IPv6 with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: sock.connect((rhost, rport)) except OSError as e: diff --git a/gns3server/modules/iou/adapters/adapter.py b/gns3server/modules/iou/adapters/adapter.py index 4d2f4053..06645e56 100644 --- a/gns3server/modules/iou/adapters/adapter.py +++ b/gns3server/modules/iou/adapters/adapter.py @@ -17,6 +17,7 @@ class Adapter(object): + """ Base class for adapters. diff --git a/gns3server/modules/iou/adapters/ethernet_adapter.py b/gns3server/modules/iou/adapters/ethernet_adapter.py index 312ef848..bf96362f 100644 --- a/gns3server/modules/iou/adapters/ethernet_adapter.py +++ b/gns3server/modules/iou/adapters/ethernet_adapter.py @@ -19,6 +19,7 @@ from .adapter import Adapter class EthernetAdapter(Adapter): + """ IOU Ethernet adapter. """ diff --git a/gns3server/modules/iou/adapters/serial_adapter.py b/gns3server/modules/iou/adapters/serial_adapter.py index 9f2851a5..ca7d3200 100644 --- a/gns3server/modules/iou/adapters/serial_adapter.py +++ b/gns3server/modules/iou/adapters/serial_adapter.py @@ -19,6 +19,7 @@ from .adapter import Adapter class SerialAdapter(Adapter): + """ IOU Serial adapter. """ diff --git a/gns3server/modules/iou/iou_device.py b/gns3server/modules/iou/iou_device.py index dec395bb..ff8ff2c3 100644 --- a/gns3server/modules/iou/iou_device.py +++ b/gns3server/modules/iou/iou_device.py @@ -43,6 +43,7 @@ log = logging.getLogger(__name__) class IOUDevice(object): + """ IOU device implementation. diff --git a/gns3server/modules/iou/ioucon.py b/gns3server/modules/iou/ioucon.py index cb280fa1..9a0e980e 100644 --- a/gns3server/modules/iou/ioucon.py +++ b/gns3server/modules/iou/ioucon.py @@ -56,22 +56,22 @@ EXIT_ABORT = 2 # Mostly from: # https://code.google.com/p/miniboa/source/browse/trunk/miniboa/telnet.py #--[ Telnet Commands ]--------------------------------------------------------- -SE = 240 # End of sub-negotiation parameters -NOP = 241 # No operation -DATMK = 242 # Data stream portion of a sync. -BREAK = 243 # NVT Character BRK -IP = 244 # Interrupt Process -AO = 245 # Abort Output -AYT = 246 # Are you there -EC = 247 # Erase Character -EL = 248 # Erase Line -GA = 249 # The Go Ahead Signal -SB = 250 # Sub-option to follow -WILL = 251 # Will; request or confirm option begin -WONT = 252 # Wont; deny option request -DO = 253 # Do = Request or confirm remote option -DONT = 254 # Don't = Demand or confirm option halt -IAC = 255 # Interpret as Command +SE = 240 # End of sub-negotiation parameters +NOP = 241 # No operation +DATMK = 242 # Data stream portion of a sync. +BREAK = 243 # NVT Character BRK +IP = 244 # Interrupt Process +AO = 245 # Abort Output +AYT = 246 # Are you there +EC = 247 # Erase Character +EL = 248 # Erase Line +GA = 249 # The Go Ahead Signal +SB = 250 # Sub-option to follow +WILL = 251 # Will; request or confirm option begin +WONT = 252 # Wont; deny option request +DO = 253 # Do = Request or confirm remote option +DONT = 254 # Don't = Demand or confirm option halt +IAC = 255 # Interpret as Command SEND = 1 # Sub-process negotiation SEND command IS = 0 # Sub-process negotiation IS command #--[ Telnet Options ]---------------------------------------------------------- @@ -154,6 +154,7 @@ class FileLock: class Console: + def fileno(self): raise NotImplementedError("Only routers have fileno()") diff --git a/gns3server/modules/iou/nios/nio.py b/gns3server/modules/iou/nios/nio.py index 059d56a3..0c8e610e 100644 --- a/gns3server/modules/iou/nios/nio.py +++ b/gns3server/modules/iou/nios/nio.py @@ -21,6 +21,7 @@ Base interface for NIOs. class NIO(object): + """ Network Input/Output. """ diff --git a/gns3server/modules/iou/nios/nio_generic_ethernet.py b/gns3server/modules/iou/nios/nio_generic_ethernet.py index 068e9fc3..709e6474 100644 --- a/gns3server/modules/iou/nios/nio_generic_ethernet.py +++ b/gns3server/modules/iou/nios/nio_generic_ethernet.py @@ -23,6 +23,7 @@ from .nio import NIO class NIO_GenericEthernet(NIO): + """ Generic Ethernet NIO. diff --git a/gns3server/modules/iou/nios/nio_tap.py b/gns3server/modules/iou/nios/nio_tap.py index 95ec631d..f6b1663f 100644 --- a/gns3server/modules/iou/nios/nio_tap.py +++ b/gns3server/modules/iou/nios/nio_tap.py @@ -23,6 +23,7 @@ from .nio import NIO class NIO_TAP(NIO): + """ TAP NIO. diff --git a/gns3server/modules/iou/nios/nio_udp.py b/gns3server/modules/iou/nios/nio_udp.py index 2c850351..3b25f0c4 100644 --- a/gns3server/modules/iou/nios/nio_udp.py +++ b/gns3server/modules/iou/nios/nio_udp.py @@ -23,6 +23,7 @@ from .nio import NIO class NIO_UDP(NIO): + """ UDP NIO. diff --git a/gns3server/modules/nios/nio_tap.py b/gns3server/modules/nios/nio_tap.py index 85d89990..e533c32a 100644 --- a/gns3server/modules/nios/nio_tap.py +++ b/gns3server/modules/nios/nio_tap.py @@ -21,6 +21,7 @@ Interface for TAP NIOs (UNIX based OSes only). class NIO_TAP(object): + """ TAP NIO. diff --git a/gns3server/modules/nios/nio_udp.py b/gns3server/modules/nios/nio_udp.py index f499ca7e..bc6252bb 100644 --- a/gns3server/modules/nios/nio_udp.py +++ b/gns3server/modules/nios/nio_udp.py @@ -21,6 +21,7 @@ Interface for UDP NIOs. class NIO_UDP(object): + """ UDP NIO. diff --git a/gns3server/modules/port_manager.py b/gns3server/modules/port_manager.py index b0188802..4ea2cac5 100644 --- a/gns3server/modules/port_manager.py +++ b/gns3server/modules/port_manager.py @@ -21,6 +21,7 @@ import asyncio class PortManager: + """ :param host: IP address to bind for console connections """ diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index fa427027..d03f090f 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -21,6 +21,7 @@ from uuid import uuid4 class Project: + """ A project contains a list of VM. In theory VM are isolated project/project. diff --git a/gns3server/modules/project_manager.py b/gns3server/modules/project_manager.py index f2a75e4a..cad199c4 100644 --- a/gns3server/modules/project_manager.py +++ b/gns3server/modules/project_manager.py @@ -20,6 +20,7 @@ from .project import Project class ProjectManager: + """ This singleton keeps track of available projects. """ diff --git a/gns3server/modules/qemu/__init__.py b/gns3server/modules/qemu/__init__.py index beaac4ef..01b3c72e 100644 --- a/gns3server/modules/qemu/__init__.py +++ b/gns3server/modules/qemu/__init__.py @@ -49,6 +49,7 @@ log = logging.getLogger(__name__) class Qemu(IModule): + """ QEMU module. @@ -551,7 +552,7 @@ class Qemu(IModule): rhost = request["nio"]["rhost"] rport = request["nio"]["rport"] try: - #TODO: handle IPv6 + # TODO: handle IPv6 with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: sock.connect((rhost, rport)) except OSError as e: diff --git a/gns3server/modules/qemu/adapters/adapter.py b/gns3server/modules/qemu/adapters/adapter.py index cf439427..ade660f9 100644 --- a/gns3server/modules/qemu/adapters/adapter.py +++ b/gns3server/modules/qemu/adapters/adapter.py @@ -17,6 +17,7 @@ class Adapter(object): + """ Base class for adapters. diff --git a/gns3server/modules/qemu/adapters/ethernet_adapter.py b/gns3server/modules/qemu/adapters/ethernet_adapter.py index 27426ec2..2064bb68 100644 --- a/gns3server/modules/qemu/adapters/ethernet_adapter.py +++ b/gns3server/modules/qemu/adapters/ethernet_adapter.py @@ -19,6 +19,7 @@ from .adapter import Adapter class EthernetAdapter(Adapter): + """ QEMU Ethernet adapter. """ diff --git a/gns3server/modules/qemu/nios/nio.py b/gns3server/modules/qemu/nios/nio.py index eee5f1d5..3c8a6b9e 100644 --- a/gns3server/modules/qemu/nios/nio.py +++ b/gns3server/modules/qemu/nios/nio.py @@ -21,6 +21,7 @@ Base interface for NIOs. class NIO(object): + """ Network Input/Output. """ diff --git a/gns3server/modules/qemu/nios/nio_udp.py b/gns3server/modules/qemu/nios/nio_udp.py index 2c850351..3b25f0c4 100644 --- a/gns3server/modules/qemu/nios/nio_udp.py +++ b/gns3server/modules/qemu/nios/nio_udp.py @@ -23,6 +23,7 @@ from .nio import NIO class NIO_UDP(NIO): + """ UDP NIO. diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index 5ae6fad7..25ce78bb 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -43,6 +43,7 @@ log = logging.getLogger(__name__) class QemuVM(object): + """ QEMU VM implementation. @@ -462,7 +463,6 @@ class QemuVM(object): disk_image=hdb_disk_image)) self._hdb_disk_image = hdb_disk_image - @property def adapters(self): """ @@ -586,7 +586,6 @@ class QemuVM(object): priority=process_priority)) self._process_priority = process_priority - @property def ram(self): """ @@ -999,18 +998,18 @@ class QemuVM(object): if self._legacy_networking: self._control_vm("host_net_remove {} gns3-{}".format(adapter_id, adapter_id)) self._control_vm("host_net_add udp vlan={},name=gns3-{},sport={},dport={},daddr={}".format(adapter_id, - adapter_id, - nio.lport, - nio.rport, - nio.rhost)) + adapter_id, + nio.lport, + nio.rport, + nio.rhost)) else: self._control_vm("host_net_remove {} gns3-{}".format(adapter_id, adapter_id)) self._control_vm("host_net_add socket vlan={},name=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_id, - adapter_id, - nio.rhost, - nio.rport, - self._host, - nio.lport)) + adapter_id, + nio.rhost, + nio.rport, + self._host, + nio.lport)) adapter.add_nio(0, nio) log.info("QEMU VM {name} [id={id}]: {nio} added to adapter {adapter_id}".format(name=self._name, @@ -1154,8 +1153,8 @@ class QemuVM(object): if not os.path.exists(hdb_disk): try: retcode = subprocess.call([qemu_img_path, "create", "-o", - "backing_file={}".format(self._hdb_disk_image), - "-f", "qcow2", hdb_disk]) + "backing_file={}".format(self._hdb_disk_image), + "-f", "qcow2", hdb_disk]) log.info("{} returned with {}".format(qemu_img_path, retcode)) except (OSError, subprocess.SubprocessError) as e: raise QemuError("Could not create disk image {}".format(e)) @@ -1190,24 +1189,24 @@ class QemuVM(object): network_options = [] adapter_id = 0 for adapter in self._ethernet_adapters: - #TODO: let users specify a base mac address + # TODO: let users specify a base mac address mac = "00:00:ab:%02x:%02x:%02d" % (random.randint(0x00, 0xff), random.randint(0x00, 0xff), adapter_id) network_options.extend(["-net", "nic,vlan={},macaddr={},model={}".format(adapter_id, mac, self._adapter_type)]) nio = adapter.get_nio(0) if nio and isinstance(nio, NIO_UDP): if self._legacy_networking: network_options.extend(["-net", "udp,vlan={},name=gns3-{},sport={},dport={},daddr={}".format(adapter_id, - adapter_id, - nio.lport, - nio.rport, - nio.rhost)]) + adapter_id, + nio.lport, + nio.rport, + nio.rhost)]) else: network_options.extend(["-net", "socket,vlan={},name=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_id, - adapter_id, - nio.rhost, - nio.rport, - self._host, - nio.lport)]) + adapter_id, + nio.rhost, + nio.rport, + self._host, + nio.lport)]) else: network_options.extend(["-net", "user,vlan={},name=gns3-{}".format(adapter_id, adapter_id)]) adapter_id += 1 diff --git a/gns3server/modules/virtualbox/telnet_server.py b/gns3server/modules/virtualbox/telnet_server.py index 0ccde367..b5e214a5 100644 --- a/gns3server/modules/virtualbox/telnet_server.py +++ b/gns3server/modules/virtualbox/telnet_server.py @@ -31,6 +31,7 @@ if sys.platform.startswith("win"): class TelnetServer(threading.Thread): + """ Mini Telnet Server. @@ -226,37 +227,38 @@ class TelnetServer(threading.Thread): # Mostly from https://code.google.com/p/miniboa/source/browse/trunk/miniboa/telnet.py # Telnet Commands -SE = 240 # End of sub-negotiation parameters -NOP = 241 # No operation -DATMK = 242 # Data stream portion of a sync. -BREAK = 243 # NVT Character BRK -IP = 244 # Interrupt Process -AO = 245 # Abort Output -AYT = 246 # Are you there -EC = 247 # Erase Character -EL = 248 # Erase Line -GA = 249 # The Go Ahead Signal -SB = 250 # Sub-option to follow -WILL = 251 # Will; request or confirm option begin -WONT = 252 # Wont; deny option request -DO = 253 # Do = Request or confirm remote option -DONT = 254 # Don't = Demand or confirm option halt -IAC = 255 # Interpret as Command -SEND = 1 # Sub-process negotiation SEND command -IS = 0 # Sub-process negotiation IS command +SE = 240 # End of sub-negotiation parameters +NOP = 241 # No operation +DATMK = 242 # Data stream portion of a sync. +BREAK = 243 # NVT Character BRK +IP = 244 # Interrupt Process +AO = 245 # Abort Output +AYT = 246 # Are you there +EC = 247 # Erase Character +EL = 248 # Erase Line +GA = 249 # The Go Ahead Signal +SB = 250 # Sub-option to follow +WILL = 251 # Will; request or confirm option begin +WONT = 252 # Wont; deny option request +DO = 253 # Do = Request or confirm remote option +DONT = 254 # Don't = Demand or confirm option halt +IAC = 255 # Interpret as Command +SEND = 1 # Sub-process negotiation SEND command +IS = 0 # Sub-process negotiation IS command # Telnet Options -BINARY = 0 # Transmit Binary -ECHO = 1 # Echo characters back to sender -RECON = 2 # Reconnection -SGA = 3 # Suppress Go-Ahead -TMARK = 6 # Timing Mark -TTYPE = 24 # Terminal Type -NAWS = 31 # Negotiate About Window Size -LINEMO = 34 # Line Mode +BINARY = 0 # Transmit Binary +ECHO = 1 # Echo characters back to sender +RECON = 2 # Reconnection +SGA = 3 # Suppress Go-Ahead +TMARK = 6 # Timing Mark +TTYPE = 24 # Terminal Type +NAWS = 31 # Negotiate About Window Size +LINEMO = 34 # Line Mode class TelnetClient(object): + """ Represents a Telnet client connection. diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 12dfb934..8fd95bf1 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -45,6 +45,7 @@ log = logging.getLogger(__name__) class VirtualBoxVM(BaseVM): + """ VirtualBox VM implementation. """ @@ -58,7 +59,7 @@ class VirtualBoxVM(BaseVM): self._system_properties = {} - #FIXME: harcoded values + # FIXME: harcoded values if sys.platform.startswith("win"): self._vboxmanage_path = r"C:\Program Files\Oracle\VirtualBox\VBoxManage.exe" else: @@ -367,7 +368,6 @@ class VirtualBoxVM(BaseVM): except OSError as e: raise VirtualBoxError("Could not write HDD info file: {}".format(e)) - log.info("VirtualBox VM {name} [id={id}] has been deleted".format(name=self._name, id=self._id)) @@ -386,9 +386,9 @@ class VirtualBoxVM(BaseVM): if self._linked_clone: self._execute("unregistervm", [self._vmname, "--delete"]) - #try: + # try: # shutil.rmtree(self._working_dir) - #except OSError as e: + # except OSError as e: # log.error("could not delete VirtualBox VM {name} [id={id}]: {error}".format(name=self._name, # id=self._id, # error=e)) diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index a1ffdd57..5184d5e1 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -43,6 +43,7 @@ log = logging.getLogger(__name__) class VPCSVM(BaseVM): + """ VPCS vm implementation. @@ -53,6 +54,7 @@ class VPCSVM(BaseVM): :param working_dir: path to a working directory :param console: TCP console port """ + def __init__(self, name, uuid, project, manager, working_dir=None, console=None): super().__init__(name, uuid, project, manager) diff --git a/gns3server/server.py b/gns3server/server.py index d476a003..e428b508 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -33,7 +33,7 @@ from .config import Config from .modules import MODULES from .modules.port_manager import PortManager -#TODO: get rid of * have something generic to automatically import handlers so the routes can be found +# TODO: get rid of * have something generic to automatically import handlers so the routes can be found from gns3server.handlers import * from gns3server.handlers.virtualbox_handler import VirtualBoxHandler @@ -51,7 +51,7 @@ class Server: self._start_time = time.time() self._port_manager = PortManager(host, console_bind_to_any) - #TODO: server config file support, to be reviewed + # 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() @@ -78,7 +78,7 @@ class Server: Cleanup the modules (shutdown running emulators etc.) """ - #TODO: clean everything from here + # TODO: clean everything from here self._loop.stop() def _signal_handling(self): @@ -133,7 +133,7 @@ class Server: Starts the server. """ - #TODO: SSL support for Rackspace cloud integration (here or with nginx for instance). + # 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(): @@ -148,7 +148,7 @@ class Server: self._loop.run_until_complete(self._run_application(app)) self._signal_handling() - #FIXME: remove it in production or in tests + # FIXME: remove it in production or in tests self._loop.call_later(1, self._reload_hook) try: self._loop.run_forever() diff --git a/gns3server/start_server.py b/gns3server/start_server.py index 952703c0..c603a2b9 100644 --- a/gns3server/start_server.py +++ b/gns3server/start_server.py @@ -80,15 +80,15 @@ def parse_cmd_line(argv): short_args = "dvh" long_args = ("debug", - "ip=", - "verbose", - "help", - "data=", - ) + "ip=", + "verbose", + "help", + "data=", + ) try: opts, extra_opts = getopt.getopt(argv[1:], short_args, long_args) except getopt.GetoptError as e: - print("Unrecognized command line option or missing required argument: %s" %(e)) + print("Unrecognized command line option or missing required argument: %s" % (e)) print(usage) sys.exit(2) @@ -99,7 +99,7 @@ def parse_cmd_line(argv): elif sys.platform == "osx": cmd_line_option_list['syslog'] = "/var/run/syslog" else: - cmd_line_option_list['syslog'] = ('localhost',514) + cmd_line_option_list['syslog'] = ('localhost', 514) for opt, val in opts: if opt in ("-h", "--help"): diff --git a/gns3server/version.py b/gns3server/version.py index ed60edd9..f650a7bf 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -25,4 +25,3 @@ __version__ = "1.3.dev1" __version_info__ = (1, 3, 0, 0) - diff --git a/gns3server/web/documentation.py b/gns3server/web/documentation.py index 2a2d212c..b699f9dd 100644 --- a/gns3server/web/documentation.py +++ b/gns3server/web/documentation.py @@ -22,7 +22,9 @@ from gns3server.web.route import Route class Documentation(object): + """Extract API documentation as Sphinx compatible files""" + def __init__(self, route): self._documentation = route.get_documentation() diff --git a/gns3server/web/response.py b/gns3server/web/response.py index e5cd6912..2a0b3911 100644 --- a/gns3server/web/response.py +++ b/gns3server/web/response.py @@ -38,6 +38,7 @@ class Response(aiohttp.web.Response): :param anwser The response as a Python object """ + def json(self, answer): """Pass a Python object and return a JSON as answer""" diff --git a/gns3server/web/route.py b/gns3server/web/route.py index 086a6b50..4cc6a863 100644 --- a/gns3server/web/route.py +++ b/gns3server/web/route.py @@ -49,6 +49,7 @@ def parse_request(request, input_schema): class Route(object): + """ Decorator adding: * json schema verification * routing inside handlers diff --git a/old_tests/dynamips/test_c1700.py b/old_tests/dynamips/test_c1700.py index 4ed3a2d2..222d5a8f 100644 --- a/old_tests/dynamips/test_c1700.py +++ b/old_tests/dynamips/test_c1700.py @@ -89,7 +89,7 @@ def test_iomem(router_c1700): def test_mac_addr(router_c1700): - assert router_c1700.mac_addr != None + assert router_c1700.mac_addr is not None router_c1700.mac_addr = "aa:aa:aa:aa:aa:aa" assert router_c1700.mac_addr == "aa:aa:aa:aa:aa:aa" diff --git a/old_tests/dynamips/test_c2600.py b/old_tests/dynamips/test_c2600.py index ae94f1b4..53bfb0db 100644 --- a/old_tests/dynamips/test_c2600.py +++ b/old_tests/dynamips/test_c2600.py @@ -165,7 +165,7 @@ def test_iomem(router_c2600): def test_mac_addr(router_c2600): - assert router_c2600.mac_addr != None + assert router_c2600.mac_addr is not None router_c2600.mac_addr = "aa:aa:aa:aa:aa:aa" assert router_c2600.mac_addr == "aa:aa:aa:aa:aa:aa" diff --git a/old_tests/dynamips/test_c2691.py b/old_tests/dynamips/test_c2691.py index 64acc6c1..282b183b 100644 --- a/old_tests/dynamips/test_c2691.py +++ b/old_tests/dynamips/test_c2691.py @@ -29,7 +29,7 @@ def test_iomem(router_c2691): def test_mac_addr(router_c2691): - assert router_c2691.mac_addr != None + assert router_c2691.mac_addr is not None router_c2691.mac_addr = "aa:aa:aa:aa:aa:aa" assert router_c2691.mac_addr == "aa:aa:aa:aa:aa:aa" diff --git a/old_tests/dynamips/test_c3600.py b/old_tests/dynamips/test_c3600.py index 435f1b27..cd05add3 100644 --- a/old_tests/dynamips/test_c3600.py +++ b/old_tests/dynamips/test_c3600.py @@ -60,7 +60,7 @@ def test_iomem(router_c3600): def test_mac_addr(router_c3600): - assert router_c3600.mac_addr != None + assert router_c3600.mac_addr is not None router_c3600.mac_addr = "aa:aa:aa:aa:aa:aa" assert router_c3600.mac_addr == "aa:aa:aa:aa:aa:aa" diff --git a/old_tests/dynamips/test_c3725.py b/old_tests/dynamips/test_c3725.py index a4a923cf..bc3ffcbf 100644 --- a/old_tests/dynamips/test_c3725.py +++ b/old_tests/dynamips/test_c3725.py @@ -29,7 +29,7 @@ def test_iomem(router_c3725): def test_mac_addr(router_c3725): - assert router_c3725.mac_addr != None + assert router_c3725.mac_addr is not None router_c3725.mac_addr = "aa:aa:aa:aa:aa:aa" assert router_c3725.mac_addr == "aa:aa:aa:aa:aa:aa" diff --git a/old_tests/dynamips/test_c3745.py b/old_tests/dynamips/test_c3745.py index c58b5c2e..13d88583 100644 --- a/old_tests/dynamips/test_c3745.py +++ b/old_tests/dynamips/test_c3745.py @@ -29,7 +29,7 @@ def test_iomem(router_c3745): def test_mac_addr(router_c3745): - assert router_c3745.mac_addr != None + assert router_c3745.mac_addr is not None router_c3745.mac_addr = "aa:aa:aa:aa:aa:aa" assert router_c3745.mac_addr == "aa:aa:aa:aa:aa:aa" diff --git a/old_tests/dynamips/test_c7200.py b/old_tests/dynamips/test_c7200.py index 7b74cc7f..48f1eb00 100644 --- a/old_tests/dynamips/test_c7200.py +++ b/old_tests/dynamips/test_c7200.py @@ -57,7 +57,7 @@ def test_power_supplies(router_c7200): def test_mac_addr(router_c7200): - assert router_c7200.mac_addr != None + assert router_c7200.mac_addr is not None router_c7200.mac_addr = "aa:aa:aa:aa:aa:aa" assert router_c7200.mac_addr == "aa:aa:aa:aa:aa:aa" @@ -162,7 +162,7 @@ def test_slot_remove_adapter(router_c7200): adapter = PA_FE_TX() router_c7200.slot_add_binding(1, adapter) router_c7200.slot_remove_binding(1) - assert router_c7200.slots[1] == None + assert router_c7200.slots[1] is None def test_slot_add_remove_nio_binding(router_c7200): diff --git a/old_tests/dynamips/test_hypervisor_manager.py b/old_tests/dynamips/test_hypervisor_manager.py index adaa79a2..c7e42734 100644 --- a/old_tests/dynamips/test_hypervisor_manager.py +++ b/old_tests/dynamips/test_hypervisor_manager.py @@ -11,7 +11,7 @@ def hypervisor_manager(request): print("\nStarting Dynamips Hypervisor: {}".format(dynamips_path)) manager = HypervisorManager(dynamips_path, "/tmp", "127.0.0.1") - #manager.start_new_hypervisor() + # manager.start_new_hypervisor() def stop(): print("\nStopping Dynamips Hypervisor") diff --git a/old_tests/dynamips/test_nios.py b/old_tests/dynamips/test_nios.py index 0538c298..691dac49 100644 --- a/old_tests/dynamips/test_nios.py +++ b/old_tests/dynamips/test_nios.py @@ -133,7 +133,7 @@ def test_reset_stats(hypervisor): def test_set_bandwidth(hypervisor): nio = NIO_Null(hypervisor) - assert nio.bandwidth == None # no constraint by default + assert nio.bandwidth is None # no constraint by default nio.set_bandwidth(1000) # bandwidth = 1000 Kb/s assert nio.bandwidth == 1000 nio.delete() diff --git a/old_tests/dynamips/test_router.py b/old_tests/dynamips/test_router.py index ebf9835c..4b0fd3db 100644 --- a/old_tests/dynamips/test_router.py +++ b/old_tests/dynamips/test_router.py @@ -99,14 +99,14 @@ def test_nvram(router): def test_mmap(router): - assert router.mmap == True # default value + assert router.mmap # default value router.mmap = False assert router.mmap == False def test_sparsemem(router): - assert router.sparsemem == True # default value + assert router.sparsemem # default value router.sparsemem = False assert router.sparsemem == False @@ -209,7 +209,7 @@ def test_get_slot_nio_bindings(router): def test_mac_addr(router): - assert router.mac_addr != None + assert router.mac_addr is not None router.mac_addr = "aa:aa:aa:aa:aa:aa" assert router.mac_addr == "aa:aa:aa:aa:aa:aa" diff --git a/old_tests/dynamips/test_vmhandler.py b/old_tests/dynamips/test_vmhandler.py index cdc4998c..e639d59f 100644 --- a/old_tests/dynamips/test_vmhandler.py +++ b/old_tests/dynamips/test_vmhandler.py @@ -8,56 +8,56 @@ import tempfile # class TestVMHandler(AsyncHTTPTestCase): -# +# # def setUp(self): -# +# # AsyncHTTPTestCase.setUp(self) # self.post_request = partial(self.http_client.fetch, # self.get_url("/api/vms/dynamips"), # self.stop, # method="POST") -# +# # def get_app(self): # return tornado.web.Application(Dynamips().handlers()) -# +# # def test_endpoint(self): # self.http_client.fetch(self.get_url("/api/vms/dynamips"), self.stop) # response = self.wait() # assert response.code == 200 -# +# # def test_upload(self): -# +# # try: # from poster.encode import multipart_encode # except ImportError: # # poster isn't available for Python 3, let's just ignore the test # return -# +# # file_to_upload = tempfile.NamedTemporaryFile() # data, headers = multipart_encode({"file1": file_to_upload}) # body = "" # for d in data: # body += d -# +# # response = self.fetch('/api/vms/dynamips/storage/upload', # headers=headers, # body=body, # method='POST') -# +# # assert response.code == 200 -# +# # def get_new_ioloop(self): # return tornado.ioloop.IOLoop.instance() -# +# # def test_create_vm(self): -# +# # post_data = {"name": "R1", # "platform": "c3725", # "console": 2000, # "aux": 3000, # "image": "c3725.bin", # "ram": 128} -# +# # self.post_request(body=json.dumps(post_data)) # response = self.wait() # assert(response.headers['Content-Type'].startswith('application/json')) diff --git a/old_tests/test_jsonrpc.py b/old_tests/test_jsonrpc.py index 155502c5..eb6920a6 100644 --- a/old_tests/test_jsonrpc.py +++ b/old_tests/test_jsonrpc.py @@ -72,6 +72,7 @@ class JSONRPC(AsyncTestCase): class AsyncWSRequest(TornadoWebSocketClient): + """ Very basic Websocket client for tests """ diff --git a/scripts/ws_client.py b/scripts/ws_client.py index 675bc7c3..9c0911f5 100644 --- a/scripts/ws_client.py +++ b/scripts/ws_client.py @@ -43,4 +43,4 @@ if __name__ == '__main__': ws.connect() ws.run_forever() except KeyboardInterrupt: - ws.close() \ No newline at end of file + ws.close() diff --git a/setup.py b/setup.py index 40a9e409..586d3f32 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ class PyTest(TestCommand): self.test_suite = True def run_tests(self): - #import here, cause outside the eggs aren't loaded + # import here, cause outside the eggs aren't loaded import pytest errcode = pytest.main(self.test_args) sys.exit(errcode) diff --git a/tests/api/base.py b/tests/api/base.py index 85d465e9..ee3d5024 100644 --- a/tests/api/base.py +++ b/tests/api/base.py @@ -36,6 +36,7 @@ from gns3server.modules.project_manager import ProjectManager class Query: + def __init__(self, loop, host='localhost', port=8001): self._loop = loop self._port = port diff --git a/tests/utils.py b/tests/utils.py index ee2aff19..40ef2803 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -20,6 +20,7 @@ from unittest.mock import patch class _asyncio_patch: + """ A wrapper around python patch supporting asyncio. Like the original patch you can use it as context @@ -29,6 +30,7 @@ class _asyncio_patch: inspiration: https://hg.python.org/cpython/file/3.4/Lib/unittest/mock.py """ + def __init__(self, function, *args, **kwargs): self.function = function self.args = args From db41076ce5a4f865ff7464a1ef3ae83b96d966bc Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 20 Jan 2015 14:31:47 +0100 Subject: [PATCH 052/485] Use the project working directory for VPCS VM --- docs/api/examples/post_project.txt | 8 ++++---- gns3server/config.py | 2 +- gns3server/modules/base_vm.py | 17 +++++++++++++++++ gns3server/modules/project.py | 20 +++++++++++++++++++- gns3server/modules/vpcs/vpcs_vm.py | 22 +++++----------------- tests/modules/test_project.py | 10 +++++++++- tests/modules/vpcs/test_vpcs_vm.py | 10 +++++----- 7 files changed, 60 insertions(+), 29 deletions(-) diff --git a/docs/api/examples/post_project.txt b/docs/api/examples/post_project.txt index 3a99e4e6..bc57a383 100644 --- a/docs/api/examples/post_project.txt +++ b/docs/api/examples/post_project.txt @@ -1,8 +1,8 @@ -curl -i -X POST 'http://localhost:8000/project' -d '{"location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-308/test_create_project_with_dir0"}' +curl -i -X POST 'http://localhost:8000/project' -d '{"location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-321/test_create_project_with_dir0"}' POST /project HTTP/1.1 { - "location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-308/test_create_project_with_dir0" + "location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-321/test_create_project_with_dir0" } @@ -15,6 +15,6 @@ SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /project { - "location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-308/test_create_project_with_dir0", - "uuid": "7b9efb50-4909-4dc2-bb61-0bf443874c4c" + "location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-321/test_create_project_with_dir0", + "uuid": "a00bbbdf-4088-4634-816d-513e0428275f" } diff --git a/gns3server/config.py b/gns3server/config.py index 57d61180..f2ef0353 100644 --- a/gns3server/config.py +++ b/gns3server/config.py @@ -118,7 +118,7 @@ class Config(object): :returns: configparser section """ - if section is not in self._config: + if section not in self._config: return self._config["DEFAULT"] return self._config[section] diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index ee232526..c205b00e 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -33,11 +33,13 @@ class BaseVM: # TODO: When delete release console ports + @property def project(self): """Return VM current project""" return self._project + @property def name(self): """ @@ -48,6 +50,7 @@ class BaseVM: return self._name + @name.setter def name(self, new_name): """ @@ -58,6 +61,7 @@ class BaseVM: self._name = new_name + @property def uuid(self): """ @@ -68,6 +72,7 @@ class BaseVM: return self._uuid + @property def manager(self): """ @@ -78,6 +83,16 @@ class BaseVM: return self._manager + + @property + def working_dir(self): + """ + Return VM working directory + """ + + return self._project.vm_working_directory(self._uuid) + + def create(self): """ Creates the VM. @@ -85,6 +100,7 @@ class BaseVM: return + def start(self): """ Starts the VM process. @@ -92,6 +108,7 @@ class BaseVM: raise NotImplementedError + def stop(self): """ Starts the VM process. diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index d03f090f..c6daa461 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -45,23 +45,41 @@ class Project: self._path = os.path.join(self._location, self._uuid) if os.path.exists(self._path) is False: os.mkdir(self._path) - os.mkdir(os.path.join(self._path, "files")) + os.mkdir(os.path.join(self._path, "vms")) + @property def uuid(self): return self._uuid + @property def location(self): return self._location + @property def path(self): return self._path + + def vm_working_directory(self, vm_identifier): + """ + Return a working directory for a specific VM. + If the directory doesn't exist, the directory is created. + + :param vm_identifier: UUID of VM + """ + + path = os.path.join(self._path, 'vms', vm_identifier) + if os.path.exists(path) is False: + os.mkdir(path) + return path + + def __json__(self): return { diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index 5184d5e1..a29aa32c 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -51,11 +51,10 @@ class VPCSVM(BaseVM): :param uuid: VPCS instance UUID :param project: Project instance :param manager: parent VM Manager - :param working_dir: path to a working directory :param console: TCP console port """ - def __init__(self, name, uuid, project, manager, working_dir=None, console=None): + def __init__(self, name, uuid, project, manager, console=None): super().__init__(name, uuid, project, manager) @@ -63,9 +62,6 @@ class VPCSVM(BaseVM): self._console = console - # TODO: remove working_dir - self._working_dir = "/tmp" - self._command = [] self._process = None self._vpcs_stdout_file = "" @@ -75,14 +71,6 @@ class VPCSVM(BaseVM): self._script_file = "" self._ethernet_adapter = EthernetAdapter() # one adapter with 1 Ethernet interface - # working_dir_path = os.path.join(working_dir, "vpcs", "pc-{}".format(self._id)) - # - # if vpcs_id and not os.path.isdir(working_dir_path): - # raise VPCSError("Working directory {} doesn't exist".format(working_dir_path)) - # - # # create the vm own working directory - # self.working_dir = working_dir_path - # try: if not self._console: self._console = self._manager.port_manager.get_free_console_port() @@ -133,7 +121,7 @@ class VPCSVM(BaseVM): if self._script_file: # update the startup.vpc - config_path = os.path.join(self._working_dir, "startup.vpc") + config_path = os.path.join(self.working_dir, "startup.vpc") if os.path.isfile(config_path): try: with open(config_path, "r+", errors="replace") as f: @@ -155,7 +143,7 @@ class VPCSVM(BaseVM): """ # TODO: should be async try: - output = subprocess.check_output([self._path, "-v"], cwd=self._working_dir) + output = subprocess.check_output([self._path, "-v"], cwd=self.working_dir) match = re.search("Welcome to Virtual PC Simulator, version ([0-9a-z\.]+)", output.decode("utf-8")) if match: version = match.group(1) @@ -179,7 +167,7 @@ class VPCSVM(BaseVM): self._command = self._build_command() try: log.info("starting VPCS: {}".format(self._command)) - self._vpcs_stdout_file = os.path.join(self._working_dir, "vpcs.log") + self._vpcs_stdout_file = os.path.join(self.working_dir, "vpcs.log") log.info("logging to {}".format(self._vpcs_stdout_file)) flags = 0 if sys.platform.startswith("win32"): @@ -188,7 +176,7 @@ class VPCSVM(BaseVM): self._process = yield from asyncio.create_subprocess_exec(*self._command, stdout=fd, stderr=subprocess.STDOUT, - cwd=self._working_dir, + cwd=self.working_dir, creationflags=flags) log.info("VPCS instance {} started PID={}".format(self.name, self._process.pid)) self._started = True diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py index 8ecbee42..23b4055f 100644 --- a/tests/modules/test_project.py +++ b/tests/modules/test_project.py @@ -32,7 +32,7 @@ def test_path(tmpdir): p = Project(location=str(tmpdir)) assert p.path == os.path.join(str(tmpdir), p.uuid) assert os.path.exists(os.path.join(str(tmpdir), p.uuid)) - assert os.path.exists(os.path.join(str(tmpdir), p.uuid, 'files')) + assert os.path.exists(os.path.join(str(tmpdir), p.uuid, 'vms')) def test_temporary_path(): @@ -43,3 +43,11 @@ def test_temporary_path(): def test_json(tmpdir): p = Project() assert p.__json__() == {"location": p.location, "uuid": p.uuid} + + +def test_vm_working_directory(tmpdir): + p = Project(location=str(tmpdir)) + assert os.path.exists(p.vm_working_directory('00010203-0405-0607-0809-0a0b0c0d0e0f')) + assert os.path.exists(os.path.join(str(tmpdir), p.uuid, 'vms', '00010203-0405-0607-0809-0a0b0c0d0e0f')) + + diff --git a/tests/modules/vpcs/test_vpcs_vm.py b/tests/modules/vpcs/test_vpcs_vm.py index 57cf71d8..08f80be4 100644 --- a/tests/modules/vpcs/test_vpcs_vm.py +++ b/tests/modules/vpcs/test_vpcs_vm.py @@ -37,7 +37,7 @@ def manager(): @patch("subprocess.check_output", return_value="Welcome to Virtual PC Simulator, version 0.6".encode("utf-8")) -def test_vm(manager): +def test_vm(project, manager): vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) assert vm.name == "test" assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0f" @@ -81,20 +81,20 @@ def test_stop(project, loop, manager): process.terminate.assert_called_with() -def test_add_nio_binding_udp(manager): +def test_add_nio_binding_udp(manager, project): vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) assert nio.lport == 4242 def test_add_nio_binding_tap(project, manager): - vm = VPCSVM("test", 42, project, manager) + vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) with patch("gns3server.modules.vpcs.vpcs_vm.has_privileged_access", return_value=True): nio = vm.port_add_nio_binding(0, {"type": "nio_tap", "tap_device": "test"}) assert nio.tap_device == "test" -def test_add_nio_binding_tap_no_privileged_access(manager): +def test_add_nio_binding_tap_no_privileged_access(manager, project): vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) with patch("gns3server.modules.vpcs.vpcs_vm.has_privileged_access", return_value=False): with pytest.raises(VPCSError): @@ -102,7 +102,7 @@ def test_add_nio_binding_tap_no_privileged_access(manager): assert vm._ethernet_adapter.ports[0] is None -def test_port_remove_nio_binding(manager): +def test_port_remove_nio_binding(manager, project): vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) vm.port_remove_nio_binding(0) From 4488cc39607d7e7a47ff6bd3aaa8c21144cb5e24 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 20 Jan 2015 14:59:19 +0100 Subject: [PATCH 053/485] Colored logs --- gns3server/main.py | 10 ++--- gns3server/server.py | 12 +++--- gns3server/web/logger.py | 86 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 13 deletions(-) create mode 100644 gns3server/web/logger.py diff --git a/gns3server/main.py b/gns3server/main.py index 65993058..70bdacf1 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -22,6 +22,7 @@ import sys import locale from gns3server.server import Server +from gns3server.web.logger import init_logger from gns3server.version import __version__ import logging @@ -85,12 +86,7 @@ def main(): # user_log.addHandler(stream_handler) # user_log.propagate = False # END OLD LOG CODE - root_log = logging.getLogger() - root_log.setLevel(logging.DEBUG) - console_log = logging.StreamHandler(sys.stdout) - console_log.setLevel(logging.DEBUG) - root_log.addHandler(console_log) - user_log = root_log + user_log = init_logger(logging.DEBUG, quiet=False) # FIXME END Temporary user_log.info("GNS3 server version {}".format(__version__)) @@ -111,7 +107,7 @@ def main(): try: os.getcwd() except FileNotFoundError: - log.critical("the current working directory doesn't exist") + log.critical("The current working directory doesn't exist") return # TODO: Renable console_bind_to_any when we will have command line parsing diff --git a/gns3server/server.py b/gns3server/server.py index e428b508..56a6bf70 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -84,7 +84,7 @@ class Server: def _signal_handling(self): def signal_handler(signame): - log.warning("server has got signal {}, exiting...".format(signame)) + log.warning("Server has got signal {}, exiting...".format(signame)) self._stop_application() signals = ["SIGTERM", "SIGINT"] @@ -105,7 +105,7 @@ class Server: def reload(): - log.info("reloading") + log.info("Reloading") self._stop_application() os.execv(sys.executable, [sys.executable] + sys.argv) @@ -124,7 +124,7 @@ class Server: path = path[:-1] modified = os.stat(path).st_mtime if modified > self._start_time: - log.debug("file {} has been modified".format(path)) + log.debug("File {} has been modified".format(path)) reload() self._loop.call_later(1, self._reload_hook) @@ -137,14 +137,14 @@ class Server: 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)) + 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__)) + log.debug("Loading module {}".format(module.__name__)) m = module.instance() m.port_manager = self._port_manager - log.info("starting server on {}:{}".format(self._host, self._port)) + log.info("Starting server on {}:{}".format(self._host, self._port)) self._loop.run_until_complete(self._run_application(app)) self._signal_handling() diff --git a/gns3server/web/logger.py b/gns3server/web/logger.py new file mode 100644 index 00000000..09af48df --- /dev/null +++ b/gns3server/web/logger.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python +# -*- 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 . + +"""Provide a pretty logging on console""" + + +import logging +import sys + + +class ColouredFormatter(logging.Formatter): + RESET = '\x1B[0m' + RED = '\x1B[31m' + YELLOW = '\x1B[33m' + GREEN = '\x1B[32m' + PINK = '\x1b[35m' + + def format(self, record, colour=False): + message = super().format(record) + + if not colour: + return message + + level_no = record.levelno + if level_no >= logging.CRITICAL: + colour = self.RED + elif level_no >= logging.ERROR: + colour = self.RED + elif level_no >= logging.WARNING: + colour = self.YELLOW + elif level_no >= logging.INFO: + colour = self.GREEN + elif level_no >= logging.DEBUG: + colour = self.PINK + else: + colour = self.RESET + + message = '{colour}{message}{reset}'.format(colour=colour, message=message, reset=self.RESET) + + return message + + +class ColouredStreamHandler(logging.StreamHandler): + def format(self, record, colour=False): + if not isinstance(self.formatter, ColouredFormatter): + self.formatter = ColouredFormatter() + + return self.formatter.format(record, colour) + + def emit(self, record): + stream = self.stream + try: + msg = self.format(record, stream.isatty()) + stream.write(msg) + stream.write(self.terminator) + self.flush() + except Exception: + self.handleError(record) + + +def init_logger(level,quiet=False): + + stream_handler = ColouredStreamHandler(sys.stdout) + stream_handler.formatter = ColouredFormatter("{asctime} {levelname:8} {filename}:{lineno} {message}", "%Y-%m-%d %H:%M:%S", "{") + if quiet: + stream_handler.addFilter(logging.Filter(name="user_facing")) + logging.getLogger('user_facing').propagate = False + logging.basicConfig(level=level, handlers=[stream_handler]) + return logging.getLogger('user_facing') + + From 0b97509a741d97a9e2cfe65e38f977d16c17fa0f Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 20 Jan 2015 15:18:57 +0100 Subject: [PATCH 054/485] Do not color logger message --- gns3server/web/logger.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gns3server/web/logger.py b/gns3server/web/logger.py index 09af48df..0c6575e4 100644 --- a/gns3server/web/logger.py +++ b/gns3server/web/logger.py @@ -34,7 +34,7 @@ class ColouredFormatter(logging.Formatter): message = super().format(record) if not colour: - return message + return message.replace("#RESET#", "") level_no = record.levelno if level_no >= logging.CRITICAL: @@ -50,6 +50,7 @@ class ColouredFormatter(logging.Formatter): else: colour = self.RESET + message = message.replace("#RESET#", self.RESET) message = '{colour}{message}{reset}'.format(colour=colour, message=message, reset=self.RESET) return message @@ -76,7 +77,7 @@ class ColouredStreamHandler(logging.StreamHandler): def init_logger(level,quiet=False): stream_handler = ColouredStreamHandler(sys.stdout) - stream_handler.formatter = ColouredFormatter("{asctime} {levelname:8} {filename}:{lineno} {message}", "%Y-%m-%d %H:%M:%S", "{") + stream_handler.formatter = ColouredFormatter("{asctime} {levelname:8} {filename}:{lineno}#RESET# {message}", "%Y-%m-%d %H:%M:%S", "{") if quiet: stream_handler.addFilter(logging.Filter(name="user_facing")) logging.getLogger('user_facing').propagate = False From f5ac73d1ca6740b657de7f8cc9078c01a9f6721c Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 20 Jan 2015 15:21:13 +0100 Subject: [PATCH 055/485] Fix documentation generation --- docs/api/examples/post_project.txt | 8 +- docs/api/project.rst | 39 +++++++++ docs/api/version.rst | 46 ++++++++-- docs/api/vpcs.rst | 33 ++++---- docs/api/vpcsuuidportsportidnio.rst | 48 +++++++++++ docs/api/vpcsuuidstart.rst | 19 +++++ docs/api/vpcsuuidstop.rst | 19 +++++ docs/api/vpcsvpcsid.rst | 63 -------------- docs/api/vpcsvpcsidnio.rst | 127 ---------------------------- gns3server/web/documentation.py | 12 +-- scripts/documentation.sh | 8 +- 11 files changed, 197 insertions(+), 225 deletions(-) create mode 100644 docs/api/project.rst create mode 100644 docs/api/vpcsuuidportsportidnio.rst create mode 100644 docs/api/vpcsuuidstart.rst create mode 100644 docs/api/vpcsuuidstop.rst delete mode 100644 docs/api/vpcsvpcsid.rst delete mode 100644 docs/api/vpcsvpcsidnio.rst diff --git a/docs/api/examples/post_project.txt b/docs/api/examples/post_project.txt index bc57a383..8054c97f 100644 --- a/docs/api/examples/post_project.txt +++ b/docs/api/examples/post_project.txt @@ -1,8 +1,8 @@ -curl -i -X POST 'http://localhost:8000/project' -d '{"location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-321/test_create_project_with_dir0"}' +curl -i -X POST 'http://localhost:8000/project' -d '{"location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-328/test_create_project_with_dir0"}' POST /project HTTP/1.1 { - "location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-321/test_create_project_with_dir0" + "location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-328/test_create_project_with_dir0" } @@ -15,6 +15,6 @@ SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /project { - "location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-321/test_create_project_with_dir0", - "uuid": "a00bbbdf-4088-4634-816d-513e0428275f" + "location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-328/test_create_project_with_dir0", + "uuid": "b4432fe3-6743-4ce7-ab05-1f5637d04cd6" } diff --git a/docs/api/project.rst b/docs/api/project.rst new file mode 100644 index 00000000..00e354f2 --- /dev/null +++ b/docs/api/project.rst @@ -0,0 +1,39 @@ +/project +--------------------------------------------- + +.. contents:: + +POST /project +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Create a project on the server + +Response status codes +********************** +- **200**: OK + +Input +******* +.. raw:: html + + + + + +
Name Mandatory Type Description
location string Base directory where the project should be created on remote server
uuid string Project UUID
+ +Output +******* +.. raw:: html + + + + + +
Name Mandatory Type Description
location string Base directory where the project should be created on remote server
uuid string Project UUID
+ +Sample session +*************** + + +.. literalinclude:: examples/post_project.txt + diff --git a/docs/api/version.rst b/docs/api/version.rst index a5c86f19..66da7325 100644 --- a/docs/api/version.rst +++ b/docs/api/version.rst @@ -1,14 +1,14 @@ /version ------------------------------- +--------------------------------------------- .. contents:: GET /version -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Retrieve server version number +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Retrieve the server version number Response status codes -************************** +********************** - **200**: OK Output @@ -16,8 +16,8 @@ Output .. raw:: html - - + +
NameMandatoryTypeDescription
versionstringVersion number human readable
Name Mandatory Type Description
version string Version number human readable
Sample session @@ -26,3 +26,37 @@ Sample session .. literalinclude:: examples/get_version.txt + +POST /version +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Check if version is the same as the server + +Response status codes +********************** +- **200**: Same version +- **409**: Invalid version + +Input +******* +.. raw:: html + + + + +
Name Mandatory Type Description
version string Version number human readable
+ +Output +******* +.. raw:: html + + + + +
Name Mandatory Type Description
version string Version number human readable
+ +Sample session +*************** + + +.. literalinclude:: examples/post_version.txt + diff --git a/docs/api/vpcs.rst b/docs/api/vpcs.rst index b800f755..225990bb 100644 --- a/docs/api/vpcs.rst +++ b/docs/api/vpcs.rst @@ -1,19 +1,15 @@ /vpcs ------------------------------- +--------------------------------------------- .. contents:: POST /vpcs -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Create a new VPCS and return it - -Parameters -********** -- **vpcs_id**: Id of VPCS instance +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Create a new VPCS instance Response status codes -************************** -- **201**: Success of creation of VPCS +********************** +- **201**: VPCS instance created - **409**: Conflict Input @@ -21,10 +17,12 @@ Input .. raw:: html - - - - + + + + + +
NameMandatoryTypeDescription
console integerconsole TCP port
namestringVPCS device name
vpcs_id integerVPCS device instance ID
Name Mandatory Type Description
console integer console TCP port
name string VPCS device name
project_uuid string Project UUID
uuid string VPCS device UUID
vpcs_id integer VPCS device instance ID (for project created before GNS3 1.3)
Output @@ -32,10 +30,11 @@ Output .. raw:: html - - - - + + + + +
NameMandatoryTypeDescription
consoleintegerconsole TCP port
namestringVPCS device name
vpcs_idintegerVPCS device instance ID
Name Mandatory Type Description
console integer console TCP port
name string VPCS device name
project_uuid string Project UUID
uuid string VPCS device UUID
Sample session diff --git a/docs/api/vpcsuuidportsportidnio.rst b/docs/api/vpcsuuidportsportidnio.rst new file mode 100644 index 00000000..03f2deaf --- /dev/null +++ b/docs/api/vpcsuuidportsportidnio.rst @@ -0,0 +1,48 @@ +/vpcs/{uuid}/ports/{port_id}/nio +--------------------------------------------- + +.. contents:: + +POST /vpcs/{uuid}/ports/{port_id}/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Add a NIO to a VPCS + +Parameters +********** +- **port_id**: Id of the port where the nio should be add +- **uuid**: VPCS instance UUID + +Response status codes +********************** +- **400**: Invalid VPCS instance UUID +- **201**: NIO created +- **404**: VPCS instance doesn't exist + +Sample session +*************** + + +.. literalinclude:: examples/post_vpcsuuidportsportidnio.txt + + +DELETE /vpcs/{uuid}/ports/{port_id}/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Remove a NIO from a VPCS + +Parameters +********** +- **port_id**: ID of the port where the nio should be removed +- **uuid**: VPCS instance UUID + +Response status codes +********************** +- **200**: NIO deleted +- **400**: Invalid VPCS instance UUID +- **404**: VPCS instance doesn't exist + +Sample session +*************** + + +.. literalinclude:: examples/delete_vpcsuuidportsportidnio.txt + diff --git a/docs/api/vpcsuuidstart.rst b/docs/api/vpcsuuidstart.rst new file mode 100644 index 00000000..a55d296b --- /dev/null +++ b/docs/api/vpcsuuidstart.rst @@ -0,0 +1,19 @@ +/vpcs/{uuid}/start +--------------------------------------------- + +.. contents:: + +POST /vpcs/{uuid}/start +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Start a VPCS instance + +Parameters +********** +- **uuid**: VPCS instance UUID + +Response status codes +********************** +- **400**: Invalid VPCS instance UUID +- **404**: VPCS instance doesn't exist +- **204**: VPCS instance started + diff --git a/docs/api/vpcsuuidstop.rst b/docs/api/vpcsuuidstop.rst new file mode 100644 index 00000000..ceff8f62 --- /dev/null +++ b/docs/api/vpcsuuidstop.rst @@ -0,0 +1,19 @@ +/vpcs/{uuid}/stop +--------------------------------------------- + +.. contents:: + +POST /vpcs/{uuid}/stop +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Stop a VPCS instance + +Parameters +********** +- **uuid**: VPCS instance UUID + +Response status codes +********************** +- **400**: Invalid VPCS instance UUID +- **404**: VPCS instance doesn't exist +- **204**: VPCS instance stopped + diff --git a/docs/api/vpcsvpcsid.rst b/docs/api/vpcsvpcsid.rst deleted file mode 100644 index 20db8f9f..00000000 --- a/docs/api/vpcsvpcsid.rst +++ /dev/null @@ -1,63 +0,0 @@ -/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 deleted file mode 100644 index 14a9a6db..00000000 --- a/docs/api/vpcsvpcsidnio.rst +++ /dev/null @@ -1,127 +0,0 @@ -/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/gns3server/web/documentation.py b/gns3server/web/documentation.py index b699f9dd..2c165f8b 100644 --- a/gns3server/web/documentation.py +++ b/gns3server/web/documentation.py @@ -18,6 +18,7 @@ import re import os.path +from gns3server.handlers import * from gns3server.web.route import Route @@ -33,11 +34,11 @@ class Documentation(object): 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('{}\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') f.write('{}\n\n'.format(method["description"])) if len(method["parameters"]) > 0: @@ -47,7 +48,7 @@ class Documentation(object): f.write("- **{}**: {}\n".format(parameter, desc)) f.write("\n") - f.write("Response status codes\n*******************\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)) @@ -56,11 +57,11 @@ class Documentation(object): 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"]) + 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._write_json_schema(f, method["output_schema"]) self._include_query_example(f, method, path) @@ -130,4 +131,5 @@ class Documentation(object): if __name__ == '__main__': + print("Generate API documentation") Documentation(Route).write() diff --git a/scripts/documentation.sh b/scripts/documentation.sh index 92d458f7..0ee22a9a 100755 --- a/scripts/documentation.sh +++ b/scripts/documentation.sh @@ -21,7 +21,9 @@ set -e -py.test -python3 ../gns3server/web/documentation.py -cd ../docs +echo "WARNING: This script should be run at the root directory of the project" + +py.test -v +python3 gns3server/web/documentation.py +cd docs make html From 78237e9fb65bb4ef6e328397efd9baa4d2284d8d Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 20 Jan 2015 15:31:27 +0100 Subject: [PATCH 056/485] Bold parameter in documentation in order to improve readability --- docs/api/examples/post_project.txt | 8 ++++---- docs/api/vpcsuuidportsportidnio.rst | 8 ++++---- docs/api/vpcsuuidstart.rst | 2 +- docs/api/vpcsuuidstop.rst | 2 +- gns3server/web/documentation.py | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/api/examples/post_project.txt b/docs/api/examples/post_project.txt index 8054c97f..0f030d15 100644 --- a/docs/api/examples/post_project.txt +++ b/docs/api/examples/post_project.txt @@ -1,8 +1,8 @@ -curl -i -X POST 'http://localhost:8000/project' -d '{"location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-328/test_create_project_with_dir0"}' +curl -i -X POST 'http://localhost:8000/project' -d '{"location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-330/test_create_project_with_dir0"}' POST /project HTTP/1.1 { - "location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-328/test_create_project_with_dir0" + "location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-330/test_create_project_with_dir0" } @@ -15,6 +15,6 @@ SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /project { - "location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-328/test_create_project_with_dir0", - "uuid": "b4432fe3-6743-4ce7-ab05-1f5637d04cd6" + "location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-330/test_create_project_with_dir0", + "uuid": "e7d2911f-e367-46d9-b426-25663d0bb601" } diff --git a/docs/api/vpcsuuidportsportidnio.rst b/docs/api/vpcsuuidportsportidnio.rst index 03f2deaf..ef1c7278 100644 --- a/docs/api/vpcsuuidportsportidnio.rst +++ b/docs/api/vpcsuuidportsportidnio.rst @@ -3,14 +3,14 @@ .. contents:: -POST /vpcs/{uuid}/ports/{port_id}/nio +POST /vpcs/**{uuid}**/ports/**{port_id}**/nio ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Add a NIO to a VPCS Parameters ********** -- **port_id**: Id of the port where the nio should be add - **uuid**: VPCS instance UUID +- **port_id**: Id of the port where the nio should be add Response status codes ********************** @@ -25,14 +25,14 @@ Sample session .. literalinclude:: examples/post_vpcsuuidportsportidnio.txt -DELETE /vpcs/{uuid}/ports/{port_id}/nio +DELETE /vpcs/**{uuid}**/ports/**{port_id}**/nio ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Remove a NIO from a VPCS Parameters ********** -- **port_id**: ID of the port where the nio should be removed - **uuid**: VPCS instance UUID +- **port_id**: ID of the port where the nio should be removed Response status codes ********************** diff --git a/docs/api/vpcsuuidstart.rst b/docs/api/vpcsuuidstart.rst index a55d296b..bcf1f8ea 100644 --- a/docs/api/vpcsuuidstart.rst +++ b/docs/api/vpcsuuidstart.rst @@ -3,7 +3,7 @@ .. contents:: -POST /vpcs/{uuid}/start +POST /vpcs/**{uuid}**/start ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Start a VPCS instance diff --git a/docs/api/vpcsuuidstop.rst b/docs/api/vpcsuuidstop.rst index ceff8f62..16702e07 100644 --- a/docs/api/vpcsuuidstop.rst +++ b/docs/api/vpcsuuidstop.rst @@ -3,7 +3,7 @@ .. contents:: -POST /vpcs/{uuid}/stop +POST /vpcs/**{uuid}**/stop ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Stop a VPCS instance diff --git a/gns3server/web/documentation.py b/gns3server/web/documentation.py index 2c165f8b..35aee835 100644 --- a/gns3server/web/documentation.py +++ b/gns3server/web/documentation.py @@ -37,7 +37,7 @@ class Documentation(object): 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{} {}\n'.format(method["method"], path.replace("{", '**{').replace("}", "}**"))) f.write('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n') f.write('{}\n\n'.format(method["description"])) From 531265ecedd4759397c74115d5e29fbef717bc40 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 20 Jan 2015 15:35:46 +0100 Subject: [PATCH 057/485] Get a stable example between tests for project creation --- docs/api/examples/post_project.txt | 11 ++++++----- tests/api/test_project.py | 22 +++++++++++++++------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/docs/api/examples/post_project.txt b/docs/api/examples/post_project.txt index 0f030d15..68d9d07c 100644 --- a/docs/api/examples/post_project.txt +++ b/docs/api/examples/post_project.txt @@ -1,20 +1,21 @@ -curl -i -X POST 'http://localhost:8000/project' -d '{"location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-330/test_create_project_with_dir0"}' +curl -i -X POST 'http://localhost:8000/project' -d '{"location": "/tmp", "uuid": "00010203-0405-0607-0809-0a0b0c0d0e0f"}' POST /project HTTP/1.1 { - "location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-330/test_create_project_with_dir0" + "location": "/tmp", + "uuid": "00010203-0405-0607-0809-0a0b0c0d0e0f" } HTTP/1.1 200 CONNECTION: close -CONTENT-LENGTH: 171 +CONTENT-LENGTH: 78 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /project { - "location": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-330/test_create_project_with_dir0", - "uuid": "e7d2911f-e367-46d9-b426-25663d0bb601" + "location": "/tmp", + "uuid": "00010203-0405-0607-0809-0a0b0c0d0e0f" } diff --git a/tests/api/test_project.py b/tests/api/test_project.py index 13aeeabf..e4bea217 100644 --- a/tests/api/test_project.py +++ b/tests/api/test_project.py @@ -26,20 +26,28 @@ from gns3server.version import __version__ def test_create_project_with_dir(server, tmpdir): - response = server.post('/project', {"location": str(tmpdir)}, example=True) + response = server.post("/project", {"location": str(tmpdir)}) assert response.status == 200 - assert response.json['location'] == str(tmpdir) + assert response.json["location"] == str(tmpdir) def test_create_project_without_dir(server): query = {} - response = server.post('/project', query) + response = server.post("/project", query) assert response.status == 200 - assert response.json['uuid'] is not None + assert response.json["uuid"] is not None def test_create_project_with_uuid(server): - query = {'uuid': '00010203-0405-0607-0809-0a0b0c0d0e0f'} - response = server.post('/project', query) + query = {"uuid": "00010203-0405-0607-0809-0a0b0c0d0e0f"} + response = server.post("/project", query) assert response.status == 200 - assert response.json['uuid'] is not None + assert response.json["uuid"] == "00010203-0405-0607-0809-0a0b0c0d0e0f" + +def test_create_project_with_uuid(server): + query = {"uuid": "00010203-0405-0607-0809-0a0b0c0d0e0f", "location": "/tmp"} + response = server.post("/project", query, example=True) + assert response.status == 200 + assert response.json["uuid"] == "00010203-0405-0607-0809-0a0b0c0d0e0f" + assert response.json["location"] == "/tmp" + From 7cf409c392aa482fb5b1102d728e9c39adfe6639 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 20 Jan 2015 16:24:46 +0100 Subject: [PATCH 058/485] Kill VPCS process when the server exit --- gns3server/modules/vpcs/vpcs_vm.py | 38 ++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index a29aa32c..7de8216c 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -28,6 +28,7 @@ import re import asyncio import socket import shutil +import atexit from pkg_resources import parse_version from .vpcs_error import VPCSError @@ -81,6 +82,11 @@ class VPCSVM(BaseVM): self._check_requirements() + + def __del__(self): + self._kill_process() + + def _check_requirements(self): """ Check if VPCS is available with the correct version @@ -166,9 +172,9 @@ class VPCSVM(BaseVM): self._command = self._build_command() try: - log.info("starting VPCS: {}".format(self._command)) + log.info("Starting VPCS: {}".format(self._command)) self._vpcs_stdout_file = os.path.join(self.working_dir, "vpcs.log") - log.info("logging to {}".format(self._vpcs_stdout_file)) + log.info("Logging to {}".format(self._vpcs_stdout_file)) flags = 0 if sys.platform.startswith("win32"): flags = subprocess.CREATE_NEW_PROCESS_GROUP @@ -178,12 +184,14 @@ class VPCSVM(BaseVM): stderr=subprocess.STDOUT, cwd=self.working_dir, creationflags=flags) + #atexit(self._kill_process) # Ensure we don't leave orphan process log.info("VPCS instance {} started PID={}".format(self.name, self._process.pid)) self._started = True except (OSError, subprocess.SubprocessError) as e: vpcs_stdout = self.read_vpcs_stdout() - log.error("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout)) - raise VPCSError("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout)) + log.error("Could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout)) + raise VPCSError("Could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout)) + @asyncio.coroutine def stop(self): @@ -193,17 +201,27 @@ class VPCSVM(BaseVM): # stop the VPCS process if self.is_running(): - log.info("stopping VPCS instance {} PID={}".format(self.name, self._process.pid)) - if sys.platform.startswith("win32"): - self._process.send_signal(signal.CTRL_BREAK_EVENT) - else: - self._process.terminate() - + self._kill_process() yield from self._process.wait() self._process = None self._started = False + def _kill_process(self): + """Kill the process if running""" + + if self._process: + log.info("Stopping VPCS instance {} PID={}".format(self.name, self._process.pid)) + if sys.platform.startswith("win32"): + self._process.send_signal(signal.CTRL_BREAK_EVENT) + else: + try: + self._process.terminate() + # Sometime the process can already be dead when we garbage collect + except ProcessLookupError: + pass + + def read_vpcs_stdout(self): """ Reads the standard output of the VPCS process. From bbee5f90a06ceb3955028bdf389e484a6380c166 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 20 Jan 2015 16:37:18 +0100 Subject: [PATCH 059/485] Yet another PEP 8 :) --- gns3dms/modules/rackspace_cloud.py | 2 +- gns3server/modules/base_vm.py | 9 --------- gns3server/modules/project.py | 5 ----- gns3server/modules/virtualbox/virtualbox_vm.py | 4 ++-- gns3server/modules/vpcs/vpcs_vm.py | 6 ------ gns3server/{modules => old_modules}/dynamips/__init__.py | 0 .../dynamips/adapters/__init__.py | 0 .../dynamips/adapters/adapter.py | 0 .../dynamips/adapters/c1700_mb_1fe.py | 0 .../dynamips/adapters/c1700_mb_wic1.py | 0 .../dynamips/adapters/c2600_mb_1e.py | 0 .../dynamips/adapters/c2600_mb_1fe.py | 0 .../dynamips/adapters/c2600_mb_2e.py | 0 .../dynamips/adapters/c2600_mb_2fe.py | 0 .../dynamips/adapters/c7200_io_2fe.py | 0 .../dynamips/adapters/c7200_io_fe.py | 0 .../dynamips/adapters/c7200_io_ge_e.py | 0 .../dynamips/adapters/gt96100_fe.py | 0 .../dynamips/adapters/leopard_2fe.py | 0 .../dynamips/adapters/nm_16esw.py | 0 .../{modules => old_modules}/dynamips/adapters/nm_1e.py | 0 .../dynamips/adapters/nm_1fe_tx.py | 0 .../{modules => old_modules}/dynamips/adapters/nm_4e.py | 0 .../{modules => old_modules}/dynamips/adapters/nm_4t.py | 0 .../dynamips/adapters/pa_2fe_tx.py | 0 .../{modules => old_modules}/dynamips/adapters/pa_4e.py | 0 .../{modules => old_modules}/dynamips/adapters/pa_4t.py | 0 .../{modules => old_modules}/dynamips/adapters/pa_8e.py | 0 .../{modules => old_modules}/dynamips/adapters/pa_8t.py | 0 .../{modules => old_modules}/dynamips/adapters/pa_a1.py | 0 .../dynamips/adapters/pa_fe_tx.py | 0 .../{modules => old_modules}/dynamips/adapters/pa_ge.py | 0 .../dynamips/adapters/pa_pos_oc3.py | 0 .../dynamips/adapters/wic_1enet.py | 0 .../{modules => old_modules}/dynamips/adapters/wic_1t.py | 0 .../{modules => old_modules}/dynamips/adapters/wic_2t.py | 0 .../dynamips/backends/__init__.py | 0 .../{modules => old_modules}/dynamips/backends/atmsw.py | 0 .../{modules => old_modules}/dynamips/backends/ethhub.py | 0 .../{modules => old_modules}/dynamips/backends/ethsw.py | 0 .../{modules => old_modules}/dynamips/backends/frsw.py | 0 .../{modules => old_modules}/dynamips/backends/vm.py | 0 .../{modules => old_modules}/dynamips/dynamips_error.py | 0 .../dynamips/dynamips_hypervisor.py | 0 .../{modules => old_modules}/dynamips/hypervisor.py | 0 .../dynamips/hypervisor_manager.py | 0 .../{modules => old_modules}/dynamips/nios/__init__.py | 0 gns3server/{modules => old_modules}/dynamips/nios/nio.py | 0 .../{modules => old_modules}/dynamips/nios/nio_fifo.py | 0 .../dynamips/nios/nio_generic_ethernet.py | 0 .../dynamips/nios/nio_linux_ethernet.py | 0 .../{modules => old_modules}/dynamips/nios/nio_mcast.py | 0 .../{modules => old_modules}/dynamips/nios/nio_null.py | 0 .../{modules => old_modules}/dynamips/nios/nio_tap.py | 0 .../{modules => old_modules}/dynamips/nios/nio_udp.py | 0 .../dynamips/nios/nio_udp_auto.py | 0 .../{modules => old_modules}/dynamips/nios/nio_unix.py | 0 .../{modules => old_modules}/dynamips/nios/nio_vde.py | 0 .../{modules => old_modules}/dynamips/nodes/__init__.py | 0 .../dynamips/nodes/atm_bridge.py | 0 .../dynamips/nodes/atm_switch.py | 0 .../{modules => old_modules}/dynamips/nodes/bridge.py | 0 .../{modules => old_modules}/dynamips/nodes/c1700.py | 0 .../{modules => old_modules}/dynamips/nodes/c2600.py | 0 .../{modules => old_modules}/dynamips/nodes/c2691.py | 0 .../{modules => old_modules}/dynamips/nodes/c3600.py | 0 .../{modules => old_modules}/dynamips/nodes/c3725.py | 0 .../{modules => old_modules}/dynamips/nodes/c3745.py | 0 .../{modules => old_modules}/dynamips/nodes/c7200.py | 0 .../dynamips/nodes/ethernet_switch.py | 0 .../dynamips/nodes/frame_relay_switch.py | 0 .../{modules => old_modules}/dynamips/nodes/hub.py | 0 .../{modules => old_modules}/dynamips/nodes/router.py | 0 .../dynamips/schemas/__init__.py | 0 .../{modules => old_modules}/dynamips/schemas/atmsw.py | 0 .../{modules => old_modules}/dynamips/schemas/ethhub.py | 0 .../{modules => old_modules}/dynamips/schemas/ethsw.py | 0 .../{modules => old_modules}/dynamips/schemas/frsw.py | 0 .../{modules => old_modules}/dynamips/schemas/vm.py | 0 gns3server/{modules => old_modules}/iou/__init__.py | 0 .../{modules => old_modules}/iou/adapters/__init__.py | 0 .../{modules => old_modules}/iou/adapters/adapter.py | 0 .../iou/adapters/ethernet_adapter.py | 0 .../iou/adapters/serial_adapter.py | 0 gns3server/{modules => old_modules}/iou/iou_device.py | 0 gns3server/{modules => old_modules}/iou/iou_error.py | 0 gns3server/{modules => old_modules}/iou/ioucon.py | 0 gns3server/{modules => old_modules}/iou/nios/__init__.py | 0 gns3server/{modules => old_modules}/iou/nios/nio.py | 0 .../iou/nios/nio_generic_ethernet.py | 0 gns3server/{modules => old_modules}/iou/nios/nio_tap.py | 0 gns3server/{modules => old_modules}/iou/nios/nio_udp.py | 0 gns3server/{modules => old_modules}/iou/schemas.py | 0 gns3server/{modules => old_modules}/qemu/__init__.py | 0 .../{modules => old_modules}/qemu/adapters/__init__.py | 0 .../{modules => old_modules}/qemu/adapters/adapter.py | 0 .../qemu/adapters/ethernet_adapter.py | 0 .../{modules => old_modules}/qemu/nios/__init__.py | 0 gns3server/{modules => old_modules}/qemu/nios/nio.py | 0 gns3server/{modules => old_modules}/qemu/nios/nio_udp.py | 0 gns3server/{modules => old_modules}/qemu/qemu_error.py | 0 gns3server/{modules => old_modules}/qemu/qemu_vm.py | 0 gns3server/{modules => old_modules}/qemu/schemas.py | 0 gns3server/web/logger.py | 8 +++++--- tests/api/test_project.py | 2 +- tests/modules/test_project.py | 2 -- 106 files changed, 9 insertions(+), 29 deletions(-) rename gns3server/{modules => old_modules}/dynamips/__init__.py (100%) rename gns3server/{modules => old_modules}/dynamips/adapters/__init__.py (100%) rename gns3server/{modules => old_modules}/dynamips/adapters/adapter.py (100%) rename gns3server/{modules => old_modules}/dynamips/adapters/c1700_mb_1fe.py (100%) rename gns3server/{modules => old_modules}/dynamips/adapters/c1700_mb_wic1.py (100%) rename gns3server/{modules => old_modules}/dynamips/adapters/c2600_mb_1e.py (100%) rename gns3server/{modules => old_modules}/dynamips/adapters/c2600_mb_1fe.py (100%) rename gns3server/{modules => old_modules}/dynamips/adapters/c2600_mb_2e.py (100%) rename gns3server/{modules => old_modules}/dynamips/adapters/c2600_mb_2fe.py (100%) rename gns3server/{modules => old_modules}/dynamips/adapters/c7200_io_2fe.py (100%) rename gns3server/{modules => old_modules}/dynamips/adapters/c7200_io_fe.py (100%) rename gns3server/{modules => old_modules}/dynamips/adapters/c7200_io_ge_e.py (100%) rename gns3server/{modules => old_modules}/dynamips/adapters/gt96100_fe.py (100%) rename gns3server/{modules => old_modules}/dynamips/adapters/leopard_2fe.py (100%) rename gns3server/{modules => old_modules}/dynamips/adapters/nm_16esw.py (100%) rename gns3server/{modules => old_modules}/dynamips/adapters/nm_1e.py (100%) rename gns3server/{modules => old_modules}/dynamips/adapters/nm_1fe_tx.py (100%) rename gns3server/{modules => old_modules}/dynamips/adapters/nm_4e.py (100%) rename gns3server/{modules => old_modules}/dynamips/adapters/nm_4t.py (100%) rename gns3server/{modules => old_modules}/dynamips/adapters/pa_2fe_tx.py (100%) rename gns3server/{modules => old_modules}/dynamips/adapters/pa_4e.py (100%) rename gns3server/{modules => old_modules}/dynamips/adapters/pa_4t.py (100%) rename gns3server/{modules => old_modules}/dynamips/adapters/pa_8e.py (100%) rename gns3server/{modules => old_modules}/dynamips/adapters/pa_8t.py (100%) rename gns3server/{modules => old_modules}/dynamips/adapters/pa_a1.py (100%) rename gns3server/{modules => old_modules}/dynamips/adapters/pa_fe_tx.py (100%) rename gns3server/{modules => old_modules}/dynamips/adapters/pa_ge.py (100%) rename gns3server/{modules => old_modules}/dynamips/adapters/pa_pos_oc3.py (100%) rename gns3server/{modules => old_modules}/dynamips/adapters/wic_1enet.py (100%) rename gns3server/{modules => old_modules}/dynamips/adapters/wic_1t.py (100%) rename gns3server/{modules => old_modules}/dynamips/adapters/wic_2t.py (100%) rename gns3server/{modules => old_modules}/dynamips/backends/__init__.py (100%) rename gns3server/{modules => old_modules}/dynamips/backends/atmsw.py (100%) rename gns3server/{modules => old_modules}/dynamips/backends/ethhub.py (100%) rename gns3server/{modules => old_modules}/dynamips/backends/ethsw.py (100%) rename gns3server/{modules => old_modules}/dynamips/backends/frsw.py (100%) rename gns3server/{modules => old_modules}/dynamips/backends/vm.py (100%) rename gns3server/{modules => old_modules}/dynamips/dynamips_error.py (100%) rename gns3server/{modules => old_modules}/dynamips/dynamips_hypervisor.py (100%) rename gns3server/{modules => old_modules}/dynamips/hypervisor.py (100%) rename gns3server/{modules => old_modules}/dynamips/hypervisor_manager.py (100%) rename gns3server/{modules => old_modules}/dynamips/nios/__init__.py (100%) rename gns3server/{modules => old_modules}/dynamips/nios/nio.py (100%) rename gns3server/{modules => old_modules}/dynamips/nios/nio_fifo.py (100%) rename gns3server/{modules => old_modules}/dynamips/nios/nio_generic_ethernet.py (100%) rename gns3server/{modules => old_modules}/dynamips/nios/nio_linux_ethernet.py (100%) rename gns3server/{modules => old_modules}/dynamips/nios/nio_mcast.py (100%) rename gns3server/{modules => old_modules}/dynamips/nios/nio_null.py (100%) rename gns3server/{modules => old_modules}/dynamips/nios/nio_tap.py (100%) rename gns3server/{modules => old_modules}/dynamips/nios/nio_udp.py (100%) rename gns3server/{modules => old_modules}/dynamips/nios/nio_udp_auto.py (100%) rename gns3server/{modules => old_modules}/dynamips/nios/nio_unix.py (100%) rename gns3server/{modules => old_modules}/dynamips/nios/nio_vde.py (100%) rename gns3server/{modules => old_modules}/dynamips/nodes/__init__.py (100%) rename gns3server/{modules => old_modules}/dynamips/nodes/atm_bridge.py (100%) rename gns3server/{modules => old_modules}/dynamips/nodes/atm_switch.py (100%) rename gns3server/{modules => old_modules}/dynamips/nodes/bridge.py (100%) rename gns3server/{modules => old_modules}/dynamips/nodes/c1700.py (100%) rename gns3server/{modules => old_modules}/dynamips/nodes/c2600.py (100%) rename gns3server/{modules => old_modules}/dynamips/nodes/c2691.py (100%) rename gns3server/{modules => old_modules}/dynamips/nodes/c3600.py (100%) rename gns3server/{modules => old_modules}/dynamips/nodes/c3725.py (100%) rename gns3server/{modules => old_modules}/dynamips/nodes/c3745.py (100%) rename gns3server/{modules => old_modules}/dynamips/nodes/c7200.py (100%) rename gns3server/{modules => old_modules}/dynamips/nodes/ethernet_switch.py (100%) rename gns3server/{modules => old_modules}/dynamips/nodes/frame_relay_switch.py (100%) rename gns3server/{modules => old_modules}/dynamips/nodes/hub.py (100%) rename gns3server/{modules => old_modules}/dynamips/nodes/router.py (100%) rename gns3server/{modules => old_modules}/dynamips/schemas/__init__.py (100%) rename gns3server/{modules => old_modules}/dynamips/schemas/atmsw.py (100%) rename gns3server/{modules => old_modules}/dynamips/schemas/ethhub.py (100%) rename gns3server/{modules => old_modules}/dynamips/schemas/ethsw.py (100%) rename gns3server/{modules => old_modules}/dynamips/schemas/frsw.py (100%) rename gns3server/{modules => old_modules}/dynamips/schemas/vm.py (100%) rename gns3server/{modules => old_modules}/iou/__init__.py (100%) rename gns3server/{modules => old_modules}/iou/adapters/__init__.py (100%) rename gns3server/{modules => old_modules}/iou/adapters/adapter.py (100%) rename gns3server/{modules => old_modules}/iou/adapters/ethernet_adapter.py (100%) rename gns3server/{modules => old_modules}/iou/adapters/serial_adapter.py (100%) rename gns3server/{modules => old_modules}/iou/iou_device.py (100%) rename gns3server/{modules => old_modules}/iou/iou_error.py (100%) rename gns3server/{modules => old_modules}/iou/ioucon.py (100%) rename gns3server/{modules => old_modules}/iou/nios/__init__.py (100%) rename gns3server/{modules => old_modules}/iou/nios/nio.py (100%) rename gns3server/{modules => old_modules}/iou/nios/nio_generic_ethernet.py (100%) rename gns3server/{modules => old_modules}/iou/nios/nio_tap.py (100%) rename gns3server/{modules => old_modules}/iou/nios/nio_udp.py (100%) rename gns3server/{modules => old_modules}/iou/schemas.py (100%) rename gns3server/{modules => old_modules}/qemu/__init__.py (100%) rename gns3server/{modules => old_modules}/qemu/adapters/__init__.py (100%) rename gns3server/{modules => old_modules}/qemu/adapters/adapter.py (100%) rename gns3server/{modules => old_modules}/qemu/adapters/ethernet_adapter.py (100%) rename gns3server/{modules => old_modules}/qemu/nios/__init__.py (100%) rename gns3server/{modules => old_modules}/qemu/nios/nio.py (100%) rename gns3server/{modules => old_modules}/qemu/nios/nio_udp.py (100%) rename gns3server/{modules => old_modules}/qemu/qemu_error.py (100%) rename gns3server/{modules => old_modules}/qemu/qemu_vm.py (100%) rename gns3server/{modules => old_modules}/qemu/schemas.py (100%) diff --git a/gns3dms/modules/rackspace_cloud.py b/gns3dms/modules/rackspace_cloud.py index 06f81046..14c0128f 100644 --- a/gns3dms/modules/rackspace_cloud.py +++ b/gns3dms/modules/rackspace_cloud.py @@ -52,7 +52,7 @@ class Rackspace(object): self.authenticated = self.rksp.authenticate() def _find_my_instance(self): - if self.authenticated == False: + if self.authenticated is not False: log.critical("Not authenticated against rackspace!!!!") for region in self.rksp.list_regions(): diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index c205b00e..cecd8432 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -33,13 +33,11 @@ class BaseVM: # TODO: When delete release console ports - @property def project(self): """Return VM current project""" return self._project - @property def name(self): """ @@ -50,7 +48,6 @@ class BaseVM: return self._name - @name.setter def name(self, new_name): """ @@ -61,7 +58,6 @@ class BaseVM: self._name = new_name - @property def uuid(self): """ @@ -72,7 +68,6 @@ class BaseVM: return self._uuid - @property def manager(self): """ @@ -83,7 +78,6 @@ class BaseVM: return self._manager - @property def working_dir(self): """ @@ -92,7 +86,6 @@ class BaseVM: return self._project.vm_working_directory(self._uuid) - def create(self): """ Creates the VM. @@ -100,7 +93,6 @@ class BaseVM: return - def start(self): """ Starts the VM process. @@ -108,7 +100,6 @@ class BaseVM: raise NotImplementedError - def stop(self): """ Starts the VM process. diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index c6daa461..26694e21 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -47,25 +47,21 @@ class Project: os.mkdir(self._path) os.mkdir(os.path.join(self._path, "vms")) - @property def uuid(self): return self._uuid - @property def location(self): return self._location - @property def path(self): return self._path - def vm_working_directory(self, vm_identifier): """ Return a working directory for a specific VM. @@ -79,7 +75,6 @@ class Project: os.mkdir(path) return path - def __json__(self): return { diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 8fd95bf1..fcafd423 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -309,7 +309,7 @@ class VirtualBoxVM(BaseVM): hdd_info_file = os.path.join(self._working_dir, self._vmname, "hdd_info.json") try: with open(hdd_info_file, "r") as f: - #log.info("loading project: {}".format(path)) + # log.info("loading project: {}".format(path)) hdd_table = json.load(f) except OSError as e: raise VirtualBoxError("Could not read HDD info file: {}".format(e)) @@ -363,7 +363,7 @@ class VirtualBoxVM(BaseVM): try: hdd_info_file = os.path.join(self._working_dir, self._vmname, "hdd_info.json") with open(hdd_info_file, "w") as f: - #log.info("saving project: {}".format(path)) + # log.info("saving project: {}".format(path)) json.dump(hdd_table, f, indent=4) except OSError as e: raise VirtualBoxError("Could not write HDD info file: {}".format(e)) diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index 7de8216c..cd02882e 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -28,7 +28,6 @@ import re import asyncio import socket import shutil -import atexit from pkg_resources import parse_version from .vpcs_error import VPCSError @@ -82,11 +81,9 @@ class VPCSVM(BaseVM): self._check_requirements() - def __del__(self): self._kill_process() - def _check_requirements(self): """ Check if VPCS is available with the correct version @@ -184,7 +181,6 @@ class VPCSVM(BaseVM): stderr=subprocess.STDOUT, cwd=self.working_dir, creationflags=flags) - #atexit(self._kill_process) # Ensure we don't leave orphan process log.info("VPCS instance {} started PID={}".format(self.name, self._process.pid)) self._started = True except (OSError, subprocess.SubprocessError) as e: @@ -192,7 +188,6 @@ class VPCSVM(BaseVM): log.error("Could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout)) raise VPCSError("Could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout)) - @asyncio.coroutine def stop(self): """ @@ -221,7 +216,6 @@ class VPCSVM(BaseVM): except ProcessLookupError: pass - def read_vpcs_stdout(self): """ Reads the standard output of the VPCS process. diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/old_modules/dynamips/__init__.py similarity index 100% rename from gns3server/modules/dynamips/__init__.py rename to gns3server/old_modules/dynamips/__init__.py diff --git a/gns3server/modules/dynamips/adapters/__init__.py b/gns3server/old_modules/dynamips/adapters/__init__.py similarity index 100% rename from gns3server/modules/dynamips/adapters/__init__.py rename to gns3server/old_modules/dynamips/adapters/__init__.py diff --git a/gns3server/modules/dynamips/adapters/adapter.py b/gns3server/old_modules/dynamips/adapters/adapter.py similarity index 100% rename from gns3server/modules/dynamips/adapters/adapter.py rename to gns3server/old_modules/dynamips/adapters/adapter.py diff --git a/gns3server/modules/dynamips/adapters/c1700_mb_1fe.py b/gns3server/old_modules/dynamips/adapters/c1700_mb_1fe.py similarity index 100% rename from gns3server/modules/dynamips/adapters/c1700_mb_1fe.py rename to gns3server/old_modules/dynamips/adapters/c1700_mb_1fe.py diff --git a/gns3server/modules/dynamips/adapters/c1700_mb_wic1.py b/gns3server/old_modules/dynamips/adapters/c1700_mb_wic1.py similarity index 100% rename from gns3server/modules/dynamips/adapters/c1700_mb_wic1.py rename to gns3server/old_modules/dynamips/adapters/c1700_mb_wic1.py diff --git a/gns3server/modules/dynamips/adapters/c2600_mb_1e.py b/gns3server/old_modules/dynamips/adapters/c2600_mb_1e.py similarity index 100% rename from gns3server/modules/dynamips/adapters/c2600_mb_1e.py rename to gns3server/old_modules/dynamips/adapters/c2600_mb_1e.py diff --git a/gns3server/modules/dynamips/adapters/c2600_mb_1fe.py b/gns3server/old_modules/dynamips/adapters/c2600_mb_1fe.py similarity index 100% rename from gns3server/modules/dynamips/adapters/c2600_mb_1fe.py rename to gns3server/old_modules/dynamips/adapters/c2600_mb_1fe.py diff --git a/gns3server/modules/dynamips/adapters/c2600_mb_2e.py b/gns3server/old_modules/dynamips/adapters/c2600_mb_2e.py similarity index 100% rename from gns3server/modules/dynamips/adapters/c2600_mb_2e.py rename to gns3server/old_modules/dynamips/adapters/c2600_mb_2e.py diff --git a/gns3server/modules/dynamips/adapters/c2600_mb_2fe.py b/gns3server/old_modules/dynamips/adapters/c2600_mb_2fe.py similarity index 100% rename from gns3server/modules/dynamips/adapters/c2600_mb_2fe.py rename to gns3server/old_modules/dynamips/adapters/c2600_mb_2fe.py diff --git a/gns3server/modules/dynamips/adapters/c7200_io_2fe.py b/gns3server/old_modules/dynamips/adapters/c7200_io_2fe.py similarity index 100% rename from gns3server/modules/dynamips/adapters/c7200_io_2fe.py rename to gns3server/old_modules/dynamips/adapters/c7200_io_2fe.py diff --git a/gns3server/modules/dynamips/adapters/c7200_io_fe.py b/gns3server/old_modules/dynamips/adapters/c7200_io_fe.py similarity index 100% rename from gns3server/modules/dynamips/adapters/c7200_io_fe.py rename to gns3server/old_modules/dynamips/adapters/c7200_io_fe.py diff --git a/gns3server/modules/dynamips/adapters/c7200_io_ge_e.py b/gns3server/old_modules/dynamips/adapters/c7200_io_ge_e.py similarity index 100% rename from gns3server/modules/dynamips/adapters/c7200_io_ge_e.py rename to gns3server/old_modules/dynamips/adapters/c7200_io_ge_e.py diff --git a/gns3server/modules/dynamips/adapters/gt96100_fe.py b/gns3server/old_modules/dynamips/adapters/gt96100_fe.py similarity index 100% rename from gns3server/modules/dynamips/adapters/gt96100_fe.py rename to gns3server/old_modules/dynamips/adapters/gt96100_fe.py diff --git a/gns3server/modules/dynamips/adapters/leopard_2fe.py b/gns3server/old_modules/dynamips/adapters/leopard_2fe.py similarity index 100% rename from gns3server/modules/dynamips/adapters/leopard_2fe.py rename to gns3server/old_modules/dynamips/adapters/leopard_2fe.py diff --git a/gns3server/modules/dynamips/adapters/nm_16esw.py b/gns3server/old_modules/dynamips/adapters/nm_16esw.py similarity index 100% rename from gns3server/modules/dynamips/adapters/nm_16esw.py rename to gns3server/old_modules/dynamips/adapters/nm_16esw.py diff --git a/gns3server/modules/dynamips/adapters/nm_1e.py b/gns3server/old_modules/dynamips/adapters/nm_1e.py similarity index 100% rename from gns3server/modules/dynamips/adapters/nm_1e.py rename to gns3server/old_modules/dynamips/adapters/nm_1e.py diff --git a/gns3server/modules/dynamips/adapters/nm_1fe_tx.py b/gns3server/old_modules/dynamips/adapters/nm_1fe_tx.py similarity index 100% rename from gns3server/modules/dynamips/adapters/nm_1fe_tx.py rename to gns3server/old_modules/dynamips/adapters/nm_1fe_tx.py diff --git a/gns3server/modules/dynamips/adapters/nm_4e.py b/gns3server/old_modules/dynamips/adapters/nm_4e.py similarity index 100% rename from gns3server/modules/dynamips/adapters/nm_4e.py rename to gns3server/old_modules/dynamips/adapters/nm_4e.py diff --git a/gns3server/modules/dynamips/adapters/nm_4t.py b/gns3server/old_modules/dynamips/adapters/nm_4t.py similarity index 100% rename from gns3server/modules/dynamips/adapters/nm_4t.py rename to gns3server/old_modules/dynamips/adapters/nm_4t.py diff --git a/gns3server/modules/dynamips/adapters/pa_2fe_tx.py b/gns3server/old_modules/dynamips/adapters/pa_2fe_tx.py similarity index 100% rename from gns3server/modules/dynamips/adapters/pa_2fe_tx.py rename to gns3server/old_modules/dynamips/adapters/pa_2fe_tx.py diff --git a/gns3server/modules/dynamips/adapters/pa_4e.py b/gns3server/old_modules/dynamips/adapters/pa_4e.py similarity index 100% rename from gns3server/modules/dynamips/adapters/pa_4e.py rename to gns3server/old_modules/dynamips/adapters/pa_4e.py diff --git a/gns3server/modules/dynamips/adapters/pa_4t.py b/gns3server/old_modules/dynamips/adapters/pa_4t.py similarity index 100% rename from gns3server/modules/dynamips/adapters/pa_4t.py rename to gns3server/old_modules/dynamips/adapters/pa_4t.py diff --git a/gns3server/modules/dynamips/adapters/pa_8e.py b/gns3server/old_modules/dynamips/adapters/pa_8e.py similarity index 100% rename from gns3server/modules/dynamips/adapters/pa_8e.py rename to gns3server/old_modules/dynamips/adapters/pa_8e.py diff --git a/gns3server/modules/dynamips/adapters/pa_8t.py b/gns3server/old_modules/dynamips/adapters/pa_8t.py similarity index 100% rename from gns3server/modules/dynamips/adapters/pa_8t.py rename to gns3server/old_modules/dynamips/adapters/pa_8t.py diff --git a/gns3server/modules/dynamips/adapters/pa_a1.py b/gns3server/old_modules/dynamips/adapters/pa_a1.py similarity index 100% rename from gns3server/modules/dynamips/adapters/pa_a1.py rename to gns3server/old_modules/dynamips/adapters/pa_a1.py diff --git a/gns3server/modules/dynamips/adapters/pa_fe_tx.py b/gns3server/old_modules/dynamips/adapters/pa_fe_tx.py similarity index 100% rename from gns3server/modules/dynamips/adapters/pa_fe_tx.py rename to gns3server/old_modules/dynamips/adapters/pa_fe_tx.py diff --git a/gns3server/modules/dynamips/adapters/pa_ge.py b/gns3server/old_modules/dynamips/adapters/pa_ge.py similarity index 100% rename from gns3server/modules/dynamips/adapters/pa_ge.py rename to gns3server/old_modules/dynamips/adapters/pa_ge.py diff --git a/gns3server/modules/dynamips/adapters/pa_pos_oc3.py b/gns3server/old_modules/dynamips/adapters/pa_pos_oc3.py similarity index 100% rename from gns3server/modules/dynamips/adapters/pa_pos_oc3.py rename to gns3server/old_modules/dynamips/adapters/pa_pos_oc3.py diff --git a/gns3server/modules/dynamips/adapters/wic_1enet.py b/gns3server/old_modules/dynamips/adapters/wic_1enet.py similarity index 100% rename from gns3server/modules/dynamips/adapters/wic_1enet.py rename to gns3server/old_modules/dynamips/adapters/wic_1enet.py diff --git a/gns3server/modules/dynamips/adapters/wic_1t.py b/gns3server/old_modules/dynamips/adapters/wic_1t.py similarity index 100% rename from gns3server/modules/dynamips/adapters/wic_1t.py rename to gns3server/old_modules/dynamips/adapters/wic_1t.py diff --git a/gns3server/modules/dynamips/adapters/wic_2t.py b/gns3server/old_modules/dynamips/adapters/wic_2t.py similarity index 100% rename from gns3server/modules/dynamips/adapters/wic_2t.py rename to gns3server/old_modules/dynamips/adapters/wic_2t.py diff --git a/gns3server/modules/dynamips/backends/__init__.py b/gns3server/old_modules/dynamips/backends/__init__.py similarity index 100% rename from gns3server/modules/dynamips/backends/__init__.py rename to gns3server/old_modules/dynamips/backends/__init__.py diff --git a/gns3server/modules/dynamips/backends/atmsw.py b/gns3server/old_modules/dynamips/backends/atmsw.py similarity index 100% rename from gns3server/modules/dynamips/backends/atmsw.py rename to gns3server/old_modules/dynamips/backends/atmsw.py diff --git a/gns3server/modules/dynamips/backends/ethhub.py b/gns3server/old_modules/dynamips/backends/ethhub.py similarity index 100% rename from gns3server/modules/dynamips/backends/ethhub.py rename to gns3server/old_modules/dynamips/backends/ethhub.py diff --git a/gns3server/modules/dynamips/backends/ethsw.py b/gns3server/old_modules/dynamips/backends/ethsw.py similarity index 100% rename from gns3server/modules/dynamips/backends/ethsw.py rename to gns3server/old_modules/dynamips/backends/ethsw.py diff --git a/gns3server/modules/dynamips/backends/frsw.py b/gns3server/old_modules/dynamips/backends/frsw.py similarity index 100% rename from gns3server/modules/dynamips/backends/frsw.py rename to gns3server/old_modules/dynamips/backends/frsw.py diff --git a/gns3server/modules/dynamips/backends/vm.py b/gns3server/old_modules/dynamips/backends/vm.py similarity index 100% rename from gns3server/modules/dynamips/backends/vm.py rename to gns3server/old_modules/dynamips/backends/vm.py diff --git a/gns3server/modules/dynamips/dynamips_error.py b/gns3server/old_modules/dynamips/dynamips_error.py similarity index 100% rename from gns3server/modules/dynamips/dynamips_error.py rename to gns3server/old_modules/dynamips/dynamips_error.py diff --git a/gns3server/modules/dynamips/dynamips_hypervisor.py b/gns3server/old_modules/dynamips/dynamips_hypervisor.py similarity index 100% rename from gns3server/modules/dynamips/dynamips_hypervisor.py rename to gns3server/old_modules/dynamips/dynamips_hypervisor.py diff --git a/gns3server/modules/dynamips/hypervisor.py b/gns3server/old_modules/dynamips/hypervisor.py similarity index 100% rename from gns3server/modules/dynamips/hypervisor.py rename to gns3server/old_modules/dynamips/hypervisor.py diff --git a/gns3server/modules/dynamips/hypervisor_manager.py b/gns3server/old_modules/dynamips/hypervisor_manager.py similarity index 100% rename from gns3server/modules/dynamips/hypervisor_manager.py rename to gns3server/old_modules/dynamips/hypervisor_manager.py diff --git a/gns3server/modules/dynamips/nios/__init__.py b/gns3server/old_modules/dynamips/nios/__init__.py similarity index 100% rename from gns3server/modules/dynamips/nios/__init__.py rename to gns3server/old_modules/dynamips/nios/__init__.py diff --git a/gns3server/modules/dynamips/nios/nio.py b/gns3server/old_modules/dynamips/nios/nio.py similarity index 100% rename from gns3server/modules/dynamips/nios/nio.py rename to gns3server/old_modules/dynamips/nios/nio.py diff --git a/gns3server/modules/dynamips/nios/nio_fifo.py b/gns3server/old_modules/dynamips/nios/nio_fifo.py similarity index 100% rename from gns3server/modules/dynamips/nios/nio_fifo.py rename to gns3server/old_modules/dynamips/nios/nio_fifo.py diff --git a/gns3server/modules/dynamips/nios/nio_generic_ethernet.py b/gns3server/old_modules/dynamips/nios/nio_generic_ethernet.py similarity index 100% rename from gns3server/modules/dynamips/nios/nio_generic_ethernet.py rename to gns3server/old_modules/dynamips/nios/nio_generic_ethernet.py diff --git a/gns3server/modules/dynamips/nios/nio_linux_ethernet.py b/gns3server/old_modules/dynamips/nios/nio_linux_ethernet.py similarity index 100% rename from gns3server/modules/dynamips/nios/nio_linux_ethernet.py rename to gns3server/old_modules/dynamips/nios/nio_linux_ethernet.py diff --git a/gns3server/modules/dynamips/nios/nio_mcast.py b/gns3server/old_modules/dynamips/nios/nio_mcast.py similarity index 100% rename from gns3server/modules/dynamips/nios/nio_mcast.py rename to gns3server/old_modules/dynamips/nios/nio_mcast.py diff --git a/gns3server/modules/dynamips/nios/nio_null.py b/gns3server/old_modules/dynamips/nios/nio_null.py similarity index 100% rename from gns3server/modules/dynamips/nios/nio_null.py rename to gns3server/old_modules/dynamips/nios/nio_null.py diff --git a/gns3server/modules/dynamips/nios/nio_tap.py b/gns3server/old_modules/dynamips/nios/nio_tap.py similarity index 100% rename from gns3server/modules/dynamips/nios/nio_tap.py rename to gns3server/old_modules/dynamips/nios/nio_tap.py diff --git a/gns3server/modules/dynamips/nios/nio_udp.py b/gns3server/old_modules/dynamips/nios/nio_udp.py similarity index 100% rename from gns3server/modules/dynamips/nios/nio_udp.py rename to gns3server/old_modules/dynamips/nios/nio_udp.py diff --git a/gns3server/modules/dynamips/nios/nio_udp_auto.py b/gns3server/old_modules/dynamips/nios/nio_udp_auto.py similarity index 100% rename from gns3server/modules/dynamips/nios/nio_udp_auto.py rename to gns3server/old_modules/dynamips/nios/nio_udp_auto.py diff --git a/gns3server/modules/dynamips/nios/nio_unix.py b/gns3server/old_modules/dynamips/nios/nio_unix.py similarity index 100% rename from gns3server/modules/dynamips/nios/nio_unix.py rename to gns3server/old_modules/dynamips/nios/nio_unix.py diff --git a/gns3server/modules/dynamips/nios/nio_vde.py b/gns3server/old_modules/dynamips/nios/nio_vde.py similarity index 100% rename from gns3server/modules/dynamips/nios/nio_vde.py rename to gns3server/old_modules/dynamips/nios/nio_vde.py diff --git a/gns3server/modules/dynamips/nodes/__init__.py b/gns3server/old_modules/dynamips/nodes/__init__.py similarity index 100% rename from gns3server/modules/dynamips/nodes/__init__.py rename to gns3server/old_modules/dynamips/nodes/__init__.py diff --git a/gns3server/modules/dynamips/nodes/atm_bridge.py b/gns3server/old_modules/dynamips/nodes/atm_bridge.py similarity index 100% rename from gns3server/modules/dynamips/nodes/atm_bridge.py rename to gns3server/old_modules/dynamips/nodes/atm_bridge.py diff --git a/gns3server/modules/dynamips/nodes/atm_switch.py b/gns3server/old_modules/dynamips/nodes/atm_switch.py similarity index 100% rename from gns3server/modules/dynamips/nodes/atm_switch.py rename to gns3server/old_modules/dynamips/nodes/atm_switch.py diff --git a/gns3server/modules/dynamips/nodes/bridge.py b/gns3server/old_modules/dynamips/nodes/bridge.py similarity index 100% rename from gns3server/modules/dynamips/nodes/bridge.py rename to gns3server/old_modules/dynamips/nodes/bridge.py diff --git a/gns3server/modules/dynamips/nodes/c1700.py b/gns3server/old_modules/dynamips/nodes/c1700.py similarity index 100% rename from gns3server/modules/dynamips/nodes/c1700.py rename to gns3server/old_modules/dynamips/nodes/c1700.py diff --git a/gns3server/modules/dynamips/nodes/c2600.py b/gns3server/old_modules/dynamips/nodes/c2600.py similarity index 100% rename from gns3server/modules/dynamips/nodes/c2600.py rename to gns3server/old_modules/dynamips/nodes/c2600.py diff --git a/gns3server/modules/dynamips/nodes/c2691.py b/gns3server/old_modules/dynamips/nodes/c2691.py similarity index 100% rename from gns3server/modules/dynamips/nodes/c2691.py rename to gns3server/old_modules/dynamips/nodes/c2691.py diff --git a/gns3server/modules/dynamips/nodes/c3600.py b/gns3server/old_modules/dynamips/nodes/c3600.py similarity index 100% rename from gns3server/modules/dynamips/nodes/c3600.py rename to gns3server/old_modules/dynamips/nodes/c3600.py diff --git a/gns3server/modules/dynamips/nodes/c3725.py b/gns3server/old_modules/dynamips/nodes/c3725.py similarity index 100% rename from gns3server/modules/dynamips/nodes/c3725.py rename to gns3server/old_modules/dynamips/nodes/c3725.py diff --git a/gns3server/modules/dynamips/nodes/c3745.py b/gns3server/old_modules/dynamips/nodes/c3745.py similarity index 100% rename from gns3server/modules/dynamips/nodes/c3745.py rename to gns3server/old_modules/dynamips/nodes/c3745.py diff --git a/gns3server/modules/dynamips/nodes/c7200.py b/gns3server/old_modules/dynamips/nodes/c7200.py similarity index 100% rename from gns3server/modules/dynamips/nodes/c7200.py rename to gns3server/old_modules/dynamips/nodes/c7200.py diff --git a/gns3server/modules/dynamips/nodes/ethernet_switch.py b/gns3server/old_modules/dynamips/nodes/ethernet_switch.py similarity index 100% rename from gns3server/modules/dynamips/nodes/ethernet_switch.py rename to gns3server/old_modules/dynamips/nodes/ethernet_switch.py diff --git a/gns3server/modules/dynamips/nodes/frame_relay_switch.py b/gns3server/old_modules/dynamips/nodes/frame_relay_switch.py similarity index 100% rename from gns3server/modules/dynamips/nodes/frame_relay_switch.py rename to gns3server/old_modules/dynamips/nodes/frame_relay_switch.py diff --git a/gns3server/modules/dynamips/nodes/hub.py b/gns3server/old_modules/dynamips/nodes/hub.py similarity index 100% rename from gns3server/modules/dynamips/nodes/hub.py rename to gns3server/old_modules/dynamips/nodes/hub.py diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/old_modules/dynamips/nodes/router.py similarity index 100% rename from gns3server/modules/dynamips/nodes/router.py rename to gns3server/old_modules/dynamips/nodes/router.py diff --git a/gns3server/modules/dynamips/schemas/__init__.py b/gns3server/old_modules/dynamips/schemas/__init__.py similarity index 100% rename from gns3server/modules/dynamips/schemas/__init__.py rename to gns3server/old_modules/dynamips/schemas/__init__.py diff --git a/gns3server/modules/dynamips/schemas/atmsw.py b/gns3server/old_modules/dynamips/schemas/atmsw.py similarity index 100% rename from gns3server/modules/dynamips/schemas/atmsw.py rename to gns3server/old_modules/dynamips/schemas/atmsw.py diff --git a/gns3server/modules/dynamips/schemas/ethhub.py b/gns3server/old_modules/dynamips/schemas/ethhub.py similarity index 100% rename from gns3server/modules/dynamips/schemas/ethhub.py rename to gns3server/old_modules/dynamips/schemas/ethhub.py diff --git a/gns3server/modules/dynamips/schemas/ethsw.py b/gns3server/old_modules/dynamips/schemas/ethsw.py similarity index 100% rename from gns3server/modules/dynamips/schemas/ethsw.py rename to gns3server/old_modules/dynamips/schemas/ethsw.py diff --git a/gns3server/modules/dynamips/schemas/frsw.py b/gns3server/old_modules/dynamips/schemas/frsw.py similarity index 100% rename from gns3server/modules/dynamips/schemas/frsw.py rename to gns3server/old_modules/dynamips/schemas/frsw.py diff --git a/gns3server/modules/dynamips/schemas/vm.py b/gns3server/old_modules/dynamips/schemas/vm.py similarity index 100% rename from gns3server/modules/dynamips/schemas/vm.py rename to gns3server/old_modules/dynamips/schemas/vm.py diff --git a/gns3server/modules/iou/__init__.py b/gns3server/old_modules/iou/__init__.py similarity index 100% rename from gns3server/modules/iou/__init__.py rename to gns3server/old_modules/iou/__init__.py diff --git a/gns3server/modules/iou/adapters/__init__.py b/gns3server/old_modules/iou/adapters/__init__.py similarity index 100% rename from gns3server/modules/iou/adapters/__init__.py rename to gns3server/old_modules/iou/adapters/__init__.py diff --git a/gns3server/modules/iou/adapters/adapter.py b/gns3server/old_modules/iou/adapters/adapter.py similarity index 100% rename from gns3server/modules/iou/adapters/adapter.py rename to gns3server/old_modules/iou/adapters/adapter.py diff --git a/gns3server/modules/iou/adapters/ethernet_adapter.py b/gns3server/old_modules/iou/adapters/ethernet_adapter.py similarity index 100% rename from gns3server/modules/iou/adapters/ethernet_adapter.py rename to gns3server/old_modules/iou/adapters/ethernet_adapter.py diff --git a/gns3server/modules/iou/adapters/serial_adapter.py b/gns3server/old_modules/iou/adapters/serial_adapter.py similarity index 100% rename from gns3server/modules/iou/adapters/serial_adapter.py rename to gns3server/old_modules/iou/adapters/serial_adapter.py diff --git a/gns3server/modules/iou/iou_device.py b/gns3server/old_modules/iou/iou_device.py similarity index 100% rename from gns3server/modules/iou/iou_device.py rename to gns3server/old_modules/iou/iou_device.py diff --git a/gns3server/modules/iou/iou_error.py b/gns3server/old_modules/iou/iou_error.py similarity index 100% rename from gns3server/modules/iou/iou_error.py rename to gns3server/old_modules/iou/iou_error.py diff --git a/gns3server/modules/iou/ioucon.py b/gns3server/old_modules/iou/ioucon.py similarity index 100% rename from gns3server/modules/iou/ioucon.py rename to gns3server/old_modules/iou/ioucon.py diff --git a/gns3server/modules/iou/nios/__init__.py b/gns3server/old_modules/iou/nios/__init__.py similarity index 100% rename from gns3server/modules/iou/nios/__init__.py rename to gns3server/old_modules/iou/nios/__init__.py diff --git a/gns3server/modules/iou/nios/nio.py b/gns3server/old_modules/iou/nios/nio.py similarity index 100% rename from gns3server/modules/iou/nios/nio.py rename to gns3server/old_modules/iou/nios/nio.py diff --git a/gns3server/modules/iou/nios/nio_generic_ethernet.py b/gns3server/old_modules/iou/nios/nio_generic_ethernet.py similarity index 100% rename from gns3server/modules/iou/nios/nio_generic_ethernet.py rename to gns3server/old_modules/iou/nios/nio_generic_ethernet.py diff --git a/gns3server/modules/iou/nios/nio_tap.py b/gns3server/old_modules/iou/nios/nio_tap.py similarity index 100% rename from gns3server/modules/iou/nios/nio_tap.py rename to gns3server/old_modules/iou/nios/nio_tap.py diff --git a/gns3server/modules/iou/nios/nio_udp.py b/gns3server/old_modules/iou/nios/nio_udp.py similarity index 100% rename from gns3server/modules/iou/nios/nio_udp.py rename to gns3server/old_modules/iou/nios/nio_udp.py diff --git a/gns3server/modules/iou/schemas.py b/gns3server/old_modules/iou/schemas.py similarity index 100% rename from gns3server/modules/iou/schemas.py rename to gns3server/old_modules/iou/schemas.py diff --git a/gns3server/modules/qemu/__init__.py b/gns3server/old_modules/qemu/__init__.py similarity index 100% rename from gns3server/modules/qemu/__init__.py rename to gns3server/old_modules/qemu/__init__.py diff --git a/gns3server/modules/qemu/adapters/__init__.py b/gns3server/old_modules/qemu/adapters/__init__.py similarity index 100% rename from gns3server/modules/qemu/adapters/__init__.py rename to gns3server/old_modules/qemu/adapters/__init__.py diff --git a/gns3server/modules/qemu/adapters/adapter.py b/gns3server/old_modules/qemu/adapters/adapter.py similarity index 100% rename from gns3server/modules/qemu/adapters/adapter.py rename to gns3server/old_modules/qemu/adapters/adapter.py diff --git a/gns3server/modules/qemu/adapters/ethernet_adapter.py b/gns3server/old_modules/qemu/adapters/ethernet_adapter.py similarity index 100% rename from gns3server/modules/qemu/adapters/ethernet_adapter.py rename to gns3server/old_modules/qemu/adapters/ethernet_adapter.py diff --git a/gns3server/modules/qemu/nios/__init__.py b/gns3server/old_modules/qemu/nios/__init__.py similarity index 100% rename from gns3server/modules/qemu/nios/__init__.py rename to gns3server/old_modules/qemu/nios/__init__.py diff --git a/gns3server/modules/qemu/nios/nio.py b/gns3server/old_modules/qemu/nios/nio.py similarity index 100% rename from gns3server/modules/qemu/nios/nio.py rename to gns3server/old_modules/qemu/nios/nio.py diff --git a/gns3server/modules/qemu/nios/nio_udp.py b/gns3server/old_modules/qemu/nios/nio_udp.py similarity index 100% rename from gns3server/modules/qemu/nios/nio_udp.py rename to gns3server/old_modules/qemu/nios/nio_udp.py diff --git a/gns3server/modules/qemu/qemu_error.py b/gns3server/old_modules/qemu/qemu_error.py similarity index 100% rename from gns3server/modules/qemu/qemu_error.py rename to gns3server/old_modules/qemu/qemu_error.py diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/old_modules/qemu/qemu_vm.py similarity index 100% rename from gns3server/modules/qemu/qemu_vm.py rename to gns3server/old_modules/qemu/qemu_vm.py diff --git a/gns3server/modules/qemu/schemas.py b/gns3server/old_modules/qemu/schemas.py similarity index 100% rename from gns3server/modules/qemu/schemas.py rename to gns3server/old_modules/qemu/schemas.py diff --git a/gns3server/web/logger.py b/gns3server/web/logger.py index 0c6575e4..d3b61dea 100644 --- a/gns3server/web/logger.py +++ b/gns3server/web/logger.py @@ -31,6 +31,7 @@ class ColouredFormatter(logging.Formatter): PINK = '\x1b[35m' def format(self, record, colour=False): + message = super().format(record) if not colour: @@ -57,13 +58,16 @@ class ColouredFormatter(logging.Formatter): class ColouredStreamHandler(logging.StreamHandler): + def format(self, record, colour=False): + if not isinstance(self.formatter, ColouredFormatter): self.formatter = ColouredFormatter() return self.formatter.format(record, colour) def emit(self, record): + stream = self.stream try: msg = self.format(record, stream.isatty()) @@ -74,7 +78,7 @@ class ColouredStreamHandler(logging.StreamHandler): self.handleError(record) -def init_logger(level,quiet=False): +def init_logger(level, quiet=False): stream_handler = ColouredStreamHandler(sys.stdout) stream_handler.formatter = ColouredFormatter("{asctime} {levelname:8} {filename}:{lineno}#RESET# {message}", "%Y-%m-%d %H:%M:%S", "{") @@ -83,5 +87,3 @@ def init_logger(level,quiet=False): logging.getLogger('user_facing').propagate = False logging.basicConfig(level=level, handlers=[stream_handler]) return logging.getLogger('user_facing') - - diff --git a/tests/api/test_project.py b/tests/api/test_project.py index e4bea217..cf2297c0 100644 --- a/tests/api/test_project.py +++ b/tests/api/test_project.py @@ -44,10 +44,10 @@ def test_create_project_with_uuid(server): assert response.status == 200 assert response.json["uuid"] == "00010203-0405-0607-0809-0a0b0c0d0e0f" + def test_create_project_with_uuid(server): query = {"uuid": "00010203-0405-0607-0809-0a0b0c0d0e0f", "location": "/tmp"} response = server.post("/project", query, example=True) assert response.status == 200 assert response.json["uuid"] == "00010203-0405-0607-0809-0a0b0c0d0e0f" assert response.json["location"] == "/tmp" - diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py index 23b4055f..4cfdc764 100644 --- a/tests/modules/test_project.py +++ b/tests/modules/test_project.py @@ -49,5 +49,3 @@ def test_vm_working_directory(tmpdir): p = Project(location=str(tmpdir)) assert os.path.exists(p.vm_working_directory('00010203-0405-0607-0809-0a0b0c0d0e0f')) assert os.path.exists(os.path.join(str(tmpdir), p.uuid, 'vms', '00010203-0405-0607-0809-0a0b0c0d0e0f')) - - From 54eb8d9e817f313ed9203436a81a7e7e249717f8 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 20 Jan 2015 18:55:17 +0100 Subject: [PATCH 060/485] Drop decorator for async test --- docs/api/examples/post_vpcs.txt | 12 +++++++----- gns3server/handlers/__init__.py | 2 +- tests/api/test_version.py | 8 ++++---- tests/api/test_virtualbox.py | 29 +++++++++++++++-------------- tests/api/test_vpcs.py | 5 ++--- tests/utils.py | 10 +--------- 6 files changed, 30 insertions(+), 36 deletions(-) diff --git a/docs/api/examples/post_vpcs.txt b/docs/api/examples/post_vpcs.txt index b0d5e689..9ed5113d 100644 --- a/docs/api/examples/post_vpcs.txt +++ b/docs/api/examples/post_vpcs.txt @@ -1,21 +1,23 @@ -curl -i -xPOST 'http://localhost:8000/vpcs' -d '{"name": "PC TEST 1"}' +curl -i -X POST 'http://localhost:8000/vpcs' -d '{"name": "PC TEST 1", "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80"}' POST /vpcs HTTP/1.1 { - "name": "PC TEST 1" + "name": "PC TEST 1", + "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80" } HTTP/1.1 200 CONNECTION: close -CONTENT-LENGTH: 66 +CONTENT-LENGTH: 160 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, + "console": 2000, "name": "PC TEST 1", - "vpcs_id": 1 + "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", + "uuid": "0adee348-1ce4-417b-9b5c-ff96f62ce35a" } diff --git a/gns3server/handlers/__init__.py b/gns3server/handlers/__init__.py index 965f59e6..ca127009 100644 --- a/gns3server/handlers/__init__.py +++ b/gns3server/handlers/__init__.py @@ -1 +1 @@ -__all__ = ['version_handler', 'vpcs_handler', 'project_handler'] +__all__ = ['version_handler', 'vpcs_handler', 'project_handler', 'virtualbox_handler'] diff --git a/tests/api/test_version.py b/tests/api/test_version.py index a052bb43..30120b9f 100644 --- a/tests/api/test_version.py +++ b/tests/api/test_version.py @@ -51,11 +51,11 @@ def test_version_invalid_input_schema(server): assert response.status == 400 -@asyncio_patch("gns3server.handlers.version_handler.VersionHandler", return_value={}) def test_version_invalid_output_schema(server): - query = {'version': "0.4.2"} - response = server.post('/version', query) - assert response.status == 400 + with asyncio_patch("gns3server.handlers.version_handler.VersionHandler", return_value={}): + query = {'version': __version__} + response = server.post('/version', query) + assert response.status == 400 def test_version_invalid_json(server): diff --git a/tests/api/test_virtualbox.py b/tests/api/test_virtualbox.py index a73700f9..5ea86e36 100644 --- a/tests/api/test_virtualbox.py +++ b/tests/api/test_virtualbox.py @@ -15,27 +15,28 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from tests.api.base import server, loop from tests.utils import asyncio_patch -@asyncio_patch("gns3server.modules.VirtualBox.create_vm", return_value="61d61bdd-aa7d-4912-817f-65a9eb54d3ab") def test_vbox_create(server): - response = server.post("/virtualbox", {"name": "VM1"}, example=False) - assert response.status == 200 - assert response.route == "/virtualbox" - assert response.json["name"] == "VM1" - assert response.json["uuid"] == "61d61bdd-aa7d-4912-817f-65a9eb54d3ab" + with asyncio_patch("gns3server.modules.VirtualBox.create_vm", return_value="61d61bdd-aa7d-4912-817f-65a9eb54d3ab"): + response = server.post("/virtualbox", {"name": "VM1"}, example=False) + assert response.status == 400 + assert response.route == "/virtualbox" + assert response.json["name"] == "VM1" + assert response.json["uuid"] == "61d61bdd-aa7d-4912-817f-65a9eb54d3ab" -@asyncio_patch("gns3server.modules.VirtualBox.start_vm", return_value=True) def test_vbox_start(server): - response = server.post("/virtualbox/61d61bdd-aa7d-4912-817f-65a9eb54d3ab/start", {}, example=False) - assert response.status == 204 - assert response.route == "/virtualbox/61d61bdd-aa7d-4912-817f-65a9eb54d3ab/start" + with asyncio_patch("gns3server.modules.VirtualBox.start_vm", return_value=True): + response = server.post("/virtualbox/61d61bdd-aa7d-4912-817f-65a9eb54d3ab/start", {}, example=False) + assert response.status == 200 + assert response.route == "/virtualbox/{uuid}/start" -@asyncio_patch("gns3server.modules.VirtualBox.stop_vm", return_value=True) def test_vbox_stop(server): - response = server.post("/virtualbox/61d61bdd-aa7d-4912-817f-65a9eb54d3ab/stop", {}, example=False) - assert response.status == 204 - assert response.route == "/virtualbox/61d61bdd-aa7d-4912-817f-65a9eb54d3ab/stop" + with asyncio_patch("gns3server.modules.VirtualBox.stop_vm", return_value=True): + response = server.post("/virtualbox/61d61bdd-aa7d-4912-817f-65a9eb54d3ab/stop", {}, example=False) + assert response.status == 200 + assert response.route == "/virtualbox/{uuid}/stop" diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index 205e74a0..7054bfbb 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -19,6 +19,7 @@ import pytest from tests.api.base import server, loop, project from tests.utils import asyncio_patch from unittest.mock import patch +from gns3server.modules.vpcs.vpcs_vm import VPCSVM @pytest.fixture(scope="module") @@ -28,14 +29,12 @@ def vm(server, project): return response.json -@asyncio_patch("gns3server.modules.VPCS.create_vm", return_value="61d61bdd-aa7d-4912-817f-65a9eb54d3ab") def test_vpcs_create(server, project): response = server.post("/vpcs", {"name": "PC TEST 1", "project_uuid": project.uuid}, example=True) assert response.status == 200 assert response.route == "/vpcs" assert response.json["name"] == "PC TEST 1" - assert response.json["uuid"] == "61d61bdd-aa7d-4912-817f-65a9eb54d3ab" - assert response.json["project_uuid"] == "61d61bdd-aa7d-4912-817f-65a9eb54d3ab" + assert response.json["project_uuid"] == project.uuid def test_vpcs_nio_create_udp(server, vm): diff --git a/tests/utils.py b/tests/utils.py index 40ef2803..3ce32530 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -24,7 +24,7 @@ class _asyncio_patch: """ A wrapper around python patch supporting asyncio. Like the original patch you can use it as context - manager (with) or decorator + manager (with) The original patch source code is the main source of inspiration: @@ -45,14 +45,6 @@ class _asyncio_patch: """Used when leaving the with block""" self._patcher.stop() - def __call__(self, func, *args, **kwargs): - """Call is used when asyncio_patch is used as decorator""" - @patch(self.function, return_value=self._fake_anwser()) - @asyncio.coroutine - def inner(*a, **kw): - return func(*a, **kw) - return inner - def _fake_anwser(self): future = asyncio.Future() future.set_result(self.kwargs["return_value"]) From c30f7ce9a1c3c33f2b62407a2125bc362d7998d5 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 20 Jan 2015 19:23:35 +0100 Subject: [PATCH 061/485] Fix tests --- docs/api/examples/post_vpcs.txt | 2 +- tests/api/test_version.py | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/docs/api/examples/post_vpcs.txt b/docs/api/examples/post_vpcs.txt index 9ed5113d..00d846ec 100644 --- a/docs/api/examples/post_vpcs.txt +++ b/docs/api/examples/post_vpcs.txt @@ -19,5 +19,5 @@ X-ROUTE: /vpcs "console": 2000, "name": "PC TEST 1", "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "uuid": "0adee348-1ce4-417b-9b5c-ff96f62ce35a" + "uuid": "6373ecfa-a186-48de-be4d-3f3463d2e23c" } diff --git a/tests/api/test_version.py b/tests/api/test_version.py index 30120b9f..110f7b1a 100644 --- a/tests/api/test_version.py +++ b/tests/api/test_version.py @@ -51,13 +51,6 @@ def test_version_invalid_input_schema(server): assert response.status == 400 -def test_version_invalid_output_schema(server): - with asyncio_patch("gns3server.handlers.version_handler.VersionHandler", return_value={}): - query = {'version': __version__} - response = server.post('/version', query) - assert response.status == 400 - - def test_version_invalid_json(server): query = "BOUM" response = server.post('/version', query, raw=True) From fa57485f11fd3e2d6599fbf4c87d7a7f28c4c044 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 20 Jan 2015 19:56:18 +0100 Subject: [PATCH 062/485] Support script file --- docs/api/examples/post_vpcs.txt | 5 +++-- gns3server/handlers/vpcs_handler.py | 10 +++++----- gns3server/modules/base_manager.py | 4 ++-- gns3server/modules/vpcs/vpcs_vm.py | 13 +++++++++++-- gns3server/schemas/vpcs.py | 10 +++++++++- tests/api/test_vpcs.py | 10 ++++++++++ 6 files changed, 40 insertions(+), 12 deletions(-) diff --git a/docs/api/examples/post_vpcs.txt b/docs/api/examples/post_vpcs.txt index 00d846ec..9b6ffd02 100644 --- a/docs/api/examples/post_vpcs.txt +++ b/docs/api/examples/post_vpcs.txt @@ -9,7 +9,7 @@ POST /vpcs HTTP/1.1 HTTP/1.1 200 CONNECTION: close -CONTENT-LENGTH: 160 +CONTENT-LENGTH: 185 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT SERVER: Python/3.4 aiohttp/0.13.1 @@ -19,5 +19,6 @@ X-ROUTE: /vpcs "console": 2000, "name": "PC TEST 1", "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "uuid": "6373ecfa-a186-48de-be4d-3f3463d2e23c" + "script_file": null, + "uuid": "aa017896-574d-4a10-bcad-d3001ea98f8b" } diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index 3eb9b46d..2978e0de 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -41,11 +41,11 @@ class VPCSHandler: def create(request, response): vpcs = VPCS.instance() - vm = yield from vpcs.create_vm(request.json["name"], request.json["project_uuid"], uuid=request.json.get("uuid")) - response.json({"name": vm.name, - "uuid": vm.uuid, - "console": vm.console, - "project_uuid": vm.project.uuid}) + vm = yield from vpcs.create_vm(request.json["name"], + request.json["project_uuid"], + uuid=request.json.get("uuid"), + script_file=request.json.get("script_file")) + response.json(vm) @classmethod @Route.post( diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index c90a2bab..5a98ebd0 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -87,7 +87,7 @@ class BaseManager: return self._vms[uuid] @asyncio.coroutine - def create_vm(self, name, project_identifier, uuid=None): + def create_vm(self, name, project_identifier, uuid=None, **kwargs): """ Create a new VM @@ -104,7 +104,7 @@ class BaseManager: if not uuid: uuid = str(uuid4()) - vm = self._VM_CLASS(name, uuid, project, self) + vm = self._VM_CLASS(name, uuid, project, self, **kwargs) future = vm.create() if isinstance(future, asyncio.Future): yield from future diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index cd02882e..66cba55e 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -52,9 +52,10 @@ class VPCSVM(BaseVM): :param project: Project instance :param manager: parent VM Manager :param console: TCP console port + :param script_file: A VPCS startup script """ - def __init__(self, name, uuid, project, manager, console=None): + def __init__(self, name, uuid, project, manager, console=None, script_file=None): super().__init__(name, uuid, project, manager) @@ -68,7 +69,7 @@ class VPCSVM(BaseVM): self._started = False # VPCS settings - self._script_file = "" + self._script_file = script_file self._ethernet_adapter = EthernetAdapter() # one adapter with 1 Ethernet interface try: @@ -103,6 +104,14 @@ class VPCSVM(BaseVM): self._check_vpcs_version() + def __json__(self): + + return {"name": self.name, + "uuid": self.uuid, + "console": self.console, + "project_uuid": self.project.uuid, + "script_file": self.script_file} + @property def console(self): """ diff --git a/gns3server/schemas/vpcs.py b/gns3server/schemas/vpcs.py index 8a2834ae..ed738eb4 100644 --- a/gns3server/schemas/vpcs.py +++ b/gns3server/schemas/vpcs.py @@ -50,6 +50,10 @@ VPCS_CREATE_SCHEMA = { "maximum": 65535, "type": "integer" }, + "script_file": { + "description": "VPCS startup script", + "type": ["string", "null"] + }, }, "additionalProperties": False, "required": ["name", "project_uuid"] @@ -143,7 +147,11 @@ VPCS_OBJECT_SCHEMA = { "minLength": 36, "maxLength": 36, "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" - } + }, + "script_file": { + "description": "VPCS startup script", + "type": ["string", "null"] + }, }, "additionalProperties": False, "required": ["name", "uuid", "console", "project_uuid"] diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index 7054bfbb..0f56e1a1 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -35,6 +35,16 @@ def test_vpcs_create(server, project): assert response.route == "/vpcs" assert response.json["name"] == "PC TEST 1" assert response.json["project_uuid"] == project.uuid + assert response.json["script_file"] is None + + +def test_vpcs_create_script_file(server, project): + response = server.post("/vpcs", {"name": "PC TEST 1", "project_uuid": project.uuid, "script_file": "/tmp/test"}) + assert response.status == 200 + assert response.route == "/vpcs" + assert response.json["name"] == "PC TEST 1" + assert response.json["project_uuid"] == project.uuid + assert response.json["script_file"] == "/tmp/test" def test_vpcs_nio_create_udp(server, vm): From f2289874af348f5999f163a87876f71fb8c84e78 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 20 Jan 2015 20:09:20 +0100 Subject: [PATCH 063/485] Raise exception if we try to reserve an already reserve port --- gns3server/modules/port_manager.py | 11 ++++++----- tests/modules/test_port_manager.py | 27 +++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 tests/modules/test_port_manager.py diff --git a/gns3server/modules/port_manager.py b/gns3server/modules/port_manager.py index 4ea2cac5..26b2c990 100644 --- a/gns3server/modules/port_manager.py +++ b/gns3server/modules/port_manager.py @@ -18,6 +18,7 @@ import socket import ipaddress import asyncio +from aiohttp.web import HTTPConflict class PortManager: @@ -146,10 +147,10 @@ class PortManager: else: continue - raise Exception("Could not find a free port between {} and {} on host {}, last exception: {}".format(start_port, - end_port, - host, - last_exception)) + raise HTTPConflict(reason="Could not find a free port between {} and {} on host {}, last exception: {}".format(start_port, + end_port, + host, + last_exception)) def get_free_console_port(self): """ @@ -173,7 +174,7 @@ class PortManager: """ if port in self._used_tcp_ports: - raise Exception("TCP port already {} in use on host".format(port, self._host)) + raise HTTPConflict(reason="TCP port already {} in use on host".format(port, self._console_host)) self._used_tcp_ports.add(port) def release_console_port(self, port): diff --git a/tests/modules/test_port_manager.py b/tests/modules/test_port_manager.py new file mode 100644 index 00000000..e5a86ea8 --- /dev/null +++ b/tests/modules/test_port_manager.py @@ -0,0 +1,27 @@ +# -*- 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 aiohttp +import pytest +from gns3server.modules.port_manager import PortManager + + +def test_reserve_console_port(): + pm = PortManager.instance() + pm.reserve_console_port(4242) + with pytest.raises(aiohttp.web.HTTPConflict): + pm.reserve_console_port(4242) From 57c3463edc82e0525738b5e0ceff35c0e863a5d7 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 20 Jan 2015 20:11:39 +0100 Subject: [PATCH 064/485] Ignore vpcs.hist --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 876a38ec..c2cb81d9 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,6 @@ nosetests.xml #Documentation build docs/_build + +#VPCS +vpcs.hist From 649d4e514396d6e0f45e6febc7392ad0e7965fb8 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 20 Jan 2015 20:54:46 +0100 Subject: [PATCH 065/485] Allow user to set console port --- docs/api/examples/post_vpcs.txt | 2 +- gns3server/handlers/vpcs_handler.py | 1 + gns3server/modules/port_manager.py | 2 ++ gns3server/modules/vpcs/vpcs_vm.py | 17 +++++++---------- gns3server/schemas/vpcs.py | 2 +- gns3server/web/response.py | 4 +++- gns3server/web/route.py | 4 +++- tests/api/test_vpcs.py | 9 +++++++++ 8 files changed, 27 insertions(+), 14 deletions(-) diff --git a/docs/api/examples/post_vpcs.txt b/docs/api/examples/post_vpcs.txt index 9b6ffd02..4fdc51cd 100644 --- a/docs/api/examples/post_vpcs.txt +++ b/docs/api/examples/post_vpcs.txt @@ -20,5 +20,5 @@ X-ROUTE: /vpcs "name": "PC TEST 1", "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, - "uuid": "aa017896-574d-4a10-bcad-d3001ea98f8b" + "uuid": "f598eb3a-67b2-43ab-af37-3c9b1f643cdd" } diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index 2978e0de..54d32af2 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -44,6 +44,7 @@ class VPCSHandler: vm = yield from vpcs.create_vm(request.json["name"], request.json["project_uuid"], uuid=request.json.get("uuid"), + console=request.json.get("console"), script_file=request.json.get("script_file")) response.json(vm) diff --git a/gns3server/modules/port_manager.py b/gns3server/modules/port_manager.py index 26b2c990..6ffc3309 100644 --- a/gns3server/modules/port_manager.py +++ b/gns3server/modules/port_manager.py @@ -157,6 +157,7 @@ class PortManager: Get an available TCP console port and reserve it """ + print("FREE") port = self.find_unused_port(self._console_port_range[0], self._console_port_range[1], host=self._console_host, @@ -176,6 +177,7 @@ class PortManager: if port in self._used_tcp_ports: raise HTTPConflict(reason="TCP port already {} in use on host".format(port, self._console_host)) self._used_tcp_ports.add(port) + return port def release_console_port(self, port): """ diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index 66cba55e..02a20a9b 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -72,13 +72,10 @@ class VPCSVM(BaseVM): self._script_file = script_file self._ethernet_adapter = EthernetAdapter() # one adapter with 1 Ethernet interface - try: - if not self._console: - self._console = self._manager.port_manager.get_free_console_port() - else: - self._console = self._manager.port_manager.reserve_console_port(self._console) - except Exception as e: - raise VPCSError(e) + if self._console is not None: + self._console = self._manager.port_manager.reserve_console_port(self._console) + else: + self._console = self._manager.port_manager.get_free_console_port() self._check_requirements() @@ -106,9 +103,9 @@ class VPCSVM(BaseVM): def __json__(self): - return {"name": self.name, - "uuid": self.uuid, - "console": self.console, + return {"name": self._name, + "uuid": self._uuid, + "console": self._console, "project_uuid": self.project.uuid, "script_file": self.script_file} diff --git a/gns3server/schemas/vpcs.py b/gns3server/schemas/vpcs.py index ed738eb4..ea54ee0c 100644 --- a/gns3server/schemas/vpcs.py +++ b/gns3server/schemas/vpcs.py @@ -48,7 +48,7 @@ VPCS_CREATE_SCHEMA = { "description": "console TCP port", "minimum": 1, "maximum": 65535, - "type": "integer" + "type": ["integer", "null"] }, "script_file": { "description": "VPCS startup script", diff --git a/gns3server/web/response.py b/gns3server/web/response.py index 2a0b3911..04d0846a 100644 --- a/gns3server/web/response.py +++ b/gns3server/web/response.py @@ -49,6 +49,8 @@ class Response(aiohttp.web.Response): try: jsonschema.validate(answer, self._output_schema) except jsonschema.ValidationError as e: - log.error("Invalid output schema") + log.error("Invalid output schema {} '{}' in schema: {}".format(e.validator, + e.validator_value, + json.dumps(e.schema))) 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 index 4cc6a863..cd1ece5d 100644 --- a/gns3server/web/route.py +++ b/gns3server/web/route.py @@ -40,7 +40,9 @@ def parse_request(request, input_schema): try: jsonschema.validate(request.json, input_schema) except jsonschema.ValidationError as e: - log.error("Invalid input schema") + log.error("Invalid input schema {} '{}' in schema: {}".format(e.validator, + e.validator_value, + json.dumps(e.schema))) raise aiohttp.web.HTTPBadRequest(text="Request is not {} '{}' in schema: {}".format( e.validator, e.validator_value, diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index 0f56e1a1..3f9ff2f7 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -47,6 +47,15 @@ def test_vpcs_create_script_file(server, project): assert response.json["script_file"] == "/tmp/test" +def test_vpcs_create_port(server, project): + response = server.post("/vpcs", {"name": "PC TEST 1", "project_uuid": project.uuid, "console": 4242}) + assert response.status == 200 + assert response.route == "/vpcs" + assert response.json["name"] == "PC TEST 1" + assert response.json["project_uuid"] == project.uuid + assert response.json["console"] == 4242 + + def test_vpcs_nio_create_udp(server, vm): response = server.post("/vpcs/{}/ports/0/nio".format(vm["uuid"]), {"type": "nio_udp", "lport": 4242, From 0eaa7be86a2c612c1327ecfbd7e2e51a9010d1f7 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 20 Jan 2015 22:13:58 +0100 Subject: [PATCH 066/485] PEP 8 Enforcer. --- gns3server/old_modules/qemu/qemu_vm.py | 13 ++----------- scripts/pep8.sh | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 11 deletions(-) create mode 100755 scripts/pep8.sh diff --git a/gns3server/old_modules/qemu/qemu_vm.py b/gns3server/old_modules/qemu/qemu_vm.py index f4a01ea1..a5ae107d 100644 --- a/gns3server/old_modules/qemu/qemu_vm.py +++ b/gns3server/old_modules/qemu/qemu_vm.py @@ -1003,7 +1003,7 @@ class QemuVM(object): nio.rport, nio.rhost)) else: - #FIXME: does it work? very undocumented feature... + # FIXME: does it work? very undocumented feature... self._control_vm("netdev_del gns3-{}".format(adapter_id)) self._control_vm("netdev_add socket,id=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_id, nio.rhost, @@ -1038,7 +1038,7 @@ class QemuVM(object): self._control_vm("host_net_remove {} gns3-{}".format(adapter_id, adapter_id)) self._control_vm("host_net_add user vlan={},name=gns3-{}".format(adapter_id, adapter_id)) else: - #FIXME: does it work? very undocumented feature... + # FIXME: does it work? very undocumented feature... self._control_vm("netdev_del gns3-{}".format(adapter_id)) self._control_vm("netdev_add user,id=gns3-{}".format(adapter_id)) @@ -1210,20 +1210,11 @@ class QemuVM(object): nio.rport, nio.rhost)]) else: -<<<<<<< HEAD:gns3server/old_modules/qemu/qemu_vm.py - network_options.extend(["-net", "socket,vlan={},name=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_id, - adapter_id, - nio.rhost, - nio.rport, - self._host, - nio.lport)]) -======= network_options.extend(["-netdev", "socket,id=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_id, nio.rhost, nio.rport, self._host, nio.lport)]) ->>>>>>> master:gns3server/modules/qemu/qemu_vm.py else: if self._legacy_networking: network_options.extend(["-net", "user,vlan={},name=gns3-{}".format(adapter_id, adapter_id)]) diff --git a/scripts/pep8.sh b/scripts/pep8.sh new file mode 100755 index 00000000..ea0694f5 --- /dev/null +++ b/scripts/pep8.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +echo ' + _______ ________ _______ ______ +| \ | \| \ / \ +| $$$$$$$\| $$$$$$$$| $$$$$$$\| $$$$$$\ +| $$__/ $$| $$__ | $$__/ $$| $$__/ $$ +| $$ $$| $$ \ | $$ $$ >$$ $$ +| $$$$$$$ | $$$$$ | $$$$$$$ | $$$$$$ +| $$ | $$_____ | $$ | $$__/ $$ +| $$ | $$ \| $$ \$$ $$ + \$$ \$$$$$$$$ \$$ \$$$$$$ + +' + + +find . -name '*.py' -exec autopep8 --in-place -v --aggressive --aggressive \{\} \; + +echo "Its 'clean" From 984d47f9c8c0c8d6f443114509dccf3eca80d933 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 20 Jan 2015 22:50:26 +0100 Subject: [PATCH 067/485] Test work without vpcs binary --- .travis.yml | 2 +- docs/api/examples/post_vpcs.txt | 2 +- gns3server/modules/port_manager.py | 1 - gns3server/modules/vpcs/vpcs_vm.py | 4 ++-- tests/modules/vpcs/test_vpcs_vm.py | 8 ++++++-- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 59cbebed..2c741583 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ python: before_install: - sudo add-apt-repository ppa:gns3/ppa -y - sudo apt-get update -q - - sudo apt-get install vpcs dynamips + - sudo apt-get install dynamips install: - python setup.py install diff --git a/docs/api/examples/post_vpcs.txt b/docs/api/examples/post_vpcs.txt index 4fdc51cd..9b849f8c 100644 --- a/docs/api/examples/post_vpcs.txt +++ b/docs/api/examples/post_vpcs.txt @@ -20,5 +20,5 @@ X-ROUTE: /vpcs "name": "PC TEST 1", "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, - "uuid": "f598eb3a-67b2-43ab-af37-3c9b1f643cdd" + "uuid": "12171ed6-4234-4ba7-9ef5-db3631a3a2e4" } diff --git a/gns3server/modules/port_manager.py b/gns3server/modules/port_manager.py index 6ffc3309..ad90d92a 100644 --- a/gns3server/modules/port_manager.py +++ b/gns3server/modules/port_manager.py @@ -157,7 +157,6 @@ class PortManager: Get an available TCP console port and reserve it """ - print("FREE") port = self.find_unused_port(self._console_port_range[0], self._console_port_range[1], host=self._console_host, diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index 02a20a9b..f947c46a 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -77,8 +77,6 @@ class VPCSVM(BaseVM): else: self._console = self._manager.port_manager.get_free_console_port() - self._check_requirements() - def __del__(self): self._kill_process() @@ -169,6 +167,8 @@ class VPCSVM(BaseVM): Starts the VPCS process. """ + self._check_requirements() + if not self.is_running(): if not self._ethernet_adapter.get_nio(0): raise VPCSError("This VPCS instance must be connected in order to start") diff --git a/tests/modules/vpcs/test_vpcs_vm.py b/tests/modules/vpcs/test_vpcs_vm.py index 08f80be4..00689b0e 100644 --- a/tests/modules/vpcs/test_vpcs_vm.py +++ b/tests/modules/vpcs/test_vpcs_vm.py @@ -44,21 +44,24 @@ def test_vm(project, manager): @patch("subprocess.check_output", return_value="Welcome to Virtual PC Simulator, version 0.1".encode("utf-8")) -def test_vm_invalid_vpcs_version(project, manager): +def test_vm_invalid_vpcs_version(project, manager, loop): with pytest.raises(VPCSError): vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + loop.run_until_complete(asyncio.async(vm.start())) assert vm.name == "test" assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0f" @patch("gns3server.config.Config.get_section_config", return_value={"path": "/bin/test_fake"}) -def test_vm_invalid_vpcs_path(project, manager): +def test_vm_invalid_vpcs_path(project, manager, loop): with pytest.raises(VPCSError): vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + loop.run_until_complete(asyncio.async(vm.start())) assert vm.name == "test" assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0f" +@patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM._check_requirements", return_value=True) def test_start(project, loop, manager): with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) @@ -68,6 +71,7 @@ def test_start(project, loop, manager): assert vm.is_running() +@patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM._check_requirements", return_value=True) def test_stop(project, loop, manager): process = MagicMock() with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): From fc66e4592ad14ef620390b2d0f5efdaa7c77e9ee Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 20 Jan 2015 23:27:28 +0100 Subject: [PATCH 068/485] VPCS is trully async --- docs/api/examples/post_vpcs.txt | 2 +- gns3server/modules/vpcs/vpcs_vm.py | 17 +++++++---- tests/api/test_vpcs.py | 10 +++++++ tests/modules/vpcs/test_vpcs_vm.py | 45 +++++++++++++++--------------- 4 files changed, 45 insertions(+), 29 deletions(-) diff --git a/docs/api/examples/post_vpcs.txt b/docs/api/examples/post_vpcs.txt index 9b849f8c..8a72d442 100644 --- a/docs/api/examples/post_vpcs.txt +++ b/docs/api/examples/post_vpcs.txt @@ -20,5 +20,5 @@ X-ROUTE: /vpcs "name": "PC TEST 1", "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, - "uuid": "12171ed6-4234-4ba7-9ef5-db3631a3a2e4" + "uuid": "a4809caf-bfba-4f34-ad78-1d054c8b7e5a" } diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index f947c46a..dfeda3e4 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -80,6 +80,7 @@ class VPCSVM(BaseVM): def __del__(self): self._kill_process() + @asyncio.coroutine def _check_requirements(self): """ Check if VPCS is available with the correct version @@ -97,7 +98,7 @@ class VPCSVM(BaseVM): if not os.access(self._path, os.X_OK): raise VPCSError("VPCS program '{}' is not executable".format(self._path)) - self._check_vpcs_version() + yield from self._check_vpcs_version() def __json__(self): @@ -144,14 +145,14 @@ class VPCSVM(BaseVM): new_name=new_name)) BaseVM.name = new_name + @asyncio.coroutine def _check_vpcs_version(self): """ Checks if the VPCS executable version is >= 0.5b1. """ - # TODO: should be async try: - output = subprocess.check_output([self._path, "-v"], cwd=self.working_dir) - match = re.search("Welcome to Virtual PC Simulator, version ([0-9a-z\.]+)", output.decode("utf-8")) + output = yield from self._get_vpcs_welcome() + match = re.search("Welcome to Virtual PC Simulator, version ([0-9a-z\.]+)", output) if match: version = match.group(1) if parse_version(version) < parse_version("0.5b1"): @@ -161,13 +162,19 @@ class VPCSVM(BaseVM): except (OSError, subprocess.SubprocessError) as e: raise VPCSError("Error while looking for the VPCS version: {}".format(e)) + @asyncio.coroutine + def _get_vpcs_welcome(self): + proc = yield from asyncio.create_subprocess_exec(' '.join([self._path, "-v"]), stdout=asyncio.subprocess.PIPE, cwd=self.working_dir) + out = yield from proc.stdout.readline() + return out.decode("utf-8") + @asyncio.coroutine def start(self): """ Starts the VPCS process. """ - self._check_requirements() + yield from self._check_requirements() if not self.is_running(): if not self._ethernet_adapter.get_nio(0): diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index 3f9ff2f7..0df6882f 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -84,3 +84,13 @@ def test_vpcs_delete_nio(server, vm): response = server.delete("/vpcs/{}/ports/0/nio".format(vm["uuid"]), example=True) assert response.status == 200 assert response.route == "/vpcs/{uuid}/ports/{port_id}/nio" + + +def test_vpcs_start(): + # assert True == False + pass + + +def test_vpcs_stop(): + # assert True == False + pass diff --git a/tests/modules/vpcs/test_vpcs_vm.py b/tests/modules/vpcs/test_vpcs_vm.py index 00689b0e..a39676c7 100644 --- a/tests/modules/vpcs/test_vpcs_vm.py +++ b/tests/modules/vpcs/test_vpcs_vm.py @@ -36,20 +36,19 @@ def manager(): return m -@patch("subprocess.check_output", return_value="Welcome to Virtual PC Simulator, version 0.6".encode("utf-8")) def test_vm(project, manager): vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) assert vm.name == "test" assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0f" -@patch("subprocess.check_output", return_value="Welcome to Virtual PC Simulator, version 0.1".encode("utf-8")) def test_vm_invalid_vpcs_version(project, manager, loop): - with pytest.raises(VPCSError): - vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) - loop.run_until_complete(asyncio.async(vm.start())) - assert vm.name == "test" - assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0f" + with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM._get_vpcs_welcome", return_value="Welcome to Virtual PC Simulator, version 0.1"): + with pytest.raises(VPCSError): + vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + loop.run_until_complete(asyncio.async(vm.start())) + assert vm.name == "test" + assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0f" @patch("gns3server.config.Config.get_section_config", return_value={"path": "/bin/test_fake"}) @@ -61,28 +60,28 @@ def test_vm_invalid_vpcs_path(project, manager, loop): assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0f" -@patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM._check_requirements", return_value=True) def test_start(project, loop, manager): - with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): - vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) - nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) + with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM._check_requirements", return_value=True): + with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): + vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) - loop.run_until_complete(asyncio.async(vm.start())) - assert vm.is_running() + loop.run_until_complete(asyncio.async(vm.start())) + assert vm.is_running() -@patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM._check_requirements", return_value=True) def test_stop(project, loop, manager): process = MagicMock() - with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): - vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) - nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) - - loop.run_until_complete(asyncio.async(vm.start())) - assert vm.is_running() - loop.run_until_complete(asyncio.async(vm.stop())) - assert vm.is_running() is False - process.terminate.assert_called_with() + with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM._check_requirements", return_value=True): + with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): + vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) + + loop.run_until_complete(asyncio.async(vm.start())) + assert vm.is_running() + loop.run_until_complete(asyncio.async(vm.stop())) + assert vm.is_running() is False + process.terminate.assert_called_with() def test_add_nio_binding_udp(manager, project): From 17f6223fb1f99355f5e13a69c4f953b08024fe1a Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 20 Jan 2015 15:28:40 -0700 Subject: [PATCH 069/485] Try to fix VirtualBox create test. --- gns3server/handlers/virtualbox_handler.py | 6 ++++-- gns3server/modules/base_manager.py | 1 + gns3server/modules/virtualbox/virtualbox_vm.py | 4 ++-- gns3server/schemas/virtualbox.py | 14 ++++++++++++++ gns3server/server.py | 4 +++- tests/api/base.py | 2 +- tests/api/test_virtualbox.py | 8 ++++---- 7 files changed, 29 insertions(+), 10 deletions(-) diff --git a/gns3server/handlers/virtualbox_handler.py b/gns3server/handlers/virtualbox_handler.py index 01c20b3e..d6b944cc 100644 --- a/gns3server/handlers/virtualbox_handler.py +++ b/gns3server/handlers/virtualbox_handler.py @@ -40,9 +40,11 @@ class VirtualBoxHandler: def create(request, response): vbox_manager = VirtualBox.instance() - vm = yield from vbox_manager.create_vm(request.json["name"], request.json.get("uuid")) + vm = yield from vbox_manager.create_vm(request.json["name"], request.json["project_uuid"], request.json.get("uuid")) + print(vm) response.json({"name": vm.name, - "uuid": vm.uuid}) + "uuid": vm.uuid, + "project_uuid": vm.project.uuid}) @classmethod @Route.post( diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 5a98ebd0..7d51cb9f 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -95,6 +95,7 @@ class BaseManager: :param project_identifier UUID of Project :param uuid Force UUID force VM """ + project = ProjectManager.instance().get_project(project_identifier) # TODO: support for old projects VM with normal IDs. diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index fcafd423..f922e681 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -53,9 +53,9 @@ class VirtualBoxVM(BaseVM): _instances = [] _allocated_console_ports = [] - def __init__(self, name, uuid, manager): + def __init__(self, name, uuid, project, manager): - super().__init__(name, uuid, manager) + super().__init__(name, uuid, project, manager) self._system_properties = {} diff --git a/gns3server/schemas/virtualbox.py b/gns3server/schemas/virtualbox.py index d7e9e4be..63e1664e 100644 --- a/gns3server/schemas/virtualbox.py +++ b/gns3server/schemas/virtualbox.py @@ -46,6 +46,13 @@ VBOX_CREATE_SCHEMA = { "maxLength": 36, "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" }, + "project_uuid": { + "description": "Project UUID", + "type": "string", + "minLength": 36, + "maxLength": 36, + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + }, }, "additionalProperties": False, "required": ["name", "vmname"], @@ -68,6 +75,13 @@ VBOX_OBJECT_SCHEMA = { "maxLength": 36, "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" }, + "project_uuid": { + "description": "Project UUID", + "type": "string", + "minLength": 36, + "maxLength": 36, + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + }, "console": { "description": "console TCP port", "minimum": 1, diff --git a/gns3server/server.py b/gns3server/server.py index 56a6bf70..a776a71e 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -35,7 +35,6 @@ from .modules.port_manager import PortManager # TODO: get rid of * have something generic to automatically import handlers so the routes can be found from gns3server.handlers import * -from gns3server.handlers.virtualbox_handler import VirtualBoxHandler import logging log = logging.getLogger(__name__) @@ -133,6 +132,9 @@ class Server: Starts the server. """ + logger = logging.getLogger("asyncio") + logger.setLevel(logging.WARNING) + # TODO: SSL support for Rackspace cloud integration (here or with nginx for instance). self._loop = asyncio.get_event_loop() app = aiohttp.web.Application() diff --git a/tests/api/base.py b/tests/api/base.py index ee3d5024..63607498 100644 --- a/tests/api/base.py +++ b/tests/api/base.py @@ -120,7 +120,7 @@ class Query: def _example_file_path(self, method, path): path = re.sub('[^a-z0-9]', '', path) - return "docs/api/examples/{}_{}.txt".format(method.lower(), path) # FIXME: cannot find path when running tests + return "docs/api/examples/{}_{}.txt".format(method.lower(), path) def _get_unused_port(): diff --git a/tests/api/test_virtualbox.py b/tests/api/test_virtualbox.py index 5ea86e36..f9e0ce46 100644 --- a/tests/api/test_virtualbox.py +++ b/tests/api/test_virtualbox.py @@ -15,13 +15,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from tests.api.base import server, loop +from tests.api.base import server, loop, project from tests.utils import asyncio_patch -def test_vbox_create(server): +def test_vbox_create(server, project): with asyncio_patch("gns3server.modules.VirtualBox.create_vm", return_value="61d61bdd-aa7d-4912-817f-65a9eb54d3ab"): - response = server.post("/virtualbox", {"name": "VM1"}, example=False) + response = server.post("/virtualbox", {"name": "VM1", "vmname": "VM1", "project_uuid": project.uuid}, example=False) assert response.status == 400 assert response.route == "/virtualbox" assert response.json["name"] == "VM1" @@ -39,4 +39,4 @@ def test_vbox_stop(server): with asyncio_patch("gns3server.modules.VirtualBox.stop_vm", return_value=True): response = server.post("/virtualbox/61d61bdd-aa7d-4912-817f-65a9eb54d3ab/stop", {}, example=False) assert response.status == 200 - assert response.route == "/virtualbox/{uuid}/stop" + assert response.route == "/virtualbox/{uuid}/stop" \ No newline at end of file From 3530b85b56388e196825a0485509b33c82ee51a3 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 20 Jan 2015 23:40:03 +0100 Subject: [PATCH 070/485] Fix virtualbox test --- docs/api/examples/post_virtualbox.txt | 23 +++++++++++++++++++++++ docs/api/examples/post_vpcs.txt | 2 +- tests/api/test_virtualbox.py | 17 ++++++++--------- 3 files changed, 32 insertions(+), 10 deletions(-) create mode 100644 docs/api/examples/post_virtualbox.txt diff --git a/docs/api/examples/post_virtualbox.txt b/docs/api/examples/post_virtualbox.txt new file mode 100644 index 00000000..0f7721fa --- /dev/null +++ b/docs/api/examples/post_virtualbox.txt @@ -0,0 +1,23 @@ +curl -i -X POST 'http://localhost:8000/virtualbox' -d '{"name": "VM1", "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "vmname": "VM1"}' + +POST /virtualbox HTTP/1.1 +{ + "name": "VM1", + "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", + "vmname": "VM1" +} + + +HTTP/1.1 200 +CONNECTION: close +CONTENT-LENGTH: 133 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 aiohttp/0.13.1 +X-ROUTE: /virtualbox + +{ + "name": "VM1", + "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", + "uuid": "a028124a-9a69-4b06-b673-21f7eb3d034f" +} diff --git a/docs/api/examples/post_vpcs.txt b/docs/api/examples/post_vpcs.txt index 8a72d442..d5007ba4 100644 --- a/docs/api/examples/post_vpcs.txt +++ b/docs/api/examples/post_vpcs.txt @@ -20,5 +20,5 @@ X-ROUTE: /vpcs "name": "PC TEST 1", "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, - "uuid": "a4809caf-bfba-4f34-ad78-1d054c8b7e5a" + "uuid": "934f0745-0824-451e-8ad6-db0877dbf387" } diff --git a/tests/api/test_virtualbox.py b/tests/api/test_virtualbox.py index f9e0ce46..c7b8856c 100644 --- a/tests/api/test_virtualbox.py +++ b/tests/api/test_virtualbox.py @@ -17,26 +17,25 @@ from tests.api.base import server, loop, project from tests.utils import asyncio_patch +from gns3server.modules.virtualbox.virtualbox_vm import VirtualBoxVM def test_vbox_create(server, project): - with asyncio_patch("gns3server.modules.VirtualBox.create_vm", return_value="61d61bdd-aa7d-4912-817f-65a9eb54d3ab"): - response = server.post("/virtualbox", {"name": "VM1", "vmname": "VM1", "project_uuid": project.uuid}, example=False) - assert response.status == 400 - assert response.route == "/virtualbox" - assert response.json["name"] == "VM1" - assert response.json["uuid"] == "61d61bdd-aa7d-4912-817f-65a9eb54d3ab" + response = server.post("/virtualbox", {"name": "VM1", "vmname": "VM1", "project_uuid": project.uuid}, example=True) + assert response.status == 200 + assert response.route == "/virtualbox" + assert response.json["name"] == "VM1" def test_vbox_start(server): with asyncio_patch("gns3server.modules.VirtualBox.start_vm", return_value=True): - response = server.post("/virtualbox/61d61bdd-aa7d-4912-817f-65a9eb54d3ab/start", {}, example=False) + response = server.post("/virtualbox/61d61bdd-aa7d-4912-817f-65a9eb54d3ab/start", {}, example=True) assert response.status == 200 assert response.route == "/virtualbox/{uuid}/start" def test_vbox_stop(server): with asyncio_patch("gns3server.modules.VirtualBox.stop_vm", return_value=True): - response = server.post("/virtualbox/61d61bdd-aa7d-4912-817f-65a9eb54d3ab/stop", {}, example=False) + response = server.post("/virtualbox/61d61bdd-aa7d-4912-817f-65a9eb54d3ab/stop", {}, example=True) assert response.status == 200 - assert response.route == "/virtualbox/{uuid}/stop" \ No newline at end of file + assert response.route == "/virtualbox/{uuid}/stop" From 7a19c9062eda7453f6fbf196cf46e0720ddaac5c Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 20 Jan 2015 19:02:22 -0700 Subject: [PATCH 071/485] Pass *args to VM_CLASS. Move Config the the base manager. More checks for projects (UUID, makedirs). Return error 500 when a VMError exception is raised. Some more progress to VirtualBox. --- docs/api/examples/post_virtualbox.txt | 2 +- gns3server/handlers/virtualbox_handler.py | 7 ++- gns3server/handlers/vpcs_handler.py | 3 +- gns3server/modules/base_manager.py | 28 ++++++--- gns3server/modules/base_vm.py | 9 ++- gns3server/modules/port_manager.py | 6 +- gns3server/modules/project.py | 34 ++++++---- gns3server/modules/project_manager.py | 16 +++-- .../modules/virtualbox/virtualbox_vm.py | 62 +++++++------------ gns3server/modules/vpcs/vpcs_vm.py | 4 +- gns3server/web/route.py | 4 +- 11 files changed, 94 insertions(+), 81 deletions(-) diff --git a/docs/api/examples/post_virtualbox.txt b/docs/api/examples/post_virtualbox.txt index 0f7721fa..6be43ee6 100644 --- a/docs/api/examples/post_virtualbox.txt +++ b/docs/api/examples/post_virtualbox.txt @@ -19,5 +19,5 @@ X-ROUTE: /virtualbox { "name": "VM1", "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "uuid": "a028124a-9a69-4b06-b673-21f7eb3d034f" + "uuid": "3142e932-d316-40d7-bed3-7ef8e2d313b3" } diff --git a/gns3server/handlers/virtualbox_handler.py b/gns3server/handlers/virtualbox_handler.py index d6b944cc..55d9a53b 100644 --- a/gns3server/handlers/virtualbox_handler.py +++ b/gns3server/handlers/virtualbox_handler.py @@ -32,6 +32,7 @@ class VirtualBoxHandler: r"/virtualbox", status_codes={ 201: "VirtualBox VM instance created", + 400: "Invalid project UUID", 409: "Conflict" }, description="Create a new VirtualBox VM instance", @@ -40,8 +41,10 @@ class VirtualBoxHandler: def create(request, response): vbox_manager = VirtualBox.instance() - vm = yield from vbox_manager.create_vm(request.json["name"], request.json["project_uuid"], request.json.get("uuid")) - print(vm) + vm = yield from vbox_manager.create_vm(request.json["name"], + request.json["project_uuid"], + request.json.get("uuid"), + vmname=request.json["vmname"]) response.json({"name": vm.name, "uuid": vm.uuid, "project_uuid": vm.project.uuid}) diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index 54d32af2..5d4e9ac8 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -33,6 +33,7 @@ class VPCSHandler: r"/vpcs", status_codes={ 201: "VPCS instance created", + 400: "Invalid project UUID", 409: "Conflict" }, description="Create a new VPCS instance", @@ -43,7 +44,7 @@ class VPCSHandler: vpcs = VPCS.instance() vm = yield from vpcs.create_vm(request.json["name"], request.json["project_uuid"], - uuid=request.json.get("uuid"), + request.json.get("uuid"), console=request.json.get("console"), script_file=request.json.get("script_file")) response.json(vm) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 7d51cb9f..3dcbca25 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -20,6 +20,7 @@ import asyncio import aiohttp from uuid import UUID, uuid4 +from ..config import Config from .project_manager import ProjectManager @@ -34,6 +35,7 @@ class BaseManager: self._vms = {} self._port_manager = None + self._config = Config.instance() @classmethod def instance(cls): @@ -50,7 +52,7 @@ class BaseManager: @property def port_manager(self): """ - Returns the port_manager for this VMs + Returns the port manager. :returns: Port manager """ @@ -62,6 +64,16 @@ class BaseManager: self._port_manager = new_port_manager + @property + def config(self): + """ + Returns the server config. + + :returns: Config + """ + + return self._config + @classmethod @asyncio.coroutine # FIXME: why coroutine? def destroy(cls): @@ -87,25 +99,23 @@ class BaseManager: return self._vms[uuid] @asyncio.coroutine - def create_vm(self, name, project_identifier, uuid=None, **kwargs): + def create_vm(self, name, project_uuid, uuid, *args, **kwargs): """ Create a new VM - :param name VM name - :param project_identifier UUID of Project - :param uuid Force UUID force VM + :param name: VM name + :param project_uuid: UUID of Project + :param uuid: restore a VM UUID """ - project = ProjectManager.instance().get_project(project_identifier) + project = ProjectManager.instance().get_project(project_uuid) # TODO: support for old projects VM with normal IDs. - # TODO: supports specific args: pass kwargs to VM_CLASS? - if not uuid: uuid = str(uuid4()) - vm = self._VM_CLASS(name, uuid, project, self, **kwargs) + vm = self._VM_CLASS(name, uuid, project, self, *args, **kwargs) future = vm.create() if isinstance(future, asyncio.Future): yield from future diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index cecd8432..001d5b1e 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -15,7 +15,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from ..config import Config import logging log = logging.getLogger(__name__) @@ -29,13 +28,17 @@ class BaseVM: self._uuid = uuid self._project = project self._manager = manager - self._config = Config.instance() # TODO: When delete release console ports @property def project(self): - """Return VM current project""" + """ + Returns the VM current project. + + :returns: Project instance. + """ + return self._project @property diff --git a/gns3server/modules/port_manager.py b/gns3server/modules/port_manager.py index ad90d92a..31a38a4e 100644 --- a/gns3server/modules/port_manager.py +++ b/gns3server/modules/port_manager.py @@ -147,7 +147,7 @@ class PortManager: else: continue - raise HTTPConflict(reason="Could not find a free port between {} and {} on host {}, last exception: {}".format(start_port, + raise HTTPConflict(text="Could not find a free port between {} and {} on host {}, last exception: {}".format(start_port, end_port, host, last_exception)) @@ -174,7 +174,7 @@ class PortManager: """ if port in self._used_tcp_ports: - raise HTTPConflict(reason="TCP port already {} in use on host".format(port, self._console_host)) + raise HTTPConflict(text="TCP port already {} in use on host".format(port, self._console_host)) self._used_tcp_ports.add(port) return port @@ -209,7 +209,7 @@ class PortManager: """ if port in self._used_udp_ports: - raise Exception("UDP port already {} in use on host".format(port, self._host)) + raise HTTPConflict(text="UDP port already {} in use on host".format(port, self._console_host)) self._used_udp_ports.add(port) def release_udp_port(self, port): diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index 26694e21..9d98519c 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -15,13 +15,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import aiohttp import os import tempfile -from uuid import uuid4 +from uuid import UUID, uuid4 class Project: - """ A project contains a list of VM. In theory VM are isolated project/project. @@ -35,17 +35,23 @@ class Project: if uuid is None: self._uuid = str(uuid4()) else: - assert len(uuid) == 36 + try: + UUID(uuid, version=4) + except ValueError: + raise aiohttp.web.HTTPBadRequest(text="{} is not a valid UUID".format(uuid)) self._uuid = uuid self._location = location if location is None: self._location = tempfile.mkdtemp() - self._path = os.path.join(self._location, self._uuid) - if os.path.exists(self._path) is False: - os.mkdir(self._path) - os.mkdir(os.path.join(self._path, "vms")) + self._path = os.path.join(self._location, self._uuid, "vms") + try: + os.makedirs(self._path) + except FileExistsError: + pass + except OSError as e: + raise aiohttp.web.HTTPInternalServerError(text="Could not create project directory: {}".format(e)) @property def uuid(self): @@ -62,17 +68,21 @@ class Project: return self._path - def vm_working_directory(self, vm_identifier): + def vm_working_directory(self, vm_uuid): """ Return a working directory for a specific VM. If the directory doesn't exist, the directory is created. - :param vm_identifier: UUID of VM + :param vm_uuid: VM UUID """ - path = os.path.join(self._path, 'vms', vm_identifier) - if os.path.exists(path) is False: - os.mkdir(path) + path = os.path.join(self._path, "vms", vm_uuid) + try: + os.makedirs(self._path) + except FileExistsError: + pass + except OSError as e: + raise aiohttp.web.HTTPInternalServerError(text="Could not create VM working directory: {}".format(e)) return path def __json__(self): diff --git a/gns3server/modules/project_manager.py b/gns3server/modules/project_manager.py index cad199c4..f44ab0fd 100644 --- a/gns3server/modules/project_manager.py +++ b/gns3server/modules/project_manager.py @@ -17,6 +17,7 @@ import aiohttp from .project import Project +from uuid import UUID class ProjectManager: @@ -40,20 +41,23 @@ class ProjectManager: cls._instance = cls() return cls._instance - def get_project(self, project_id): + def get_project(self, project_uuid): """ Returns a Project instance. - :param project_id: Project identifier + :param project_uuid: Project UUID :returns: Project instance """ - assert len(project_id) == 36 + try: + UUID(project_uuid, version=4) + except ValueError: + raise aiohttp.web.HTTPBadRequest(text="{} is not a valid UUID".format(project_uuid)) - if project_id not in self._projects: - raise aiohttp.web.HTTPNotFound(text="Project UUID {} doesn't exist".format(project_id)) - return self._projects[project_id] + if project_uuid not in self._projects: + raise aiohttp.web.HTTPNotFound(text="Project UUID {} doesn't exist".format(project_uuid)) + return self._projects[project_uuid] def create_project(self, **kwargs): """ diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index f922e681..9a61f76b 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -29,6 +29,7 @@ import json import socket import time import asyncio +import shutil from .virtualbox_error import VirtualBoxError from ..adapters.ethernet_adapter import EthernetAdapter @@ -45,41 +46,44 @@ log = logging.getLogger(__name__) class VirtualBoxVM(BaseVM): - """ VirtualBox VM implementation. """ - _instances = [] - _allocated_console_ports = [] - - def __init__(self, name, uuid, project, manager): + def __init__(self, name, uuid, project, manager, vmname, linked_clone): super().__init__(name, uuid, project, manager) - self._system_properties = {} + # look for VBoxManage + self._vboxmanage_path = manager.config.get_section_config("VirtualBox").get("vboxmanage_path") + if not self._vboxmanage_path: + if sys.platform.startswith("win"): + if "VBOX_INSTALL_PATH" in os.environ: + self._vboxmanage_path = os.path.join(os.environ["VBOX_INSTALL_PATH"], "VBoxManage.exe") + elif "VBOX_MSI_INSTALL_PATH" in os.environ: + self._vboxmanage_path = os.path.join(os.environ["VBOX_MSI_INSTALL_PATH"], "VBoxManage.exe") + elif sys.platform.startswith("darwin"): + self._vboxmanage_path = "/Applications/VirtualBox.app/Contents/MacOS/VBoxManage" + else: + self._vboxmanage_path = shutil.which("vboxmanage") - # FIXME: harcoded values - if sys.platform.startswith("win"): - self._vboxmanage_path = r"C:\Program Files\Oracle\VirtualBox\VBoxManage.exe" - else: - self._vboxmanage_path = "/usr/bin/vboxmanage" + if not self._vboxmanage_path: + raise VirtualBoxError("Could not find VBoxManage") + if not os.access(self._vboxmanage_path, os.X_OK): + raise VirtualBoxError("VBoxManage is not executable") + self._vmname = vmname + self._started = False + self._linked_clone = linked_clone + self._system_properties = {} self._queue = asyncio.Queue() self._created = asyncio.Future() self._worker = asyncio.async(self._run()) return - self._linked_clone = linked_clone - self._working_dir = None self._command = [] - self._vboxmanage_path = vboxmanage_path self._vbox_user = vbox_user - self._started = False - self._console_host = console_host - self._console_start_port_range = console_start_port_range - self._console_end_port_range = console_end_port_range self._telnet_server_thread = None self._serial_pipe = None @@ -101,28 +105,6 @@ class VirtualBoxVM(BaseVM): # create the device own working directory self.working_dir = working_dir_path - if not self._console: - # allocate a console port - try: - self._console = find_unused_port(self._console_start_port_range, - self._console_end_port_range, - self._console_host, - ignore_ports=self._allocated_console_ports) - except Exception as e: - raise VirtualBoxError(e) - - if self._console in self._allocated_console_ports: - raise VirtualBoxError("Console port {} is already used by another VirtualBox VM".format(console)) - self._allocated_console_ports.append(self._console) - - self._system_properties = {} - properties = self._execute("list", ["systemproperties"]) - for prop in properties: - try: - name, value = prop.split(':', 1) - except ValueError: - continue - self._system_properties[name.strip()] = value.strip() if linked_clone: if vbox_id and os.path.isdir(os.path.join(self.working_dir, self._vmname)): diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index dfeda3e4..00ad5a73 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -16,7 +16,7 @@ # along with this program. If not, see . """ -VPCS vm management (creates command line, processes, files etc.) in +VPCS VM management (creates command line, processes, files etc.) in order to run an VPCS instance. """ @@ -59,7 +59,7 @@ class VPCSVM(BaseVM): super().__init__(name, uuid, project, manager) - self._path = self._config.get_section_config("VPCS").get("path", "vpcs") + self._path = manager.config.get_section_config("VPCS").get("path", "vpcs") self._console = console diff --git a/gns3server/web/route.py b/gns3server/web/route.py index cd1ece5d..b59acb4a 100644 --- a/gns3server/web/route.py +++ b/gns3server/web/route.py @@ -111,8 +111,8 @@ class Route(object): response.json({"message": e.text, "status": e.status}) except VMError as e: response = Response(route=route) - response.set_status(400) - response.json({"message": str(e), "status": 400}) + response.set_status(500) + response.json({"message": str(e), "status": 500}) return response cls._routes.append((method, cls._path, control_schema)) From ba91cbaac0ac6654d6f4b1291814e3be02ab50e5 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 20 Jan 2015 19:10:08 -0700 Subject: [PATCH 072/485] Remove find_unused_port from the attic. --- gns3server/modules/attic.py | 44 ------------------------------------- 1 file changed, 44 deletions(-) diff --git a/gns3server/modules/attic.py b/gns3server/modules/attic.py index be09b57a..6331317b 100644 --- a/gns3server/modules/attic.py +++ b/gns3server/modules/attic.py @@ -30,50 +30,6 @@ import logging log = logging.getLogger(__name__) -def find_unused_port(start_port, end_port, host='127.0.0.1', socket_type="TCP", ignore_ports=[]): - """ - Finds an unused port in a range. - - :param start_port: first port in the range - :param end_port: last port in the range - :param host: host/address for bind() - :param socket_type: TCP (default) or UDP - :param ignore_ports: list of port to ignore within the range - """ - - if end_port < start_port: - raise Exception("Invalid port range {}-{}".format(start_port, end_port)) - - if socket_type == "UDP": - socket_type = socket.SOCK_DGRAM - else: - socket_type = socket.SOCK_STREAM - - last_exception = None - for port in range(start_port, end_port + 1): - if port in ignore_ports: - continue - try: - if ":" in host: - # IPv6 address support - with socket.socket(socket.AF_INET6, socket_type) as s: - s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - s.bind((host, port)) # the port is available if bind is a success - else: - with socket.socket(socket.AF_INET, socket_type) as s: - s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - s.bind((host, port)) # the port is available if bind is a success - return port - except OSError as e: - last_exception = e - if port + 1 == end_port: - break - else: - continue - - raise Exception("Could not find a free port between {} and {} on host {}, last exception: {}".format(start_port, end_port, host, last_exception)) - - def wait_socket_is_ready(host, port, wait=2.0, socket_timeout=10): """ Waits for a socket to be ready for wait time. From df31b2ad5af1c75a24c3f919405dc64f59b53cfe Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 21 Jan 2015 11:33:24 +0100 Subject: [PATCH 073/485] Fix project path --- docs/api/examples/post_vpcs.txt | 2 +- gns3server/modules/attic.py | 1 + gns3server/modules/base_vm.py | 2 +- gns3server/modules/port_manager.py | 6 +++--- gns3server/modules/project.py | 15 +++++++-------- gns3server/modules/virtualbox/virtualbox_vm.py | 2 -- gns3server/modules/vpcs/vpcs_vm.py | 1 + tests/api/base.py | 3 ++- tests/modules/test_project.py | 4 ++-- 9 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/api/examples/post_vpcs.txt b/docs/api/examples/post_vpcs.txt index d5007ba4..1e529a62 100644 --- a/docs/api/examples/post_vpcs.txt +++ b/docs/api/examples/post_vpcs.txt @@ -20,5 +20,5 @@ X-ROUTE: /vpcs "name": "PC TEST 1", "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, - "uuid": "934f0745-0824-451e-8ad6-db0877dbf387" + "uuid": "21e9a130-450e-41e4-8864-e5c83ba7aa80" } diff --git a/gns3server/modules/attic.py b/gns3server/modules/attic.py index 6331317b..98e2413b 100644 --- a/gns3server/modules/attic.py +++ b/gns3server/modules/attic.py @@ -25,6 +25,7 @@ import struct import socket import stat import time +import aiohttp import logging log = logging.getLogger(__name__) diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index 001d5b1e..e1e19d1d 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -87,7 +87,7 @@ class BaseVM: Return VM working directory """ - return self._project.vm_working_directory(self._uuid) + return self._project.vm_working_directory(self.module_name, self._uuid) def create(self): """ diff --git a/gns3server/modules/port_manager.py b/gns3server/modules/port_manager.py index 31a38a4e..48d7e8af 100644 --- a/gns3server/modules/port_manager.py +++ b/gns3server/modules/port_manager.py @@ -148,9 +148,9 @@ class PortManager: continue raise HTTPConflict(text="Could not find a free port between {} and {} on host {}, last exception: {}".format(start_port, - end_port, - host, - last_exception)) + end_port, + host, + last_exception)) def get_free_console_port(self): """ diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index 9d98519c..c924a9d5 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -45,11 +45,9 @@ class Project: if location is None: self._location = tempfile.mkdtemp() - self._path = os.path.join(self._location, self._uuid, "vms") + self._path = os.path.join(self._location, self._uuid) try: - os.makedirs(self._path) - except FileExistsError: - pass + os.makedirs(os.path.join(self._path, 'vms'), exist_ok=True) except OSError as e: raise aiohttp.web.HTTPInternalServerError(text="Could not create project directory: {}".format(e)) @@ -68,22 +66,23 @@ class Project: return self._path - def vm_working_directory(self, vm_uuid): + def vm_working_directory(self, module, vm_uuid): """ Return a working directory for a specific VM. If the directory doesn't exist, the directory is created. + :param module: The module name (vpcs, dynamips...) :param vm_uuid: VM UUID """ - path = os.path.join(self._path, "vms", vm_uuid) + p = os.path.join(self._path, "vms", module, vm_uuid) try: - os.makedirs(self._path) + os.makedirs(p, exist_ok=True) except FileExistsError: pass except OSError as e: raise aiohttp.web.HTTPInternalServerError(text="Could not create VM working directory: {}".format(e)) - return path + return p def __json__(self): diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 9a61f76b..772beedc 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -33,7 +33,6 @@ import shutil from .virtualbox_error import VirtualBoxError from ..adapters.ethernet_adapter import EthernetAdapter -from ..attic import find_unused_port from .telnet_server import TelnetServer from ..base_vm import BaseVM @@ -105,7 +104,6 @@ class VirtualBoxVM(BaseVM): # create the device own working directory self.working_dir = working_dir_path - if linked_clone: if vbox_id and os.path.isdir(os.path.join(self.working_dir, self._vmname)): vbox_file = os.path.join(self.working_dir, self._vmname, self._vmname + ".vbox") diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index 00ad5a73..c92f703c 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -43,6 +43,7 @@ log = logging.getLogger(__name__) class VPCSVM(BaseVM): + module_name = 'vpcs' """ VPCS vm implementation. diff --git a/tests/api/base.py b/tests/api/base.py index 63607498..e8fac3f8 100644 --- a/tests/api/base.py +++ b/tests/api/base.py @@ -151,11 +151,12 @@ def server(request, loop): port = _get_unused_port() host = "localhost" app = web.Application() + port_manager = PortManager("127.0.0.1", False) for method, route, handler in Route.get_routes(): app.router.add_route(method, route, handler) for module in MODULES: instance = module.instance() - instance.port_manager = PortManager("127.0.0.1", False) + instance.port_manager = port_manager srv = loop.create_server(app.make_handler(), host, port) srv = loop.run_until_complete(srv) diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py index 4cfdc764..cb980b76 100644 --- a/tests/modules/test_project.py +++ b/tests/modules/test_project.py @@ -47,5 +47,5 @@ def test_json(tmpdir): def test_vm_working_directory(tmpdir): p = Project(location=str(tmpdir)) - assert os.path.exists(p.vm_working_directory('00010203-0405-0607-0809-0a0b0c0d0e0f')) - assert os.path.exists(os.path.join(str(tmpdir), p.uuid, 'vms', '00010203-0405-0607-0809-0a0b0c0d0e0f')) + assert os.path.exists(p.vm_working_directory('vpcs', '00010203-0405-0607-0809-0a0b0c0d0e0f')) + assert os.path.exists(os.path.join(str(tmpdir), p.uuid, 'vms', 'vpcs', '00010203-0405-0607-0809-0a0b0c0d0e0f')) From f99538ccefe1d7784fe8c68ea193d98b99cd4c0d Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 21 Jan 2015 15:50:35 +0100 Subject: [PATCH 074/485] Cleanup test --- tests/modules/vpcs/test_vpcs_vm.py | 31 +++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/tests/modules/vpcs/test_vpcs_vm.py b/tests/modules/vpcs/test_vpcs_vm.py index a39676c7..4e4f5c96 100644 --- a/tests/modules/vpcs/test_vpcs_vm.py +++ b/tests/modules/vpcs/test_vpcs_vm.py @@ -36,16 +36,22 @@ def manager(): return m +@pytest.fixture(scope="function") +def vm(project, manager): + return VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + + def test_vm(project, manager): vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) assert vm.name == "test" assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0f" -def test_vm_invalid_vpcs_version(project, manager, loop): +def test_vm_invalid_vpcs_version(loop, project, manager): with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM._get_vpcs_welcome", return_value="Welcome to Virtual PC Simulator, version 0.1"): with pytest.raises(VPCSError): vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) loop.run_until_complete(asyncio.async(vm.start())) assert vm.name == "test" assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0f" @@ -54,27 +60,26 @@ def test_vm_invalid_vpcs_version(project, manager, loop): @patch("gns3server.config.Config.get_section_config", return_value={"path": "/bin/test_fake"}) def test_vm_invalid_vpcs_path(project, manager, loop): with pytest.raises(VPCSError): - vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0e", project, manager) + vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) loop.run_until_complete(asyncio.async(vm.start())) assert vm.name == "test" - assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0f" + assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0e" -def test_start(project, loop, manager): +def test_start(loop, vm): with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM._check_requirements", return_value=True): with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): - vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) loop.run_until_complete(asyncio.async(vm.start())) assert vm.is_running() -def test_stop(project, loop, manager): +def test_stop(loop, vm): process = MagicMock() with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM._check_requirements", return_value=True): with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): - vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) loop.run_until_complete(asyncio.async(vm.start())) @@ -84,29 +89,25 @@ def test_stop(project, loop, manager): process.terminate.assert_called_with() -def test_add_nio_binding_udp(manager, project): - vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) +def test_add_nio_binding_udp(vm): nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) assert nio.lport == 4242 -def test_add_nio_binding_tap(project, manager): - vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) +def test_add_nio_binding_tap(vm): with patch("gns3server.modules.vpcs.vpcs_vm.has_privileged_access", return_value=True): nio = vm.port_add_nio_binding(0, {"type": "nio_tap", "tap_device": "test"}) assert nio.tap_device == "test" -def test_add_nio_binding_tap_no_privileged_access(manager, project): - vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) +def test_add_nio_binding_tap_no_privileged_access(vm): with patch("gns3server.modules.vpcs.vpcs_vm.has_privileged_access", return_value=False): with pytest.raises(VPCSError): vm.port_add_nio_binding(0, {"type": "nio_tap", "tap_device": "test"}) assert vm._ethernet_adapter.ports[0] is None -def test_port_remove_nio_binding(manager, project): - vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) +def test_port_remove_nio_binding(vm): nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) vm.port_remove_nio_binding(0) assert vm._ethernet_adapter.ports[0] is None From 87a089457f483ef67252cd42b8b7463fa712f5f9 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 21 Jan 2015 16:43:34 +0100 Subject: [PATCH 075/485] Update script file --- .gitignore | 1 + docs/api/examples/post_vpcs.txt | 5 ++-- gns3server/handlers/vpcs_handler.py | 3 ++- gns3server/modules/vpcs/vpcs_vm.py | 41 ++++++++++++++++++++++++----- gns3server/schemas/vpcs.py | 8 ++++++ tests/api/test_vpcs.py | 19 ++++++++++--- tests/modules/vpcs/test_vpcs_vm.py | 25 ++++++++++++++++++ 7 files changed, 90 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index c2cb81d9..d63cf29f 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,4 @@ docs/_build #VPCS vpcs.hist +startup.vpcs diff --git a/docs/api/examples/post_vpcs.txt b/docs/api/examples/post_vpcs.txt index 1e529a62..bcd8a539 100644 --- a/docs/api/examples/post_vpcs.txt +++ b/docs/api/examples/post_vpcs.txt @@ -9,7 +9,7 @@ POST /vpcs HTTP/1.1 HTTP/1.1 200 CONNECTION: close -CONTENT-LENGTH: 185 +CONTENT-LENGTH: 213 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT SERVER: Python/3.4 aiohttp/0.13.1 @@ -20,5 +20,6 @@ X-ROUTE: /vpcs "name": "PC TEST 1", "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, - "uuid": "21e9a130-450e-41e4-8864-e5c83ba7aa80" + "startup_script": null, + "uuid": "009a7260-e44c-4349-8df7-08668a3c4e17" } diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index 5d4e9ac8..007b95dd 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -46,7 +46,8 @@ class VPCSHandler: request.json["project_uuid"], request.json.get("uuid"), console=request.json.get("console"), - script_file=request.json.get("script_file")) + script_file=request.json.get("script_file"), + startup_script=request.json.get("startup_script")) response.json(vm) @classmethod diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index c92f703c..cabc6df4 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -54,9 +54,10 @@ class VPCSVM(BaseVM): :param manager: parent VM Manager :param console: TCP console port :param script_file: A VPCS startup script + :param startup_script: Content of vpcs startup script file """ - def __init__(self, name, uuid, project, manager, console=None, script_file=None): + def __init__(self, name, uuid, project, manager, console=None, script_file=None, startup_script=None): super().__init__(name, uuid, project, manager) @@ -71,6 +72,8 @@ class VPCSVM(BaseVM): # VPCS settings self._script_file = script_file + if startup_script is not None: + self.startup_script = startup_script self._ethernet_adapter = EthernetAdapter() # one adapter with 1 Ethernet interface if self._console is not None: @@ -107,7 +110,8 @@ class VPCSVM(BaseVM): "uuid": self._uuid, "console": self._console, "project_uuid": self.project.uuid, - "script_file": self.script_file} + "script_file": self.script_file, + "startup_script": self.startup_script} @property def console(self): @@ -146,6 +150,33 @@ class VPCSVM(BaseVM): new_name=new_name)) BaseVM.name = new_name + @property + def startup_script(self): + """Return the content of the current startup script""" + if self._script_file is None: + return None + try: + with open(self._script_file) as f: + return f.read() + except OSError as e: + raise VPCSError("Can't read VPCS startup file '{}'".format(self._script_file)) + + @startup_script.setter + def startup_script(self, startup_script): + """ + Update the startup script + + :param startup_script The content of the vpcs startup script + """ + + if self._script_file is None: + self._script_file = os.path.join(self.working_dir, 'startup.vpcs') + try: + with open(self._script_file, '+w') as f: + f.write(startup_script) + except OSError as e: + raise VPCSError("Can't write VPCS startup file '{}'".format(self._script_file)) + @asyncio.coroutine def _check_vpcs_version(self): """ @@ -165,8 +196,8 @@ class VPCSVM(BaseVM): @asyncio.coroutine def _get_vpcs_welcome(self): - proc = yield from asyncio.create_subprocess_exec(' '.join([self._path, "-v"]), stdout=asyncio.subprocess.PIPE, cwd=self.working_dir) - out = yield from proc.stdout.readline() + proc = yield from asyncio.create_subprocess_exec(self._path, "-v", stdout=asyncio.subprocess.PIPE, cwd=self.working_dir) + out = yield from proc.stdout.read() return out.decode("utf-8") @asyncio.coroutine @@ -357,8 +388,6 @@ class VPCSVM(BaseVM): nio = self._ethernet_adapter.get_nio(0) if nio: - print(nio) - print(isinstance(nio, NIO_UDP)) if isinstance(nio, NIO_UDP): # UDP tunnel command.extend(["-s", str(nio.lport)]) # source UDP port diff --git a/gns3server/schemas/vpcs.py b/gns3server/schemas/vpcs.py index ea54ee0c..a0190ee6 100644 --- a/gns3server/schemas/vpcs.py +++ b/gns3server/schemas/vpcs.py @@ -54,6 +54,10 @@ VPCS_CREATE_SCHEMA = { "description": "VPCS startup script", "type": ["string", "null"] }, + "startup_script": { + "description": "Content of the VPCS startup script", + "type": ["string", "null"] + }, }, "additionalProperties": False, "required": ["name", "project_uuid"] @@ -152,6 +156,10 @@ VPCS_OBJECT_SCHEMA = { "description": "VPCS startup script", "type": ["string", "null"] }, + "startup_script": { + "description": "Content of the VPCS startup script", + "type": ["string", "null"] + }, }, "additionalProperties": False, "required": ["name", "uuid", "console", "project_uuid"] diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index 0df6882f..7b13e389 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -16,6 +16,7 @@ # along with this program. If not, see . import pytest +import os from tests.api.base import server, loop, project from tests.utils import asyncio_patch from unittest.mock import patch @@ -38,13 +39,25 @@ def test_vpcs_create(server, project): assert response.json["script_file"] is None -def test_vpcs_create_script_file(server, project): - response = server.post("/vpcs", {"name": "PC TEST 1", "project_uuid": project.uuid, "script_file": "/tmp/test"}) +def test_vpcs_create_script_file(server, project, tmpdir): + path = os.path.join(str(tmpdir), "test") + with open(path, 'w+') as f: + f.write("ip 192.168.1.2") + response = server.post("/vpcs", {"name": "PC TEST 1", "project_uuid": project.uuid, "script_file": path}) assert response.status == 200 assert response.route == "/vpcs" assert response.json["name"] == "PC TEST 1" assert response.json["project_uuid"] == project.uuid - assert response.json["script_file"] == "/tmp/test" + assert response.json["script_file"] == path + + +def test_vpcs_create_startup_script(server, project): + response = server.post("/vpcs", {"name": "PC TEST 1", "project_uuid": project.uuid, "startup_script": "ip 192.168.1.2\necho TEST"}) + assert response.status == 200 + assert response.route == "/vpcs" + assert response.json["name"] == "PC TEST 1" + assert response.json["project_uuid"] == project.uuid + assert response.json["startup_script"] == "ip 192.168.1.2\necho TEST" def test_vpcs_create_port(server, project): diff --git a/tests/modules/vpcs/test_vpcs_vm.py b/tests/modules/vpcs/test_vpcs_vm.py index 4e4f5c96..a26b99b9 100644 --- a/tests/modules/vpcs/test_vpcs_vm.py +++ b/tests/modules/vpcs/test_vpcs_vm.py @@ -17,6 +17,7 @@ import pytest import asyncio +import os from tests.utils import asyncio_patch # TODO: Move loop to util @@ -111,3 +112,27 @@ def test_port_remove_nio_binding(vm): nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) vm.port_remove_nio_binding(0) assert vm._ethernet_adapter.ports[0] is None + + +def test_update_startup_script(vm): + content = "echo GNS3 VPCS\nip 192.168.1.2\n" + vm.startup_script = content + filepath = os.path.join(vm.working_dir, 'startup.vpcs') + assert os.path.exists(filepath) + with open(filepath) as f: + assert f.read() == content + + +def test_update_startup_script(vm): + content = "echo GNS3 VPCS\nip 192.168.1.2\n" + vm.startup_script = content + filepath = os.path.join(vm.working_dir, 'startup.vpcs') + assert os.path.exists(filepath) + with open(filepath) as f: + assert f.read() == content + + +def test_get_startup_script(vm): + content = "echo GNS3 VPCS\nip 192.168.1.2\n" + vm.startup_script = content + assert vm.startup_script == content From ce9fd3cb255f92a05eb36ce716d66254fc165bea Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 21 Jan 2015 17:11:21 +0100 Subject: [PATCH 076/485] Test start / stop. And check if the mocked function is really called --- docs/api/examples/post_vpcs.txt | 2 +- tests/api/base.py | 2 +- tests/api/test_vpcs.py | 18 +++++++++++------- tests/utils.py | 3 ++- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/docs/api/examples/post_vpcs.txt b/docs/api/examples/post_vpcs.txt index bcd8a539..395b109e 100644 --- a/docs/api/examples/post_vpcs.txt +++ b/docs/api/examples/post_vpcs.txt @@ -21,5 +21,5 @@ X-ROUTE: /vpcs "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, "startup_script": null, - "uuid": "009a7260-e44c-4349-8df7-08668a3c4e17" + "uuid": "6370f75e-0a48-4e2b-95a8-0140da6ef1fb" } diff --git a/tests/api/base.py b/tests/api/base.py index e8fac3f8..6de05b45 100644 --- a/tests/api/base.py +++ b/tests/api/base.py @@ -42,7 +42,7 @@ class Query: self._port = port self._host = host - def post(self, path, body, **kwargs): + def post(self, path, body={}, **kwargs): return self._fetch("POST", path, body, **kwargs) def get(self, path, **kwargs): diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index 7b13e389..00ce14f4 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -19,7 +19,7 @@ import pytest import os from tests.api.base import server, loop, project from tests.utils import asyncio_patch -from unittest.mock import patch +from unittest.mock import patch, Mock from gns3server.modules.vpcs.vpcs_vm import VPCSVM @@ -99,11 +99,15 @@ def test_vpcs_delete_nio(server, vm): assert response.route == "/vpcs/{uuid}/ports/{port_id}/nio" -def test_vpcs_start(): - # assert True == False - pass +def test_vpcs_start(server, vm): + with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM.start", return_value=True) as mock: + response = server.post("/vpcs/{}/start".format(vm["uuid"])) + assert mock.called + assert response.status == 200 -def test_vpcs_stop(): - # assert True == False - pass +def test_vpcs_stop(server, vm): + with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM.stop", return_value=True) as mock: + response = server.post("/vpcs/{}/stop".format(vm["uuid"])) + assert mock.called + assert response.status == 200 diff --git a/tests/utils.py b/tests/utils.py index 3ce32530..bc8dc5dc 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -39,7 +39,8 @@ class _asyncio_patch: def __enter__(self): """Used when enter in the with block""" self._patcher = patch(self.function, return_value=self._fake_anwser()) - self._patcher.start() + mock_class = self._patcher.start() + return mock_class def __exit__(self, *exc_info): """Used when leaving the with block""" From 7abb426d0476573d4aabe67c9b14878afc3ac943 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 21 Jan 2015 17:21:17 +0100 Subject: [PATCH 077/485] Get informations about a VPCS instance --- docs/api/examples/get_vpcsuuid.txt | 22 +++++++++++++++++++ .../api/examples/post_virtualboxuuidstart.txt | 15 +++++++++++++ docs/api/examples/post_virtualboxuuidstop.txt | 15 +++++++++++++ docs/api/examples/post_vpcs.txt | 2 +- gns3server/handlers/vpcs_handler.py | 17 ++++++++++++++ tests/api/test_vpcs.py | 8 +++++++ 6 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 docs/api/examples/get_vpcsuuid.txt create mode 100644 docs/api/examples/post_virtualboxuuidstart.txt create mode 100644 docs/api/examples/post_virtualboxuuidstop.txt diff --git a/docs/api/examples/get_vpcsuuid.txt b/docs/api/examples/get_vpcsuuid.txt new file mode 100644 index 00000000..797f00bd --- /dev/null +++ b/docs/api/examples/get_vpcsuuid.txt @@ -0,0 +1,22 @@ +curl -i -X GET 'http://localhost:8000/vpcs/{uuid}' + +GET /vpcs/{uuid} HTTP/1.1 + + + +HTTP/1.1 200 +CONNECTION: close +CONTENT-LENGTH: 213 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 aiohttp/0.13.1 +X-ROUTE: /vpcs/{uuid} + +{ + "console": 2001, + "name": "PC TEST 1", + "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", + "script_file": null, + "startup_script": null, + "uuid": "40f76457-de2b-4399-8853-a35393a72a2d" +} diff --git a/docs/api/examples/post_virtualboxuuidstart.txt b/docs/api/examples/post_virtualboxuuidstart.txt new file mode 100644 index 00000000..dee153b5 --- /dev/null +++ b/docs/api/examples/post_virtualboxuuidstart.txt @@ -0,0 +1,15 @@ +curl -i -X POST 'http://localhost:8000/virtualbox/{uuid}/start' -d '{}' + +POST /virtualbox/{uuid}/start HTTP/1.1 +{} + + +HTTP/1.1 200 +CONNECTION: close +CONTENT-LENGTH: 2 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 aiohttp/0.13.1 +X-ROUTE: /virtualbox/{uuid}/start + +{} diff --git a/docs/api/examples/post_virtualboxuuidstop.txt b/docs/api/examples/post_virtualboxuuidstop.txt new file mode 100644 index 00000000..5bcfc6af --- /dev/null +++ b/docs/api/examples/post_virtualboxuuidstop.txt @@ -0,0 +1,15 @@ +curl -i -X POST 'http://localhost:8000/virtualbox/{uuid}/stop' -d '{}' + +POST /virtualbox/{uuid}/stop HTTP/1.1 +{} + + +HTTP/1.1 200 +CONNECTION: close +CONTENT-LENGTH: 2 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 aiohttp/0.13.1 +X-ROUTE: /virtualbox/{uuid}/stop + +{} diff --git a/docs/api/examples/post_vpcs.txt b/docs/api/examples/post_vpcs.txt index 395b109e..1977fc1d 100644 --- a/docs/api/examples/post_vpcs.txt +++ b/docs/api/examples/post_vpcs.txt @@ -21,5 +21,5 @@ X-ROUTE: /vpcs "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, "startup_script": null, - "uuid": "6370f75e-0a48-4e2b-95a8-0140da6ef1fb" + "uuid": "a022aa0d-acab-4554-b2a6-6e6f51c9d65e" } diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index 007b95dd..df6788f7 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -50,6 +50,23 @@ class VPCSHandler: startup_script=request.json.get("startup_script")) response.json(vm) + @classmethod + @Route.get( + r"/vpcs/{uuid}", + parameters={ + "uuid": "VPCS instance UUID" + }, + status_codes={ + 200: "VPCS instance started", + 404: "VPCS instance doesn't exist" + }, + description="Get a VPCS instance") + def show(request, response): + + vpcs_manager = VPCS.instance() + vm = vpcs_manager.get_vm(request.match_info["uuid"]) + response.json(vm) + @classmethod @Route.post( r"/vpcs/{uuid}/start", diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index 00ce14f4..0b24753f 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -39,6 +39,14 @@ def test_vpcs_create(server, project): assert response.json["script_file"] is None +def test_vpcs_get(server, project, vm): + response = server.get("/vpcs/{}".format(vm["uuid"]), example=True) + assert response.status == 200 + assert response.route == "/vpcs/{uuid}" + assert response.json["name"] == "PC TEST 1" + assert response.json["project_uuid"] == project.uuid + + def test_vpcs_create_script_file(server, project, tmpdir): path = os.path.join(str(tmpdir), "test") with open(path, 'w+') as f: From 368d1ff70b214e04ad85ecc3b8e421cbcce2abac Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 21 Jan 2015 21:46:16 +0100 Subject: [PATCH 078/485] Update VPCS instance --- docs/api/examples/get_vpcsuuid.txt | 2 +- docs/api/examples/post_vpcs.txt | 2 +- gns3server/handlers/vpcs_handler.py | 22 +++++++++++++++++ gns3server/modules/base_vm.py | 4 +++ gns3server/modules/vpcs/vpcs_vm.py | 38 +++++++++++++++-------------- gns3server/schemas/vpcs.py | 27 ++++++++++++++++++++ tests/api/base.py | 6 +++-- tests/api/test_project.py | 2 +- tests/api/test_version.py | 2 +- tests/api/test_virtualbox.py | 2 +- tests/api/test_vpcs.py | 23 ++++++++++++++--- tests/modules/vpcs/test_vpcs_vm.py | 27 +++++++++++++++++++- tests/utils.py | 18 ++++++++++++++ 13 files changed, 145 insertions(+), 30 deletions(-) diff --git a/docs/api/examples/get_vpcsuuid.txt b/docs/api/examples/get_vpcsuuid.txt index 797f00bd..1abe5fd9 100644 --- a/docs/api/examples/get_vpcsuuid.txt +++ b/docs/api/examples/get_vpcsuuid.txt @@ -18,5 +18,5 @@ X-ROUTE: /vpcs/{uuid} "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, "startup_script": null, - "uuid": "40f76457-de2b-4399-8853-a35393a72a2d" + "uuid": "f8155d67-c0bf-4229-be4c-97edaaae7b0b" } diff --git a/docs/api/examples/post_vpcs.txt b/docs/api/examples/post_vpcs.txt index 1977fc1d..26ee627f 100644 --- a/docs/api/examples/post_vpcs.txt +++ b/docs/api/examples/post_vpcs.txt @@ -21,5 +21,5 @@ X-ROUTE: /vpcs "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, "startup_script": null, - "uuid": "a022aa0d-acab-4554-b2a6-6e6f51c9d65e" + "uuid": "5a9aac64-5b62-41bd-955a-fcef90a2fac5" } diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index df6788f7..633a668e 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -17,6 +17,7 @@ from ..web.route import Route from ..schemas.vpcs import VPCS_CREATE_SCHEMA +from ..schemas.vpcs import VPCS_UPDATE_SCHEMA from ..schemas.vpcs import VPCS_OBJECT_SCHEMA from ..schemas.vpcs import VPCS_NIO_SCHEMA from ..modules.vpcs import VPCS @@ -67,6 +68,27 @@ class VPCSHandler: vm = vpcs_manager.get_vm(request.match_info["uuid"]) response.json(vm) + @classmethod + @Route.put( + r"/vpcs/{uuid}", + status_codes={ + 200: "VPCS instance updated", + 409: "Conflict" + }, + description="Update a VPCS instance", + input=VPCS_UPDATE_SCHEMA, + output=VPCS_OBJECT_SCHEMA) + def update(request, response): + + vpcs_manager = VPCS.instance() + vm = vpcs_manager.get_vm(request.match_info["uuid"]) + vm.name = request.json.get("name", vm.name) + vm.console = request.json.get("console", vm.console) + vm.script_file = request.json.get("script_file", vm.script_file) + vm.startup_script = request.json.get("startup_script", vm.startup_script) + + response.json(vm) + @classmethod @Route.post( r"/vpcs/{uuid}/start", diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index e1e19d1d..c4531fba 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -59,6 +59,10 @@ class BaseVM: :param new_name: name """ + log.info("{module} {name} [{uuid}]: renamed to {new_name}".format(module=self.module_name, + name=self._name, + uuid=self.uuid, + new_name=new_name)) self._name = new_name @property diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index cabc6df4..df7dc5fc 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -123,7 +123,17 @@ class VPCSVM(BaseVM): return self._console - # FIXME: correct way to subclass a property? + @console.setter + def console(self, console): + """ + Change console port + + :params console: Console port (integer) + """ + if self._console: + self._manager.port_manager.release_console_port(self._console) + self._console = self._manager.port_manager.reserve_console_port(console) + @BaseVM.name.setter def name(self, new_name): """ @@ -133,22 +143,11 @@ class VPCSVM(BaseVM): """ if self._script_file: - # update the startup.vpc - config_path = os.path.join(self.working_dir, "startup.vpc") - if os.path.isfile(config_path): - try: - with open(config_path, "r+", errors="replace") as f: - old_config = f.read() - new_config = old_config.replace(self._name, new_name) - f.seek(0) - f.write(new_config) - except OSError as e: - raise VPCSError("Could not amend the configuration {}: {}".format(config_path, e)) - - log.info("VPCS {name} [{uuid}]: renamed to {new_name}".format(name=self._name, - uuid=self.uuid, - new_name=new_name)) - BaseVM.name = new_name + content = self.startup_script + content = content.replace(self._name, new_name) + self.startup_script = content + + super(VPCSVM, VPCSVM).name.__set__(self, new_name) @property def startup_script(self): @@ -173,7 +172,10 @@ class VPCSVM(BaseVM): self._script_file = os.path.join(self.working_dir, 'startup.vpcs') try: with open(self._script_file, '+w') as f: - f.write(startup_script) + if startup_script is None: + f.write('') + else: + f.write(startup_script) except OSError as e: raise VPCSError("Can't write VPCS startup file '{}'".format(self._script_file)) diff --git a/gns3server/schemas/vpcs.py b/gns3server/schemas/vpcs.py index a0190ee6..437651bb 100644 --- a/gns3server/schemas/vpcs.py +++ b/gns3server/schemas/vpcs.py @@ -63,6 +63,33 @@ VPCS_CREATE_SCHEMA = { "required": ["name", "project_uuid"] } +VPCS_UPDATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to update a VPCS instance", + "type": "object", + "properties": { + "name": { + "description": "VPCS device name", + "type": ["string", "null"], + "minLength": 1, + }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": ["integer", "null"] + }, + "script_file": { + "description": "VPCS startup script", + "type": ["string", "null"] + }, + "startup_script": { + "description": "Content of the VPCS startup script", + "type": ["string", "null"] + }, + }, + "additionalProperties": False, +} VPCS_NIO_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", diff --git a/tests/api/base.py b/tests/api/base.py index 6de05b45..4664f41a 100644 --- a/tests/api/base.py +++ b/tests/api/base.py @@ -45,6 +45,9 @@ class Query: def post(self, path, body={}, **kwargs): return self._fetch("POST", path, body, **kwargs) + def put(self, path, body={}, **kwargs): + return self._fetch("PUT", path, body, **kwargs) + def get(self, path, **kwargs): return self._fetch("GET", path, **kwargs) @@ -147,11 +150,10 @@ def loop(request): @pytest.fixture(scope="session") -def server(request, loop): +def server(request, loop, port_manager): port = _get_unused_port() host = "localhost" app = web.Application() - port_manager = PortManager("127.0.0.1", False) for method, route, handler in Route.get_routes(): app.router.add_route(method, route, handler) for module in MODULES: diff --git a/tests/api/test_project.py b/tests/api/test_project.py index cf2297c0..b7ae8b55 100644 --- a/tests/api/test_project.py +++ b/tests/api/test_project.py @@ -20,7 +20,7 @@ This test suite check /project endpoint """ -from tests.utils import asyncio_patch +from tests.utils import asyncio_patch, port_manager from tests.api.base import server, loop from gns3server.version import __version__ diff --git a/tests/api/test_version.py b/tests/api/test_version.py index 110f7b1a..5b479006 100644 --- a/tests/api/test_version.py +++ b/tests/api/test_version.py @@ -20,7 +20,7 @@ This test suite check /version endpoint It's also used for unittest the HTTP implementation. """ -from tests.utils import asyncio_patch +from tests.utils import asyncio_patch, port_manager from tests.api.base import server, loop from gns3server.version import __version__ diff --git a/tests/api/test_virtualbox.py b/tests/api/test_virtualbox.py index c7b8856c..3d9e56c3 100644 --- a/tests/api/test_virtualbox.py +++ b/tests/api/test_virtualbox.py @@ -16,7 +16,7 @@ # along with this program. If not, see . from tests.api.base import server, loop, project -from tests.utils import asyncio_patch +from tests.utils import asyncio_patch, port_manager from gns3server.modules.virtualbox.virtualbox_vm import VirtualBoxVM diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index 0b24753f..56b5bc56 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -18,7 +18,7 @@ import pytest import os from tests.api.base import server, loop, project -from tests.utils import asyncio_patch +from tests.utils import asyncio_patch, free_console_port, port_manager from unittest.mock import patch, Mock from gns3server.modules.vpcs.vpcs_vm import VPCSVM @@ -68,13 +68,13 @@ def test_vpcs_create_startup_script(server, project): assert response.json["startup_script"] == "ip 192.168.1.2\necho TEST" -def test_vpcs_create_port(server, project): - response = server.post("/vpcs", {"name": "PC TEST 1", "project_uuid": project.uuid, "console": 4242}) +def test_vpcs_create_port(server, project, free_console_port): + response = server.post("/vpcs", {"name": "PC TEST 1", "project_uuid": project.uuid, "console": free_console_port}) assert response.status == 200 assert response.route == "/vpcs" assert response.json["name"] == "PC TEST 1" assert response.json["project_uuid"] == project.uuid - assert response.json["console"] == 4242 + assert response.json["console"] == free_console_port def test_vpcs_nio_create_udp(server, vm): @@ -119,3 +119,18 @@ def test_vpcs_stop(server, vm): response = server.post("/vpcs/{}/stop".format(vm["uuid"])) assert mock.called assert response.status == 200 + + +def test_vpcs_update(server, vm, tmpdir, free_console_port): + path = os.path.join(str(tmpdir), 'startup2.vpcs') + with open(path, 'w+') as f: + f.write(path) + response = server.put("/vpcs/{}".format(vm["uuid"]), {"name": "test", + "console": free_console_port, + "script_file": path, + "startup_script": "ip 192.168.1.1"}) + assert response.status == 200 + assert response.json["name"] == "test" + assert response.json["console"] == free_console_port + assert response.json["script_file"] == path + assert response.json["startup_script"] == "ip 192.168.1.1" diff --git a/tests/modules/vpcs/test_vpcs_vm.py b/tests/modules/vpcs/test_vpcs_vm.py index a26b99b9..1cd27646 100644 --- a/tests/modules/vpcs/test_vpcs_vm.py +++ b/tests/modules/vpcs/test_vpcs_vm.py @@ -18,7 +18,7 @@ import pytest import asyncio import os -from tests.utils import asyncio_patch +from tests.utils import asyncio_patch, port_manager, free_console_port # TODO: Move loop to util from tests.api.base import loop, project @@ -136,3 +136,28 @@ def test_get_startup_script(vm): content = "echo GNS3 VPCS\nip 192.168.1.2\n" vm.startup_script = content assert vm.startup_script == content + + +def test_change_console_port(vm, free_console_port): + vm.console = free_console_port + vm.console = free_console_port + 1 + assert vm.console == free_console_port + PortManager.instance().reserve_console_port(free_console_port + 1) + + +def test_change_name(vm, tmpdir): + path = os.path.join(str(tmpdir), 'startup.vpcs') + vm.name = "world" + with open(path, 'w+') as f: + f.write("name world") + vm.script_file = path + vm.name = "hello" + assert vm.name == "hello" + with open(path) as f: + assert f.read() == "name hello" + + +def test_change_script_file(vm, tmpdir): + path = os.path.join(str(tmpdir), 'startup2.vpcs') + vm.script_file = path + assert vm.script_file == path diff --git a/tests/utils.py b/tests/utils.py index bc8dc5dc..55069f5e 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -16,7 +16,10 @@ # along with this program. If not, see . import asyncio +import pytest from unittest.mock import patch +from gns3server.modules.project_manager import ProjectManager +from gns3server.modules.port_manager import PortManager class _asyncio_patch: @@ -54,3 +57,18 @@ class _asyncio_patch: def asyncio_patch(function, *args, **kwargs): return _asyncio_patch(function, *args, **kwargs) + + +@pytest.fixture(scope="session") +def port_manager(): + return PortManager("127.0.0.1", False) + + +@pytest.fixture(scope="function") +def free_console_port(request, port_manager): + # In case of already use ports we will raise an exception + port = port_manager.get_free_console_port() + # We release the port immediately in order to allow + # the test do whatever the test want + port_manager.release_console_port(port) + return port From 7ce1cf3f8492d568c3af62f6351eb2bb661b1782 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 21 Jan 2015 14:01:15 -0700 Subject: [PATCH 079/485] Return correct status codes and fix tests. --- gns3server/handlers/virtualbox_handler.py | 8 +++++--- gns3server/handlers/vpcs_handler.py | 13 ++++++++----- tests/api/base.py | 5 +++-- tests/api/test_virtualbox.py | 14 +++++++------- tests/api/test_vpcs.py | 20 ++++++++++---------- 5 files changed, 33 insertions(+), 27 deletions(-) diff --git a/gns3server/handlers/virtualbox_handler.py b/gns3server/handlers/virtualbox_handler.py index 55d9a53b..84a340ed 100644 --- a/gns3server/handlers/virtualbox_handler.py +++ b/gns3server/handlers/virtualbox_handler.py @@ -44,7 +44,9 @@ class VirtualBoxHandler: vm = yield from vbox_manager.create_vm(request.json["name"], request.json["project_uuid"], request.json.get("uuid"), - vmname=request.json["vmname"]) + request.json["vmname"], + request.json.get("linked_clone", False)) + response.set_status(201) response.json({"name": vm.name, "uuid": vm.uuid, "project_uuid": vm.project.uuid}) @@ -65,7 +67,7 @@ class VirtualBoxHandler: vbox_manager = VirtualBox.instance() yield from vbox_manager.start_vm(request.match_info["uuid"]) - response.json({}) + response.set_status(204) @classmethod @Route.post( @@ -83,4 +85,4 @@ class VirtualBoxHandler: vbox_manager = VirtualBox.instance() yield from vbox_manager.stop_vm(request.match_info["uuid"]) - response.json({}) + response.set_status(204) diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index 633a668e..0eb354b5 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -49,6 +49,7 @@ class VPCSHandler: console=request.json.get("console"), script_file=request.json.get("script_file"), startup_script=request.json.get("startup_script")) + response.set_status(201) response.json(vm) @classmethod @@ -58,7 +59,7 @@ class VPCSHandler: "uuid": "VPCS instance UUID" }, status_codes={ - 200: "VPCS instance started", + 200: "Success", 404: "VPCS instance doesn't exist" }, description="Get a VPCS instance") @@ -105,7 +106,7 @@ class VPCSHandler: vpcs_manager = VPCS.instance() yield from vpcs_manager.start_vm(request.match_info["uuid"]) - response.json({}) + response.set_status(204) @classmethod @Route.post( @@ -123,7 +124,7 @@ class VPCSHandler: vpcs_manager = VPCS.instance() yield from vpcs_manager.stop_vm(request.match_info["uuid"]) - response.json({}) + response.set_status(204) @Route.post( r"/vpcs/{uuid}/ports/{port_id}/nio", @@ -144,6 +145,7 @@ class VPCSHandler: vpcs_manager = VPCS.instance() vm = vpcs_manager.get_vm(request.match_info["uuid"]) nio = vm.port_add_nio_binding(int(request.match_info["port_id"]), request.json) + response.set_status(201) response.json(nio) @classmethod @@ -154,7 +156,7 @@ class VPCSHandler: "port_id": "ID of the port where the nio should be removed" }, status_codes={ - 200: "NIO deleted", + 204: "NIO deleted", 400: "Invalid VPCS instance UUID", 404: "VPCS instance doesn't exist" }, @@ -164,4 +166,5 @@ class VPCSHandler: vpcs_manager = VPCS.instance() vm = vpcs_manager.get_vm(request.match_info["uuid"]) nio = vm.port_remove_nio_binding(int(request.match_info["port_id"])) - response.json({}) + response.set_status(204) + diff --git a/tests/api/base.py b/tests/api/base.py index 4664f41a..c2b6d299 100644 --- a/tests/api/base.py +++ b/tests/api/base.py @@ -118,8 +118,9 @@ class Query: value = "Thu, 08 Jan 2015 16:09:15 GMT" f.write("{}: {}\n".format(header, value)) f.write("\n") - f.write(json.dumps(json.loads(response.body.decode('utf-8')), sort_keys=True, indent=4)) - f.write("\n") + if response.body: + f.write(json.dumps(json.loads(response.body.decode('utf-8')), sort_keys=True, indent=4)) + f.write("\n") def _example_file_path(self, method, path): path = re.sub('[^a-z0-9]', '', path) diff --git a/tests/api/test_virtualbox.py b/tests/api/test_virtualbox.py index 3d9e56c3..6ce767aa 100644 --- a/tests/api/test_virtualbox.py +++ b/tests/api/test_virtualbox.py @@ -22,20 +22,20 @@ from gns3server.modules.virtualbox.virtualbox_vm import VirtualBoxVM def test_vbox_create(server, project): response = server.post("/virtualbox", {"name": "VM1", "vmname": "VM1", "project_uuid": project.uuid}, example=True) - assert response.status == 200 + assert response.status == 201 assert response.route == "/virtualbox" assert response.json["name"] == "VM1" def test_vbox_start(server): - with asyncio_patch("gns3server.modules.VirtualBox.start_vm", return_value=True): + with asyncio_patch("gns3server.modules.VirtualBox.start_vm", return_value=True) as mock: response = server.post("/virtualbox/61d61bdd-aa7d-4912-817f-65a9eb54d3ab/start", {}, example=True) - assert response.status == 200 - assert response.route == "/virtualbox/{uuid}/start" + assert mock.called + assert response.status == 204 def test_vbox_stop(server): - with asyncio_patch("gns3server.modules.VirtualBox.stop_vm", return_value=True): + with asyncio_patch("gns3server.modules.VirtualBox.stop_vm", return_value=True) as mock: response = server.post("/virtualbox/61d61bdd-aa7d-4912-817f-65a9eb54d3ab/stop", {}, example=True) - assert response.status == 200 - assert response.route == "/virtualbox/{uuid}/stop" + assert mock.called + assert response.status == 204 diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index 56b5bc56..029e4db1 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -26,13 +26,13 @@ from gns3server.modules.vpcs.vpcs_vm import VPCSVM @pytest.fixture(scope="module") def vm(server, project): response = server.post("/vpcs", {"name": "PC TEST 1", "project_uuid": project.uuid}) - assert response.status == 200 + assert response.status == 201 return response.json def test_vpcs_create(server, project): response = server.post("/vpcs", {"name": "PC TEST 1", "project_uuid": project.uuid}, example=True) - assert response.status == 200 + assert response.status == 201 assert response.route == "/vpcs" assert response.json["name"] == "PC TEST 1" assert response.json["project_uuid"] == project.uuid @@ -52,7 +52,7 @@ def test_vpcs_create_script_file(server, project, tmpdir): with open(path, 'w+') as f: f.write("ip 192.168.1.2") response = server.post("/vpcs", {"name": "PC TEST 1", "project_uuid": project.uuid, "script_file": path}) - assert response.status == 200 + assert response.status == 201 assert response.route == "/vpcs" assert response.json["name"] == "PC TEST 1" assert response.json["project_uuid"] == project.uuid @@ -61,7 +61,7 @@ def test_vpcs_create_script_file(server, project, tmpdir): def test_vpcs_create_startup_script(server, project): response = server.post("/vpcs", {"name": "PC TEST 1", "project_uuid": project.uuid, "startup_script": "ip 192.168.1.2\necho TEST"}) - assert response.status == 200 + assert response.status == 201 assert response.route == "/vpcs" assert response.json["name"] == "PC TEST 1" assert response.json["project_uuid"] == project.uuid @@ -70,7 +70,7 @@ def test_vpcs_create_startup_script(server, project): def test_vpcs_create_port(server, project, free_console_port): response = server.post("/vpcs", {"name": "PC TEST 1", "project_uuid": project.uuid, "console": free_console_port}) - assert response.status == 200 + assert response.status == 201 assert response.route == "/vpcs" assert response.json["name"] == "PC TEST 1" assert response.json["project_uuid"] == project.uuid @@ -83,7 +83,7 @@ def test_vpcs_nio_create_udp(server, vm): "rport": 4343, "rhost": "127.0.0.1"}, example=True) - assert response.status == 200 + assert response.status == 201 assert response.route == "/vpcs/{uuid}/ports/{port_id}/nio" assert response.json["type"] == "nio_udp" @@ -92,7 +92,7 @@ def test_vpcs_nio_create_udp(server, vm): def test_vpcs_nio_create_tap(mock, server, vm): response = server.post("/vpcs/{}/ports/0/nio".format(vm["uuid"]), {"type": "nio_tap", "tap_device": "test"}) - assert response.status == 200 + assert response.status == 201 assert response.route == "/vpcs/{uuid}/ports/{port_id}/nio" assert response.json["type"] == "nio_tap" @@ -103,7 +103,7 @@ def test_vpcs_delete_nio(server, vm): "rport": 4343, "rhost": "127.0.0.1"}) response = server.delete("/vpcs/{}/ports/0/nio".format(vm["uuid"]), example=True) - assert response.status == 200 + assert response.status == 204 assert response.route == "/vpcs/{uuid}/ports/{port_id}/nio" @@ -111,14 +111,14 @@ def test_vpcs_start(server, vm): with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM.start", return_value=True) as mock: response = server.post("/vpcs/{}/start".format(vm["uuid"])) assert mock.called - assert response.status == 200 + assert response.status == 204 def test_vpcs_stop(server, vm): with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM.stop", return_value=True) as mock: response = server.post("/vpcs/{}/stop".format(vm["uuid"])) assert mock.called - assert response.status == 200 + assert response.status == 204 def test_vpcs_update(server, vm, tmpdir, free_console_port): From ef4ecbfb6a32245c3a65d1c10a8db40ba01c65eb Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 21 Jan 2015 22:06:25 +0100 Subject: [PATCH 080/485] Improve VPCS port change test --- gns3server/modules/vpcs/vpcs_vm.py | 3 +++ tests/modules/vpcs/test_vpcs_vm.py | 14 +++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index df7dc5fc..548b7ec3 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -130,6 +130,9 @@ class VPCSVM(BaseVM): :params console: Console port (integer) """ + + if console == self._console: + return if self._console: self._manager.port_manager.release_console_port(self._console) self._console = self._manager.port_manager.reserve_console_port(console) diff --git a/tests/modules/vpcs/test_vpcs_vm.py b/tests/modules/vpcs/test_vpcs_vm.py index 1cd27646..d2baf04b 100644 --- a/tests/modules/vpcs/test_vpcs_vm.py +++ b/tests/modules/vpcs/test_vpcs_vm.py @@ -138,11 +138,15 @@ def test_get_startup_script(vm): assert vm.startup_script == content -def test_change_console_port(vm, free_console_port): - vm.console = free_console_port - vm.console = free_console_port + 1 - assert vm.console == free_console_port - PortManager.instance().reserve_console_port(free_console_port + 1) +def test_change_console_port(vm, port_manager): + port1 = port_manager.get_free_console_port() + port2 = port_manager.get_free_console_port() + port_manager.release_console_port(port1) + port_manager.release_console_port(port2) + vm.console = port1 + vm.console = port2 + assert vm.console == port2 + PortManager.instance().reserve_console_port(port1) def test_change_name(vm, tmpdir): From 8d9da999e6cb74b2d8dde2795e71f074e7ac706a Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 21 Jan 2015 22:08:54 +0100 Subject: [PATCH 081/485] Update examples only when launching test with documentation.sh --- docs/api/examples/get_vpcsuuid.txt | 2 +- docs/api/examples/post_vpcs.txt | 2 +- docs/api/virtualbox.rst | 47 +++++++++++++++++++++++ docs/api/virtualboxuuidstart.rst | 25 +++++++++++++ docs/api/virtualboxuuidstop.rst | 25 +++++++++++++ docs/api/vpcs.rst | 7 +++- docs/api/vpcsuuid.rst | 60 ++++++++++++++++++++++++++++++ scripts/documentation.sh | 2 + tests/api/base.py | 4 +- 9 files changed, 169 insertions(+), 5 deletions(-) create mode 100644 docs/api/virtualbox.rst create mode 100644 docs/api/virtualboxuuidstart.rst create mode 100644 docs/api/virtualboxuuidstop.rst create mode 100644 docs/api/vpcsuuid.rst diff --git a/docs/api/examples/get_vpcsuuid.txt b/docs/api/examples/get_vpcsuuid.txt index 1abe5fd9..57b93cbf 100644 --- a/docs/api/examples/get_vpcsuuid.txt +++ b/docs/api/examples/get_vpcsuuid.txt @@ -18,5 +18,5 @@ X-ROUTE: /vpcs/{uuid} "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, "startup_script": null, - "uuid": "f8155d67-c0bf-4229-be4c-97edaaae7b0b" + "uuid": "c9e15d9f-cff3-402a-b9c9-57f4d008832f" } diff --git a/docs/api/examples/post_vpcs.txt b/docs/api/examples/post_vpcs.txt index 26ee627f..cbd49f85 100644 --- a/docs/api/examples/post_vpcs.txt +++ b/docs/api/examples/post_vpcs.txt @@ -21,5 +21,5 @@ X-ROUTE: /vpcs "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, "startup_script": null, - "uuid": "5a9aac64-5b62-41bd-955a-fcef90a2fac5" + "uuid": "f5016337-fa62-4e82-95da-9f66f68e6e8f" } diff --git a/docs/api/virtualbox.rst b/docs/api/virtualbox.rst new file mode 100644 index 00000000..9a6cb762 --- /dev/null +++ b/docs/api/virtualbox.rst @@ -0,0 +1,47 @@ +/virtualbox +--------------------------------------------- + +.. contents:: + +POST /virtualbox +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Create a new VirtualBox VM instance + +Response status codes +********************** +- **400**: Invalid project UUID +- **201**: VirtualBox VM instance created +- **409**: Conflict + +Input +******* +.. raw:: html + + + + + + + + + +
Name Mandatory Type Description
linked_clone boolean either the VM is a linked clone or not
name string VirtualBox VM instance name
project_uuid string Project UUID
uuid string VirtualBox VM instance UUID
vbox_id integer VirtualBox VM instance ID (for project created before GNS3 1.3)
vmname string VirtualBox VM name (in VirtualBox itself)
+ +Output +******* +.. raw:: html + + + + + + + +
Name Mandatory Type Description
console integer console TCP port
name string VirtualBox VM instance name
project_uuid string Project UUID
uuid string VirtualBox VM instance UUID
+ +Sample session +*************** + + +.. literalinclude:: examples/post_virtualbox.txt + diff --git a/docs/api/virtualboxuuidstart.rst b/docs/api/virtualboxuuidstart.rst new file mode 100644 index 00000000..570a64f7 --- /dev/null +++ b/docs/api/virtualboxuuidstart.rst @@ -0,0 +1,25 @@ +/virtualbox/{uuid}/start +--------------------------------------------- + +.. contents:: + +POST /virtualbox/**{uuid}**/start +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Start a VirtualBox VM instance + +Parameters +********** +- **uuid**: VirtualBox VM instance UUID + +Response status codes +********************** +- **400**: Invalid VirtualBox VM instance UUID +- **404**: VirtualBox VM instance doesn't exist +- **204**: VirtualBox VM instance started + +Sample session +*************** + + +.. literalinclude:: examples/post_virtualboxuuidstart.txt + diff --git a/docs/api/virtualboxuuidstop.rst b/docs/api/virtualboxuuidstop.rst new file mode 100644 index 00000000..21fd9809 --- /dev/null +++ b/docs/api/virtualboxuuidstop.rst @@ -0,0 +1,25 @@ +/virtualbox/{uuid}/stop +--------------------------------------------- + +.. contents:: + +POST /virtualbox/**{uuid}**/stop +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Stop a VirtualBox VM instance + +Parameters +********** +- **uuid**: VirtualBox VM instance UUID + +Response status codes +********************** +- **400**: Invalid VirtualBox VM instance UUID +- **404**: VirtualBox VM instance doesn't exist +- **204**: VirtualBox VM instance stopped + +Sample session +*************** + + +.. literalinclude:: examples/post_virtualboxuuidstop.txt + diff --git a/docs/api/vpcs.rst b/docs/api/vpcs.rst index 225990bb..4790ad9e 100644 --- a/docs/api/vpcs.rst +++ b/docs/api/vpcs.rst @@ -9,6 +9,7 @@ Create a new VPCS instance Response status codes ********************** +- **400**: Invalid project UUID - **201**: VPCS instance created - **409**: Conflict @@ -18,9 +19,11 @@ Input - + + +
Name Mandatory Type Description
console integer console TCP port
console ['integer', 'null'] console TCP port
name string VPCS device name
project_uuid string Project UUID
script_file ['string', 'null'] VPCS startup script
startup_script ['string', 'null'] Content of the VPCS startup script
uuid string VPCS device UUID
vpcs_id integer VPCS device instance ID (for project created before GNS3 1.3)
@@ -34,6 +37,8 @@ Output console ✔ integer console TCP port name ✔ string VPCS device name project_uuid ✔ string Project UUID + script_file ['string', 'null'] VPCS startup script + startup_script ['string', 'null'] Content of the VPCS startup script uuid ✔ string VPCS device UUID diff --git a/docs/api/vpcsuuid.rst b/docs/api/vpcsuuid.rst new file mode 100644 index 00000000..f2489f1c --- /dev/null +++ b/docs/api/vpcsuuid.rst @@ -0,0 +1,60 @@ +/vpcs/{uuid} +--------------------------------------------- + +.. contents:: + +GET /vpcs/**{uuid}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Get a VPCS instance + +Parameters +********** +- **uuid**: VPCS instance UUID + +Response status codes +********************** +- **200**: VPCS instance started +- **404**: VPCS instance doesn't exist + +Sample session +*************** + + +.. literalinclude:: examples/get_vpcsuuid.txt + + +PUT /vpcs/**{uuid}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Update a VPCS instance + +Response status codes +********************** +- **200**: VPCS instance updated +- **409**: Conflict + +Input +******* +.. raw:: html + + + + + + + +
Name Mandatory Type Description
console ['integer', 'null'] console TCP port
name ['string', 'null'] VPCS device name
script_file ['string', 'null'] VPCS startup script
startup_script ['string', 'null'] Content of the VPCS startup script
+ +Output +******* +.. raw:: html + + + + + + + + + +
Name Mandatory Type Description
console integer console TCP port
name string VPCS device name
project_uuid string Project UUID
script_file ['string', 'null'] VPCS startup script
startup_script ['string', 'null'] Content of the VPCS startup script
uuid string VPCS device UUID
+ diff --git a/scripts/documentation.sh b/scripts/documentation.sh index 0ee22a9a..fb7af59c 100755 --- a/scripts/documentation.sh +++ b/scripts/documentation.sh @@ -23,6 +23,8 @@ set -e echo "WARNING: This script should be run at the root directory of the project" +export PYTEST_BUILD_DOCUMENTATION=1 + py.test -v python3 gns3server/web/documentation.py cd docs diff --git a/tests/api/base.py b/tests/api/base.py index c2b6d299..638d53cc 100644 --- a/tests/api/base.py +++ b/tests/api/base.py @@ -25,7 +25,7 @@ import socket import pytest from aiohttp import web import aiohttp - +import os from gns3server.web.route import Route # TODO: get rid of * @@ -93,7 +93,7 @@ class Query: response.json = None else: response.json = {} - if kwargs.get('example'): + if kwargs.get('example') and os.environ.get("PYTEST_BUILD_DOCUMENTATION") == "1": self._dump_example(method, response.route, body, response) return response From f3e07d5ad9c976185edbea3b8771555e8e597cfd Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 21 Jan 2015 22:21:01 +0100 Subject: [PATCH 082/485] Fix random failure related to ports --- gns3server/handlers/vpcs_handler.py | 1 - gns3server/modules/port_manager.py | 18 ------------------ tests/modules/test_port_manager.py | 2 +- tests/modules/vpcs/test_vpcs_vm.py | 8 +++++--- 4 files changed, 6 insertions(+), 23 deletions(-) diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index 0eb354b5..25fdcc2d 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -167,4 +167,3 @@ class VPCSHandler: vm = vpcs_manager.get_vm(request.match_info["uuid"]) nio = vm.port_remove_nio_binding(int(request.match_info["port_id"])) response.set_status(204) - diff --git a/gns3server/modules/port_manager.py b/gns3server/modules/port_manager.py index 48d7e8af..ed4404dc 100644 --- a/gns3server/modules/port_manager.py +++ b/gns3server/modules/port_manager.py @@ -45,24 +45,6 @@ class PortManager: else: self._console_host = host - @classmethod - def instance(cls): - """ - Singleton to return only one instance of BaseManager. - - :returns: instance of Manager - """ - - if not hasattr(cls, "_instance") or cls._instance is None: - cls._instance = cls() - return cls._instance - - @classmethod - @asyncio.coroutine # FIXME: why coroutine? - def destroy(cls): - - cls._instance = None - @property def console_host(self): diff --git a/tests/modules/test_port_manager.py b/tests/modules/test_port_manager.py index e5a86ea8..06735272 100644 --- a/tests/modules/test_port_manager.py +++ b/tests/modules/test_port_manager.py @@ -21,7 +21,7 @@ from gns3server.modules.port_manager import PortManager def test_reserve_console_port(): - pm = PortManager.instance() + pm = PortManager() pm.reserve_console_port(4242) with pytest.raises(aiohttp.web.HTTPConflict): pm.reserve_console_port(4242) diff --git a/tests/modules/vpcs/test_vpcs_vm.py b/tests/modules/vpcs/test_vpcs_vm.py index d2baf04b..cc10cb7f 100644 --- a/tests/modules/vpcs/test_vpcs_vm.py +++ b/tests/modules/vpcs/test_vpcs_vm.py @@ -31,9 +31,9 @@ from gns3server.modules.port_manager import PortManager @pytest.fixture(scope="module") -def manager(): +def manager(port_manager): m = VPCS.instance() - m.port_manager = PortManager("127.0.0.1", False) + m.port_manager = port_manager return m @@ -143,10 +143,12 @@ def test_change_console_port(vm, port_manager): port2 = port_manager.get_free_console_port() port_manager.release_console_port(port1) port_manager.release_console_port(port2) + print(vm.console) + print(port1) vm.console = port1 vm.console = port2 assert vm.console == port2 - PortManager.instance().reserve_console_port(port1) + port_manager.reserve_console_port(port1) def test_change_name(vm, tmpdir): From 97cefa23fbd844a3dc9d8f237fa20a41151616bd Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 21 Jan 2015 22:32:33 +0100 Subject: [PATCH 083/485] Move fixtures to conftest --- tests/api/base.py | 60 ------------------- tests/api/test_project.py | 3 +- tests/api/test_version.py | 3 +- tests/api/test_virtualbox.py | 3 +- tests/api/test_vpcs.py | 3 +- tests/conftest.py | 93 ++++++++++++++++++++++++++++++ tests/modules/vpcs/test_vpcs_vm.py | 5 +- tests/utils.py | 18 ------ 8 files changed, 99 insertions(+), 89 deletions(-) create mode 100644 tests/conftest.py diff --git a/tests/api/base.py b/tests/api/base.py index 638d53cc..01d460d1 100644 --- a/tests/api/base.py +++ b/tests/api/base.py @@ -21,19 +21,9 @@ import json import re import asyncio -import socket -import pytest -from aiohttp import web import aiohttp import os -from gns3server.web.route import Route -# TODO: get rid of * -from gns3server.handlers import * -from gns3server.modules import MODULES -from gns3server.modules.port_manager import PortManager -from gns3server.modules.project_manager import ProjectManager - class Query: @@ -125,53 +115,3 @@ class Query: def _example_file_path(self, method, path): path = re.sub('[^a-z0-9]', '', path) return "docs/api/examples/{}_{}.txt".format(method.lower(), path) - - -def _get_unused_port(): - """ Return an unused port on localhost. In rare occasion it can return - an already used port (race condition)""" - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.bind(('localhost', 0)) - addr, port = s.getsockname() - s.close() - return port - - -@pytest.fixture(scope="session") -def loop(request): - """Return an event loop and destroy it at the end of test""" - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) # Replace main loop to avoid conflict between tests - - def tear_down(): - loop.close() - asyncio.set_event_loop(None) - request.addfinalizer(tear_down) - return loop - - -@pytest.fixture(scope="session") -def server(request, loop, port_manager): - port = _get_unused_port() - host = "localhost" - app = web.Application() - for method, route, handler in Route.get_routes(): - app.router.add_route(method, route, handler) - for module in MODULES: - instance = module.instance() - instance.port_manager = port_manager - srv = loop.create_server(app.make_handler(), host, port) - srv = loop.run_until_complete(srv) - - def tear_down(): - for module in MODULES: - loop.run_until_complete(module.destroy()) - srv.close() - srv.wait_closed() - request.addfinalizer(tear_down) - return Query(loop, host=host, port=port) - - -@pytest.fixture(scope="module") -def project(): - return ProjectManager.instance().create_project(uuid="a1e920ca-338a-4e9f-b363-aa607b09dd80") diff --git a/tests/api/test_project.py b/tests/api/test_project.py index b7ae8b55..7c7d1c6b 100644 --- a/tests/api/test_project.py +++ b/tests/api/test_project.py @@ -20,8 +20,7 @@ This test suite check /project endpoint """ -from tests.utils import asyncio_patch, port_manager -from tests.api.base import server, loop +from tests.utils import asyncio_patch from gns3server.version import __version__ diff --git a/tests/api/test_version.py b/tests/api/test_version.py index 5b479006..76e7db72 100644 --- a/tests/api/test_version.py +++ b/tests/api/test_version.py @@ -20,8 +20,7 @@ This test suite check /version endpoint It's also used for unittest the HTTP implementation. """ -from tests.utils import asyncio_patch, port_manager -from tests.api.base import server, loop +from tests.utils import asyncio_patch from gns3server.version import __version__ diff --git a/tests/api/test_virtualbox.py b/tests/api/test_virtualbox.py index 6ce767aa..820bf34a 100644 --- a/tests/api/test_virtualbox.py +++ b/tests/api/test_virtualbox.py @@ -15,8 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from tests.api.base import server, loop, project -from tests.utils import asyncio_patch, port_manager +from tests.utils import asyncio_patch from gns3server.modules.virtualbox.virtualbox_vm import VirtualBoxVM diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index 029e4db1..5fed9aa9 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -17,8 +17,7 @@ import pytest import os -from tests.api.base import server, loop, project -from tests.utils import asyncio_patch, free_console_port, port_manager +from tests.utils import asyncio_patch from unittest.mock import patch, Mock from gns3server.modules.vpcs.vpcs_vm import VPCSVM diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..572e3f00 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,93 @@ +# -*- 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 pytest +import socket +import asyncio +from aiohttp import web +from gns3server.web.route import Route +# TODO: get rid of * +from gns3server.handlers import * +from gns3server.modules import MODULES +from gns3server.modules.port_manager import PortManager +from gns3server.modules.project_manager import ProjectManager +from tests.api.base import Query + + +@pytest.fixture(scope="session") +def loop(request): + """Return an event loop and destroy it at the end of test""" + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) # Replace main loop to avoid conflict between tests + + def tear_down(): + loop.close() + asyncio.set_event_loop(None) + request.addfinalizer(tear_down) + return loop + + +def _get_unused_port(): + """ Return an unused port on localhost. In rare occasion it can return + an already used port (race condition)""" + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.bind(('localhost', 0)) + addr, port = s.getsockname() + s.close() + return port + + +@pytest.fixture(scope="session") +def server(request, loop, port_manager): + port = _get_unused_port() + host = "localhost" + app = web.Application() + for method, route, handler in Route.get_routes(): + app.router.add_route(method, route, handler) + for module in MODULES: + instance = module.instance() + instance.port_manager = port_manager + srv = loop.create_server(app.make_handler(), host, port) + srv = loop.run_until_complete(srv) + + def tear_down(): + for module in MODULES: + loop.run_until_complete(module.destroy()) + srv.close() + srv.wait_closed() + request.addfinalizer(tear_down) + return Query(loop, host=host, port=port) + + +@pytest.fixture(scope="module") +def project(): + return ProjectManager.instance().create_project(uuid="a1e920ca-338a-4e9f-b363-aa607b09dd80") + + +@pytest.fixture(scope="session") +def port_manager(): + return PortManager("127.0.0.1", False) + + +@pytest.fixture(scope="function") +def free_console_port(request, port_manager): + # In case of already use ports we will raise an exception + port = port_manager.get_free_console_port() + # We release the port immediately in order to allow + # the test do whatever the test want + port_manager.release_console_port(port) + return port diff --git a/tests/modules/vpcs/test_vpcs_vm.py b/tests/modules/vpcs/test_vpcs_vm.py index cc10cb7f..9887c97b 100644 --- a/tests/modules/vpcs/test_vpcs_vm.py +++ b/tests/modules/vpcs/test_vpcs_vm.py @@ -18,10 +18,9 @@ import pytest import asyncio import os -from tests.utils import asyncio_patch, port_manager, free_console_port +from tests.utils import asyncio_patch + -# TODO: Move loop to util -from tests.api.base import loop, project from asyncio.subprocess import Process from unittest.mock import patch, MagicMock from gns3server.modules.vpcs.vpcs_vm import VPCSVM diff --git a/tests/utils.py b/tests/utils.py index 55069f5e..bc8dc5dc 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -16,10 +16,7 @@ # along with this program. If not, see . import asyncio -import pytest from unittest.mock import patch -from gns3server.modules.project_manager import ProjectManager -from gns3server.modules.port_manager import PortManager class _asyncio_patch: @@ -57,18 +54,3 @@ class _asyncio_patch: def asyncio_patch(function, *args, **kwargs): return _asyncio_patch(function, *args, **kwargs) - - -@pytest.fixture(scope="session") -def port_manager(): - return PortManager("127.0.0.1", False) - - -@pytest.fixture(scope="function") -def free_console_port(request, port_manager): - # In case of already use ports we will raise an exception - port = port_manager.get_free_console_port() - # We release the port immediately in order to allow - # the test do whatever the test want - port_manager.release_console_port(port) - return port From 0249a21409d1a18a5c4a91d34a4c48c42cb5ce6f Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 21 Jan 2015 22:33:41 +0100 Subject: [PATCH 084/485] Build doc --- docs/api/examples/delete_vpcsuuidportsportidnio.txt | 6 ++---- docs/api/examples/get_vpcsuuid.txt | 2 +- docs/api/examples/post_virtualbox.txt | 4 ++-- docs/api/examples/post_virtualboxuuidstart.txt | 6 ++---- docs/api/examples/post_virtualboxuuidstop.txt | 6 ++---- docs/api/examples/post_vpcs.txt | 4 ++-- docs/api/examples/post_vpcsuuidportsportidnio.txt | 2 +- docs/api/vpcsuuid.rst | 2 +- docs/api/vpcsuuidportsportidnio.rst | 6 +++--- 9 files changed, 16 insertions(+), 22 deletions(-) diff --git a/docs/api/examples/delete_vpcsuuidportsportidnio.txt b/docs/api/examples/delete_vpcsuuidportsportidnio.txt index 64bbd964..34cfef4d 100644 --- a/docs/api/examples/delete_vpcsuuidportsportidnio.txt +++ b/docs/api/examples/delete_vpcsuuidportsportidnio.txt @@ -4,12 +4,10 @@ DELETE /vpcs/{uuid}/ports/{port_id}/nio HTTP/1.1 -HTTP/1.1 200 +HTTP/1.1 204 CONNECTION: close -CONTENT-LENGTH: 2 -CONTENT-TYPE: application/json +CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /vpcs/{uuid}/ports/{port_id}/nio -{} diff --git a/docs/api/examples/get_vpcsuuid.txt b/docs/api/examples/get_vpcsuuid.txt index 57b93cbf..f0eb88f9 100644 --- a/docs/api/examples/get_vpcsuuid.txt +++ b/docs/api/examples/get_vpcsuuid.txt @@ -18,5 +18,5 @@ X-ROUTE: /vpcs/{uuid} "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, "startup_script": null, - "uuid": "c9e15d9f-cff3-402a-b9c9-57f4d008832f" + "uuid": "a0ecc3ea-907f-4751-9415-9f6d5da4dc3a" } diff --git a/docs/api/examples/post_virtualbox.txt b/docs/api/examples/post_virtualbox.txt index 6be43ee6..efc04fdb 100644 --- a/docs/api/examples/post_virtualbox.txt +++ b/docs/api/examples/post_virtualbox.txt @@ -8,7 +8,7 @@ POST /virtualbox HTTP/1.1 } -HTTP/1.1 200 +HTTP/1.1 201 CONNECTION: close CONTENT-LENGTH: 133 CONTENT-TYPE: application/json @@ -19,5 +19,5 @@ X-ROUTE: /virtualbox { "name": "VM1", "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "uuid": "3142e932-d316-40d7-bed3-7ef8e2d313b3" + "uuid": "0f7b32bb-13e1-4c3f-8176-bbf277672b58" } diff --git a/docs/api/examples/post_virtualboxuuidstart.txt b/docs/api/examples/post_virtualboxuuidstart.txt index dee153b5..8f567c27 100644 --- a/docs/api/examples/post_virtualboxuuidstart.txt +++ b/docs/api/examples/post_virtualboxuuidstart.txt @@ -4,12 +4,10 @@ POST /virtualbox/{uuid}/start HTTP/1.1 {} -HTTP/1.1 200 +HTTP/1.1 204 CONNECTION: close -CONTENT-LENGTH: 2 -CONTENT-TYPE: application/json +CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /virtualbox/{uuid}/start -{} diff --git a/docs/api/examples/post_virtualboxuuidstop.txt b/docs/api/examples/post_virtualboxuuidstop.txt index 5bcfc6af..9c4982a7 100644 --- a/docs/api/examples/post_virtualboxuuidstop.txt +++ b/docs/api/examples/post_virtualboxuuidstop.txt @@ -4,12 +4,10 @@ POST /virtualbox/{uuid}/stop HTTP/1.1 {} -HTTP/1.1 200 +HTTP/1.1 204 CONNECTION: close -CONTENT-LENGTH: 2 -CONTENT-TYPE: application/json +CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /virtualbox/{uuid}/stop -{} diff --git a/docs/api/examples/post_vpcs.txt b/docs/api/examples/post_vpcs.txt index cbd49f85..67c5e242 100644 --- a/docs/api/examples/post_vpcs.txt +++ b/docs/api/examples/post_vpcs.txt @@ -7,7 +7,7 @@ POST /vpcs HTTP/1.1 } -HTTP/1.1 200 +HTTP/1.1 201 CONNECTION: close CONTENT-LENGTH: 213 CONTENT-TYPE: application/json @@ -21,5 +21,5 @@ X-ROUTE: /vpcs "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, "startup_script": null, - "uuid": "f5016337-fa62-4e82-95da-9f66f68e6e8f" + "uuid": "f4f04818-610c-4e95-aa0e-6d29afa72fc7" } diff --git a/docs/api/examples/post_vpcsuuidportsportidnio.txt b/docs/api/examples/post_vpcsuuidportsportidnio.txt index f29d6532..da7e6fd9 100644 --- a/docs/api/examples/post_vpcsuuidportsportidnio.txt +++ b/docs/api/examples/post_vpcsuuidportsportidnio.txt @@ -9,7 +9,7 @@ POST /vpcs/{uuid}/ports/{port_id}/nio HTTP/1.1 } -HTTP/1.1 200 +HTTP/1.1 201 CONNECTION: close CONTENT-LENGTH: 89 CONTENT-TYPE: application/json diff --git a/docs/api/vpcsuuid.rst b/docs/api/vpcsuuid.rst index f2489f1c..bba2e33f 100644 --- a/docs/api/vpcsuuid.rst +++ b/docs/api/vpcsuuid.rst @@ -13,7 +13,7 @@ Parameters Response status codes ********************** -- **200**: VPCS instance started +- **200**: Success - **404**: VPCS instance doesn't exist Sample session diff --git a/docs/api/vpcsuuidportsportidnio.rst b/docs/api/vpcsuuidportsportidnio.rst index ef1c7278..c186c9bb 100644 --- a/docs/api/vpcsuuidportsportidnio.rst +++ b/docs/api/vpcsuuidportsportidnio.rst @@ -9,8 +9,8 @@ Add a NIO to a VPCS Parameters ********** -- **uuid**: VPCS instance UUID - **port_id**: Id of the port where the nio should be add +- **uuid**: VPCS instance UUID Response status codes ********************** @@ -31,14 +31,14 @@ Remove a NIO from a VPCS Parameters ********** -- **uuid**: VPCS instance UUID - **port_id**: ID of the port where the nio should be removed +- **uuid**: VPCS instance UUID Response status codes ********************** -- **200**: NIO deleted - **400**: Invalid VPCS instance UUID - **404**: VPCS instance doesn't exist +- **204**: NIO deleted Sample session *************** From 0b1b27db8f259fcf55bfd1b23a75ced75c0e7875 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 21 Jan 2015 15:21:15 -0700 Subject: [PATCH 085/485] Add module name to base manager. --- gns3server/handlers/virtualbox_handler.py | 7 ++- gns3server/modules/base_manager.py | 10 +++++ gns3server/modules/base_vm.py | 14 +++--- gns3server/modules/project.py | 4 +- .../modules/virtualbox/virtualbox_vm.py | 44 +++++++++---------- gns3server/modules/vpcs/vpcs_vm.py | 4 +- gns3server/schemas/virtualbox.py | 4 +- tests/api/test_virtualbox.py | 17 ++++--- 8 files changed, 60 insertions(+), 44 deletions(-) diff --git a/gns3server/handlers/virtualbox_handler.py b/gns3server/handlers/virtualbox_handler.py index 84a340ed..78a0310a 100644 --- a/gns3server/handlers/virtualbox_handler.py +++ b/gns3server/handlers/virtualbox_handler.py @@ -45,11 +45,10 @@ class VirtualBoxHandler: request.json["project_uuid"], request.json.get("uuid"), request.json["vmname"], - request.json.get("linked_clone", False)) + request.json["linked_clone"], + console=request.json.get("console")) response.set_status(201) - response.json({"name": vm.name, - "uuid": vm.uuid, - "project_uuid": vm.project.uuid}) + response.json(vm) @classmethod @Route.post( diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 3dcbca25..32423fba 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -49,6 +49,16 @@ class BaseManager: cls._instance = cls() return cls._instance + @property + def module_name(self): + """ + Returns the module name. + + :returns: module name + """ + + return self.__class__.__name__ + @property def port_manager(self): """ diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index c4531fba..3db4bb32 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -29,6 +29,10 @@ class BaseVM: self._project = project self._manager = manager + log.info("{module}: {name} [{uuid}] has been created".format(module=self.manager.module_name, + name=self.name, + uuid=self.uuid)) + # TODO: When delete release console ports @property @@ -59,10 +63,10 @@ class BaseVM: :param new_name: name """ - log.info("{module} {name} [{uuid}]: renamed to {new_name}".format(module=self.module_name, - name=self._name, - uuid=self.uuid, - new_name=new_name)) + log.info("{module}: {name} [{uuid}]: renamed to {new_name}".format(module=self.manager.module_name, + name=self.name, + uuid=self.uuid, + new_name=new_name)) self._name = new_name @property @@ -91,7 +95,7 @@ class BaseVM: Return VM working directory """ - return self._project.vm_working_directory(self.module_name, self._uuid) + return self._project.vm_working_directory(self.manager.module_name.lower(), self._uuid) def create(self): """ diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index c924a9d5..31e00171 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -47,7 +47,7 @@ class Project: self._path = os.path.join(self._location, self._uuid) try: - os.makedirs(os.path.join(self._path, 'vms'), exist_ok=True) + os.makedirs(os.path.join(self._path, "vms"), exist_ok=True) except OSError as e: raise aiohttp.web.HTTPInternalServerError(text="Could not create project directory: {}".format(e)) @@ -75,7 +75,7 @@ class Project: :param vm_uuid: VM UUID """ - p = os.path.join(self._path, "vms", module, vm_uuid) + p = os.path.join(self._path, module, vm_uuid) try: os.makedirs(p, exist_ok=True) except FileExistsError: diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 772beedc..27695215 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -49,7 +49,7 @@ class VirtualBoxVM(BaseVM): VirtualBox VM implementation. """ - def __init__(self, name, uuid, project, manager, vmname, linked_clone): + def __init__(self, name, uuid, project, manager, vmname, linked_clone, console=None): super().__init__(name, uuid, project, manager) @@ -71,22 +71,14 @@ class VirtualBoxVM(BaseVM): if not os.access(self._vboxmanage_path, os.X_OK): raise VirtualBoxError("VBoxManage is not executable") - self._vmname = vmname self._started = False self._linked_clone = linked_clone self._system_properties = {} self._queue = asyncio.Queue() self._created = asyncio.Future() + self._running = True self._worker = asyncio.async(self._run()) - return - - self._command = [] - self._vbox_user = vbox_user - - self._telnet_server_thread = None - self._serial_pipe = None - # VirtualBox settings self._console = console self._ethernet_adapters = [] @@ -96,27 +88,30 @@ class VirtualBoxVM(BaseVM): self._adapter_start_index = 0 self._adapter_type = "Intel PRO/1000 MT Desktop (82540EM)" - working_dir_path = os.path.join(working_dir, "vbox") - - if vbox_id and not os.path.isdir(working_dir_path): - raise VirtualBoxError("Working directory {} doesn't exist".format(working_dir_path)) - - # create the device own working directory - self.working_dir = working_dir_path - if linked_clone: - if vbox_id and os.path.isdir(os.path.join(self.working_dir, self._vmname)): + if uuid and os.path.isdir(os.path.join(self.working_dir, self._vmname)): vbox_file = os.path.join(self.working_dir, self._vmname, self._vmname + ".vbox") self._execute("registervm", [vbox_file]) self._reattach_hdds() else: self._create_linked_clone() - self._maximum_adapters = 8 - self.adapters = 2 # creates 2 adapters by default + #self._maximum_adapters = 8 + #self.adapters = 2 # creates 2 adapters by default - log.info("VirtualBox VM {name} [id={id}] has been created".format(name=self._name, - id=self._id)) + return + + self._command = [] + self._vbox_user = vbox_user + + self._telnet_server_thread = None + self._serial_pipe = None + + def __json__(self): + + return {"name": self.name, + "uuid": self.uuid, + "project_uuid": self.project.uuid} @asyncio.coroutine def _execute(self, subcommand, args, timeout=60): @@ -156,12 +151,13 @@ class VirtualBoxVM(BaseVM): try: yield from self._get_system_properties() + #TODO: check for API version self._created.set_result(True) except VirtualBoxError as e: self._created.set_exception(e) return - while True: + while self._running: future, subcommand, args = yield from self._queue.get() try: yield from self._execute(subcommand, args) diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index 548b7ec3..35c61407 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -106,8 +106,8 @@ class VPCSVM(BaseVM): def __json__(self): - return {"name": self._name, - "uuid": self._uuid, + return {"name": self.name, + "uuid": self.uuid, "console": self._console, "project_uuid": self.project.uuid, "script_file": self.script_file, diff --git a/gns3server/schemas/virtualbox.py b/gns3server/schemas/virtualbox.py index 63e1664e..d272efe1 100644 --- a/gns3server/schemas/virtualbox.py +++ b/gns3server/schemas/virtualbox.py @@ -55,7 +55,7 @@ VBOX_CREATE_SCHEMA = { }, }, "additionalProperties": False, - "required": ["name", "vmname"], + "required": ["name", "vmname", "linked_clone", "project_uuid"], } VBOX_OBJECT_SCHEMA = { @@ -90,5 +90,5 @@ VBOX_OBJECT_SCHEMA = { }, }, "additionalProperties": False, - "required": ["name", "uuid"] + "required": ["name", "uuid", "project_uuid"] } diff --git a/tests/api/test_virtualbox.py b/tests/api/test_virtualbox.py index 820bf34a..ed5ae696 100644 --- a/tests/api/test_virtualbox.py +++ b/tests/api/test_virtualbox.py @@ -16,14 +16,21 @@ # along with this program. If not, see . from tests.utils import asyncio_patch -from gns3server.modules.virtualbox.virtualbox_vm import VirtualBoxVM def test_vbox_create(server, project): - response = server.post("/virtualbox", {"name": "VM1", "vmname": "VM1", "project_uuid": project.uuid}, example=True) - assert response.status == 201 - assert response.route == "/virtualbox" - assert response.json["name"] == "VM1" + + with asyncio_patch("gns3server.modules.VirtualBox.create_vm", return_value={"name": "VM1", + "uuid": "61d61bdd-aa7d-4912-817f-65a9eb54d3ab", + "project_uuid": project.uuid}): + response = server.post("/virtualbox", {"name": "VM1", + "vmname": "VM1", + "linked_clone": False, + "project_uuid": project.uuid}, + example=True) + assert response.status == 201 + assert response.json["name"] == "VM1" + assert response.json["project_uuid"] == project.uuid def test_vbox_start(server): From f231b068332692eebb1a87a9ed81ab19d2b240aa Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 21 Jan 2015 17:41:35 -0700 Subject: [PATCH 086/485] No need for start_vm and stop_vm in the manager. --- gns3server/handlers/virtualbox_handler.py | 10 ++++++---- gns3server/handlers/vpcs_handler.py | 10 ++++++---- gns3server/modules/base_manager.py | 19 ++++--------------- gns3server/modules/base_vm.py | 19 ++++++++++--------- gns3server/modules/project_manager.py | 1 + gns3server/web/logger.py | 2 +- 6 files changed, 28 insertions(+), 33 deletions(-) diff --git a/gns3server/handlers/virtualbox_handler.py b/gns3server/handlers/virtualbox_handler.py index 78a0310a..209c68d3 100644 --- a/gns3server/handlers/virtualbox_handler.py +++ b/gns3server/handlers/virtualbox_handler.py @@ -62,10 +62,11 @@ class VirtualBoxHandler: 404: "VirtualBox VM instance doesn't exist" }, description="Start a VirtualBox VM instance") - def create(request, response): + def start(request, response): vbox_manager = VirtualBox.instance() - yield from vbox_manager.start_vm(request.match_info["uuid"]) + vm = vbox_manager.get_vm(request.match_info["uuid"]) + yield from vm.start() response.set_status(204) @classmethod @@ -80,8 +81,9 @@ class VirtualBoxHandler: 404: "VirtualBox VM instance doesn't exist" }, description="Stop a VirtualBox VM instance") - def create(request, response): + def stop(request, response): vbox_manager = VirtualBox.instance() - yield from vbox_manager.stop_vm(request.match_info["uuid"]) + vm = vbox_manager.get_vm(request.match_info["uuid"]) + yield from vm.stop() response.set_status(204) diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index 25fdcc2d..86ed9312 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -102,10 +102,11 @@ class VPCSHandler: 404: "VPCS instance doesn't exist" }, description="Start a VPCS instance") - def create(request, response): + def start(request, response): vpcs_manager = VPCS.instance() - yield from vpcs_manager.start_vm(request.match_info["uuid"]) + vm = vpcs_manager.get_vm(request.match_info["uuid"]) + yield from vm.start() response.set_status(204) @classmethod @@ -120,10 +121,11 @@ class VPCSHandler: 404: "VPCS instance doesn't exist" }, description="Stop a VPCS instance") - def create(request, response): + def stop(request, response): vpcs_manager = VPCS.instance() - yield from vpcs_manager.stop_vm(request.match_info["uuid"]) + vm = vpcs_manager.get_vm(request.match_info["uuid"]) + yield from vm.stop() response.set_status(204) @Route.post( diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 32423fba..402b9422 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -126,20 +126,9 @@ class BaseManager: uuid = str(uuid4()) vm = self._VM_CLASS(name, uuid, project, self, *args, **kwargs) - future = vm.create() - if isinstance(future, asyncio.Future): - yield from future + if asyncio.iscoroutinefunction(vm.create): + yield from vm.create() + else: + vm.create() self._vms[vm.uuid] = vm return vm - - @asyncio.coroutine - def start_vm(self, uuid): - - vm = self.get_vm(uuid) - yield from vm.start() - - @asyncio.coroutine - def stop_vm(self, uuid): - - vm = self.get_vm(uuid) - yield from vm.stop() diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index 3db4bb32..cd6a7aa4 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -15,7 +15,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - import logging log = logging.getLogger(__name__) @@ -29,9 +28,9 @@ class BaseVM: self._project = project self._manager = manager - log.info("{module}: {name} [{uuid}] has been created".format(module=self.manager.module_name, - name=self.name, - uuid=self.uuid)) + log.debug("{module}: {name} [{uuid}] initialized".format(module=self.manager.module_name, + name=self.name, + uuid=self.uuid)) # TODO: When delete release console ports @@ -63,10 +62,10 @@ class BaseVM: :param new_name: name """ - log.info("{module}: {name} [{uuid}]: renamed to {new_name}".format(module=self.manager.module_name, - name=self.name, - uuid=self.uuid, - new_name=new_name)) + log.info("{module}: {name} [{uuid}] renamed to {new_name}".format(module=self.manager.module_name, + name=self.name, + uuid=self.uuid, + new_name=new_name)) self._name = new_name @property @@ -102,7 +101,9 @@ class BaseVM: Creates the VM. """ - return + log.info("{module}: {name} [{uuid}] created".format(module=self.manager.module_name, + name=self.name, + uuid=self.uuid)) def start(self): """ diff --git a/gns3server/modules/project_manager.py b/gns3server/modules/project_manager.py index f44ab0fd..edcfe425 100644 --- a/gns3server/modules/project_manager.py +++ b/gns3server/modules/project_manager.py @@ -27,6 +27,7 @@ class ProjectManager: """ def __init__(self): + self._projects = {} @classmethod diff --git a/gns3server/web/logger.py b/gns3server/web/logger.py index d3b61dea..f2a51484 100644 --- a/gns3server/web/logger.py +++ b/gns3server/web/logger.py @@ -81,7 +81,7 @@ class ColouredStreamHandler(logging.StreamHandler): def init_logger(level, quiet=False): stream_handler = ColouredStreamHandler(sys.stdout) - stream_handler.formatter = ColouredFormatter("{asctime} {levelname:8} {filename}:{lineno}#RESET# {message}", "%Y-%m-%d %H:%M:%S", "{") + stream_handler.formatter = ColouredFormatter("{asctime} {levelname} {filename}:{lineno}#RESET# {message}", "%Y-%m-%d %H:%M:%S", "{") if quiet: stream_handler.addFilter(logging.Filter(name="user_facing")) logging.getLogger('user_facing').propagate = False From 87bd0d1869248af3b658aafe84b62fd8aedebb65 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 21 Jan 2015 19:26:39 -0700 Subject: [PATCH 087/485] VirtualBox VM almost done. --- .../modules/virtualbox/virtualbox_vm.py | 595 ++++++++---------- 1 file changed, 253 insertions(+), 342 deletions(-) diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 27695215..b44c9107 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -27,10 +27,10 @@ import subprocess import tempfile import json import socket -import time import asyncio import shutil +from pkg_resources import parse_version from .virtualbox_error import VirtualBoxError from ..adapters.ethernet_adapter import EthernetAdapter from .telnet_server import TelnetServer @@ -49,66 +49,42 @@ class VirtualBoxVM(BaseVM): VirtualBox VM implementation. """ - def __init__(self, name, uuid, project, manager, vmname, linked_clone, console=None): + def __init__(self, name, uuid, project, manager, vmname, linked_clone, console=None, vbox_user=None): super().__init__(name, uuid, project, manager) - # look for VBoxManage - self._vboxmanage_path = manager.config.get_section_config("VirtualBox").get("vboxmanage_path") - if not self._vboxmanage_path: - if sys.platform.startswith("win"): - if "VBOX_INSTALL_PATH" in os.environ: - self._vboxmanage_path = os.path.join(os.environ["VBOX_INSTALL_PATH"], "VBoxManage.exe") - elif "VBOX_MSI_INSTALL_PATH" in os.environ: - self._vboxmanage_path = os.path.join(os.environ["VBOX_MSI_INSTALL_PATH"], "VBoxManage.exe") - elif sys.platform.startswith("darwin"): - self._vboxmanage_path = "/Applications/VirtualBox.app/Contents/MacOS/VBoxManage" - else: - self._vboxmanage_path = shutil.which("vboxmanage") - - if not self._vboxmanage_path: - raise VirtualBoxError("Could not find VBoxManage") - if not os.access(self._vboxmanage_path, os.X_OK): - raise VirtualBoxError("VBoxManage is not executable") - - self._started = False + self._vboxmanage_path = None + self._maximum_adapters = 8 self._linked_clone = linked_clone + self._vbox_user = vbox_user self._system_properties = {} - self._queue = asyncio.Queue() - self._created = asyncio.Future() - self._running = True - self._worker = asyncio.async(self._run()) + self._telnet_server_thread = None + self._serial_pipe = None # VirtualBox settings self._console = console self._ethernet_adapters = [] self._headless = False - self._enable_remote_console = True + self._enable_remote_console = False self._vmname = vmname self._adapter_start_index = 0 self._adapter_type = "Intel PRO/1000 MT Desktop (82540EM)" - if linked_clone: - if uuid and os.path.isdir(os.path.join(self.working_dir, self._vmname)): - vbox_file = os.path.join(self.working_dir, self._vmname, self._vmname + ".vbox") - self._execute("registervm", [vbox_file]) - self._reattach_hdds() - else: - self._create_linked_clone() - - #self._maximum_adapters = 8 + #TODO: finish adapters support #self.adapters = 2 # creates 2 adapters by default - return - - self._command = [] - self._vbox_user = vbox_user - - self._telnet_server_thread = None - self._serial_pipe = None - def __json__(self): + #TODO: send more info + # {"name": self._name, + # "vmname": self._vmname, + # "adapters": self.adapters, + # "adapter_start_index": self._adapter_start_index, + # "adapter_type": "Intel PRO/1000 MT Desktop (82540EM)", + # "console": self._console, + # "enable_remote_console": self._enable_remote_console, + # "headless": self._headless} + return {"name": self.name, "uuid": self.uuid, "project_uuid": self.project.uuid} @@ -119,7 +95,12 @@ class VirtualBoxVM(BaseVM): command = [self._vboxmanage_path, "--nologo", subcommand] command.extend(args) try: - process = yield from asyncio.create_subprocess_exec(*command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) + if self._vbox_user and self._vbox_user.strip(): + #TODO: test & review this part + sudo_command = "sudo -i -u {}".format(self._vbox_user.strip()) + " ".join(command) + process = yield from asyncio.create_subprocess_shell(sudo_command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) + else: + process = yield from asyncio.create_subprocess_exec(*command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) except (OSError, subprocess.SubprocessError) as e: raise VirtualBoxError("Could not execute VBoxManage: {}".format(e)) @@ -147,96 +128,175 @@ class VirtualBoxVM(BaseVM): self._system_properties[name.strip()] = value.strip() @asyncio.coroutine - def _run(self): + def _get_vm_state(self): + """ + Returns the VM state (e.g. running, paused etc.) - try: - yield from self._get_system_properties() - #TODO: check for API version - self._created.set_result(True) - except VirtualBoxError as e: - self._created.set_exception(e) - return + :returns: state (string) + """ - while self._running: - future, subcommand, args = yield from self._queue.get() - try: - yield from self._execute(subcommand, args) - future.set_result(True) - except VirtualBoxError as e: - future.set_exception(e) + results = yield from self._execute("showvminfo", [self._vmname, "--machinereadable"]) + for info in results: + name, value = info.split('=', 1) + if name == "VMState": + return value.strip('"') + raise VirtualBoxError("Could not get VM state for {}".format(self._vmname)) + + @asyncio.coroutine + def _control_vm(self, params): + """ + Change setting in this VM when running. + + :param params: params to use with sub-command controlvm + + :returns: result of the command. + """ + + args = shlex.split(params) + result = yield from self._execute("controlvm", [self._vmname] + args) + return result + + @asyncio.coroutine + def _modify_vm(self, params): + """ + Change setting in this VM when not running. + + :param params: params to use with sub-command modifyvm + """ + args = shlex.split(params) + yield from self._execute("modifyvm", [self._vmname] + args) + + @asyncio.coroutine def create(self): - return self._created + # look for VBoxManage + self._vboxmanage_path = self.manager.config.get_section_config("VirtualBox").get("vboxmanage_path") + if not self._vboxmanage_path: + if sys.platform.startswith("win"): + if "VBOX_INSTALL_PATH" in os.environ: + self._vboxmanage_path = os.path.join(os.environ["VBOX_INSTALL_PATH"], "VBoxManage.exe") + elif "VBOX_MSI_INSTALL_PATH" in os.environ: + self._vboxmanage_path = os.path.join(os.environ["VBOX_MSI_INSTALL_PATH"], "VBoxManage.exe") + elif sys.platform.startswith("darwin"): + self._vboxmanage_path = "/Applications/VirtualBox.app/Contents/MacOS/VBoxManage" + else: + self._vboxmanage_path = shutil.which("vboxmanage") - def _put(self, item): + if not self._vboxmanage_path: + raise VirtualBoxError("Could not find VBoxManage") + if not os.access(self._vboxmanage_path, os.X_OK): + raise VirtualBoxError("VBoxManage is not executable") - try: - self._queue.put_nowait(item) - except asyncio.qeues.QueueFull: - raise VirtualBoxError("Queue is full") + yield from self._get_system_properties() + if parse_version(self._system_properties["API version"]) < parse_version("4_3"): + raise VirtualBoxError("The VirtualBox API version is lower than 4.3") + log.info("VirtualBox VM '{name}' [{uuid}] created".format(name=self.name, uuid=self.uuid)) + + if self._linked_clone: + #TODO: finish linked clone support + if self.uuid and os.path.isdir(os.path.join(self.working_dir, self._vmname)): + vbox_file = os.path.join(self.working_dir, self._vmname, self._vmname + ".vbox") + self._execute("registervm", [vbox_file]) + self._reattach_hdds() + else: + self._create_linked_clone() + @asyncio.coroutine def start(self): + """ + Starts this VirtualBox VM. + """ - args = [self._name] - future = asyncio.Future() - self._put((future, "startvm", args)) - return future + # resume the VM if it is paused + vm_state = yield from self._get_vm_state() + if vm_state == "paused": + yield from self.resume() + return - def stop(self): + # VM must be powered off and in saved state to start it + if vm_state != "poweroff" and vm_state != "saved": + raise VirtualBoxError("VirtualBox VM not powered off or saved") - args = [self._name, "poweroff"] - future = asyncio.Future() - self._put((future, "controlvm", args)) - return future + yield from self._set_network_options() + yield from self._set_serial_console() - def defaults(self): - """ - Returns all the default attribute values for this VirtualBox VM. + args = [self._vmname] + if self._headless: + args.extend(["--type", "headless"]) + result = yield from self._execute("startvm", args) + log.info("VirtualBox VM '{name}' [{uuid}] started".format(name=self.name, uuid=self.uuid)) + log.debug("Start result: {}".format(result)) + + # add a guest property to let the VM know about the GNS3 name + yield from self._execute("guestproperty", ["set", self._vmname, "NameInGNS3", self.name]) + # add a guest property to let the VM know about the GNS3 project directory + yield from self._execute("guestproperty", ["set", self._vmname, "ProjectDirInGNS3", self.working_dir]) + + if self._enable_remote_console: + self._start_remote_console() - :returns: default values (dictionary) + @asyncio.coroutine + def stop(self): + """ + Stops this VirtualBox VM. """ - vbox_defaults = {"name": self._name, - "vmname": self._vmname, - "adapters": self.adapters, - "adapter_start_index": self._adapter_start_index, - "adapter_type": "Intel PRO/1000 MT Desktop (82540EM)", - "console": self._console, - "enable_remote_console": self._enable_remote_console, - "headless": self._headless} + self._stop_remote_console() + vm_state = yield from self._get_vm_state() + if vm_state == "running" or vm_state == "paused" or vm_state == "stuck": + # power off the VM + result = yield from self._control_vm("poweroff") + log.info("VirtualBox VM '{name}' [{uuid}] stopped".format(name=self.name, uuid=self.uuid)) + log.debug("Stop result: {}".format(result)) - return vbox_defaults + yield from asyncio.sleep(0.5) # give some time for VirtualBox to unlock the VM + try: + # deactivate the first serial port + yield from self._modify_vm("--uart1 off") + except VirtualBoxError as e: + log.warn("Could not deactivate the first serial port: {}".format(e)) - @property - def working_dir(self): - """ - Returns current working directory + for adapter_id in range(0, len(self._ethernet_adapters)): + if self._ethernet_adapters[adapter_id] is None: + continue + yield from self._modify_vm("--nictrace{} off".format(adapter_id + 1)) + yield from self._modify_vm("--cableconnected{} off".format(adapter_id + 1)) + yield from self._modify_vm("--nic{} null".format(adapter_id + 1)) - :returns: path to the working directory + @asyncio.coroutine + def suspend(self): + """ + Suspends this VirtualBox VM. """ - return self._working_dir + vm_state = yield from self._get_vm_state() + if vm_state == "running": + yield from self._control_vm("pause") + log.info("VirtualBox VM '{name}' [{uuid}] suspended".format(name=self.name, uuid=self.uuid)) + else: + log.warn("VirtualBox VM '{name}' [{uuid}] cannot be suspended, current state: {state}".format(name=self.name, + uuid=self.uuid, + state=vm_state)) - @working_dir.setter - def working_dir(self, working_dir): + @asyncio.coroutine + def resume(self): """ - Sets the working directory this VirtualBox VM. - - :param working_dir: path to the working directory + Resumes this VirtualBox VM. """ - try: - os.makedirs(working_dir) - except FileExistsError: - pass - except OSError as e: - raise VirtualBoxError("Could not create working directory {}: {}".format(working_dir, e)) + yield from self._control_vm("resume") + log.info("VirtualBox VM '{name}' [{uuid}] resumed".format(name=self.name, uuid=self.uuid)) + + @asyncio.coroutine + def reload(self): + """ + Reloads this VirtualBox VM. + """ - self._working_dir = working_dir - log.info("VirtualBox VM {name} [id={id}]: working directory changed to {wd}".format(name=self._name, - id=self._id, - wd=self._working_dir)) + result = yield from self._control_vm("reset") + log.info("VirtualBox VM '{name}' [{uuid}] reloaded".format(name=self.name, uuid=self.uuid)) + log.debug("Reload result: {}".format(result)) @property def console(self): @@ -370,8 +430,8 @@ class VirtualBoxVM(BaseVM): # error=e)) # return - log.info("VirtualBox VM {name} [id={id}] has been deleted (including associated files)".format(name=self._name, - id=self._id)) + log.info("VirtualBox VM '{name}' [{uuid}] has been deleted (including associated files)".format(name=self.name, + uuid=self.uuid)) @property def headless(self): @@ -392,9 +452,9 @@ class VirtualBoxVM(BaseVM): """ if headless: - log.info("VirtualBox VM {name} [id={id}] has enabled the headless mode".format(name=self._name, id=self._id)) + log.info("VirtualBox VM '{name}' [{uuid}] has enabled the headless mode".format(name=self.name, uuid=self.uuid)) else: - log.info("VirtualBox VM {name} [id={id}] has disabled the headless mode".format(name=self._name, id=self._id)) + log.info("VirtualBox VM '{name}' [{uuid}] has disabled the headless mode".format(name=self.name, uuid=self.uuid)) self._headless = headless @property @@ -416,10 +476,10 @@ class VirtualBoxVM(BaseVM): """ if enable_remote_console: - log.info("VirtualBox VM {name} [id={id}] has enabled the console".format(name=self._name, id=self._id)) + log.info("VirtualBox VM '{name}' [{uuid}] has enabled the console".format(name=self.name, uuid=self.uuid)) self._start_remote_console() else: - log.info("VirtualBox VM {name} [id={id}] has disabled the console".format(name=self._name, id=self._id)) + log.info("VirtualBox VM '{name}' [{uuid}] has disabled the console".format(name=self.name, uuid=self.uuid)) self._stop_remote_console() self._enable_remote_console = enable_remote_console @@ -441,7 +501,7 @@ class VirtualBoxVM(BaseVM): :param vmname: VirtualBox VM name """ - log.info("VirtualBox VM {name} [id={id}] has set the VM name to {vmname}".format(name=self._name, id=self._id, vmname=vmname)) + log.info("VirtualBox VM '{name}' [{uuid}] has set the VM name to '{vmname}'".format(name=self.name, uuid=self.uuid, vmname=vmname)) if self._linked_clone: self._modify_vm('--name "{}"'.format(vmname)) self._vmname = vmname @@ -476,9 +536,9 @@ class VirtualBoxVM(BaseVM): continue self._ethernet_adapters.append(EthernetAdapter()) - log.info("VirtualBox VM {name} [id={id}]: number of Ethernet adapters changed to {adapters}".format(name=self._name, - id=self._id, - adapters=adapters)) + log.info("VirtualBox VM '{name}' [{uuid}]: number of Ethernet adapters changed to {adapters}".format(name=self.name, + uuid=self.uuid, + adapters=adapters)) @property def adapter_start_index(self): @@ -500,9 +560,9 @@ class VirtualBoxVM(BaseVM): self._adapter_start_index = adapter_start_index self.adapters = self.adapters # this forces to recreate the adapter list with the correct index - log.info("VirtualBox VM {name} [id={id}]: adapter start index changed to {index}".format(name=self._name, - id=self._id, - index=adapter_start_index)) + log.info("VirtualBox VM '{name}' [{uuid}]: adapter start index changed to {index}".format(name=self.name, + uuid=self.uuid, + index=adapter_start_index)) @property def adapter_type(self): @@ -524,42 +584,11 @@ class VirtualBoxVM(BaseVM): self._adapter_type = adapter_type - log.info("VirtualBox VM {name} [id={id}]: adapter type changed to {adapter_type}".format(name=self._name, - id=self._id, - adapter_type=adapter_type)) - - def _old_execute(self, subcommand, args, timeout=60): - """ - Executes a command with VBoxManage. - - :param subcommand: vboxmanage subcommand (e.g. modifyvm, controlvm etc.) - :param args: arguments for the subcommand. - :param timeout: how long to wait for vboxmanage - - :returns: result (list) - """ - - command = [self._vboxmanage_path, "--nologo", subcommand] - command.extend(args) - log.debug("Execute vboxmanage command: {}".format(command)) - user = self._vbox_user - try: - if not user.strip() or sys.platform.startswith("win") or sys.platform.startswith("darwin"): - result = subprocess.check_output(command, stderr=subprocess.STDOUT, timeout=timeout) - else: - sudo_command = "sudo -i -u " + user.strip() + " " + " ".join(command) - result = subprocess.check_output(sudo_command, stderr=subprocess.STDOUT, shell=True, timeout=timeout) - except subprocess.CalledProcessError as e: - if e.output: - # only the first line of the output is useful - virtualbox_error = e.output.decode("utf-8").splitlines()[0] - raise VirtualBoxError("{}".format(virtualbox_error)) - else: - raise VirtualBoxError("{}".format(e)) - except (OSError, subprocess.SubprocessError) as e: - raise VirtualBoxError("Could not execute VBoxManage: {}".format(e)) - return result.decode("utf-8", errors="ignore").splitlines() + log.info("VirtualBox VM '{name}' [{uuid}]: adapter type changed to {adapter_type}".format(name=self.name, + uuid=self.uuid, + adapter_type=adapter_type)) + @asyncio.coroutine def _get_vm_info(self): """ Returns this VM info. @@ -568,7 +597,7 @@ class VirtualBoxVM(BaseVM): """ vm_info = {} - results = self._execute("showvminfo", [self._vmname, "--machinereadable"]) + results = yield from self._execute("showvminfo", [self._vmname, "--machinereadable"]) for info in results: try: name, value = info.split('=', 1) @@ -577,20 +606,7 @@ class VirtualBoxVM(BaseVM): vm_info[name.strip('"')] = value.strip('"') return vm_info - def _get_vm_state(self): - """ - Returns this VM state (e.g. running, paused etc.) - - :returns: state (string) - """ - - results = self._execute("showvminfo", [self._vmname, "--machinereadable"]) - for info in results: - name, value = info.split('=', 1) - if name == "VMState": - return value.strip('"') - raise VirtualBoxError("Could not get VM state for {}".format(self._vmname)) - + @asyncio.coroutine def _get_maximum_supported_adapters(self): """ Returns the maximum adapters supported by this VM. @@ -599,7 +615,7 @@ class VirtualBoxVM(BaseVM): """ # check the maximum number of adapters supported by the VM - vm_info = self._get_vm_info() + vm_info = yield from self._get_vm_info() chipset = vm_info["chipset"] maximum_adapters = 8 if chipset == "ich9": @@ -615,46 +631,25 @@ class VirtualBoxVM(BaseVM): p = re.compile('\s+', re.UNICODE) pipe_name = p.sub("_", self._vmname) - if sys.platform.startswith('win'): + if sys.platform.startswith("win"): pipe_name = r"\\.\pipe\VBOX\{}".format(pipe_name) else: pipe_name = os.path.join(tempfile.gettempdir(), "pipe_{}".format(pipe_name)) return pipe_name + @asyncio.coroutine def _set_serial_console(self): """ Configures the first serial port to allow a serial console connection. """ # activate the first serial port - self._modify_vm("--uart1 0x3F8 4") + yield from self._modify_vm("--uart1 0x3F8 4") # set server mode with a pipe on the first serial port pipe_name = self._get_pipe_name() args = [self._vmname, "--uartmode1", "server", pipe_name] - self._execute("modifyvm", args) - - def _modify_vm(self, params): - """ - Change setting in this VM when not running. - - :param params: params to use with sub-command modifyvm - """ - - args = shlex.split(params) - self._execute("modifyvm", [self._vmname] + args) - - def _control_vm(self, params): - """ - Change setting in this VM when running. - - :param params: params to use with sub-command controlvm - - :returns: result of the command. - """ - - args = shlex.split(params) - return self._execute("controlvm", [self._vmname] + args) + yield from self._execute("modifyvm", args) def _storage_attach(self, params): """ @@ -666,6 +661,7 @@ class VirtualBoxVM(BaseVM): args = shlex.split(params) self._execute("storageattach", [self._vmname] + args) + @asyncio.coroutine def _get_nic_attachements(self, maximum_adapters): """ Returns NIC attachements. @@ -675,7 +671,7 @@ class VirtualBoxVM(BaseVM): """ nics = [] - vm_info = self._get_vm_info() + vm_info = yield from self._get_vm_info() for adapter_id in range(0, maximum_adapters): entry = "nic{}".format(adapter_id + 1) if entry in vm_info: @@ -685,12 +681,13 @@ class VirtualBoxVM(BaseVM): nics.append(None) return nics + @asyncio.coroutine def _set_network_options(self): """ Configures network options. """ - nic_attachements = self._get_nic_attachements(self._maximum_adapters) + nic_attachements = yield from self._get_nic_attachements(self._maximum_adapters) for adapter_id in range(0, len(self._ethernet_adapters)): if self._ethernet_adapters[adapter_id] is None: # force enable to avoid any discrepancy in the interface numbering inside the VM @@ -698,7 +695,7 @@ class VirtualBoxVM(BaseVM): attachement = nic_attachements[adapter_id] if attachement: # attachement can be none, null, nat, bridged, intnet, hostonly or generic - self._modify_vm("--nic{} {}".format(adapter_id + 1, attachement)) + yield from self._modify_vm("--nic{} {}".format(adapter_id + 1, attachement)) continue vbox_adapter_type = "82540EM" @@ -716,44 +713,45 @@ class VirtualBoxVM(BaseVM): vbox_adapter_type = "virtio" args = [self._vmname, "--nictype{}".format(adapter_id + 1), vbox_adapter_type] - self._execute("modifyvm", args) + yield from self._execute("modifyvm", args) - self._modify_vm("--nictrace{} off".format(adapter_id + 1)) + yield from self._modify_vm("--nictrace{} off".format(adapter_id + 1)) nio = self._ethernet_adapters[adapter_id].get_nio(0) if nio: log.debug("setting UDP params on adapter {}".format(adapter_id)) - self._modify_vm("--nic{} generic".format(adapter_id + 1)) - self._modify_vm("--nicgenericdrv{} UDPTunnel".format(adapter_id + 1)) - self._modify_vm("--nicproperty{} sport={}".format(adapter_id + 1, nio.lport)) - self._modify_vm("--nicproperty{} dest={}".format(adapter_id + 1, nio.rhost)) - self._modify_vm("--nicproperty{} dport={}".format(adapter_id + 1, nio.rport)) - self._modify_vm("--cableconnected{} on".format(adapter_id + 1)) + yield from self._modify_vm("--nic{} generic".format(adapter_id + 1)) + yield from self._modify_vm("--nicgenericdrv{} UDPTunnel".format(adapter_id + 1)) + yield from self._modify_vm("--nicproperty{} sport={}".format(adapter_id + 1, nio.lport)) + yield from self._modify_vm("--nicproperty{} dest={}".format(adapter_id + 1, nio.rhost)) + yield from self._modify_vm("--nicproperty{} dport={}".format(adapter_id + 1, nio.rport)) + yield from self._modify_vm("--cableconnected{} on".format(adapter_id + 1)) if nio.capturing: - self._modify_vm("--nictrace{} on".format(adapter_id + 1)) - self._modify_vm("--nictracefile{} {}".format(adapter_id + 1, nio.pcap_output_file)) + yield from self._modify_vm("--nictrace{} on".format(adapter_id + 1)) + yield from self._modify_vm("--nictracefile{} {}".format(adapter_id + 1, nio.pcap_output_file)) else: # shutting down unused adapters... - self._modify_vm("--cableconnected{} off".format(adapter_id + 1)) - self._modify_vm("--nic{} null".format(adapter_id + 1)) + yield from self._modify_vm("--cableconnected{} off".format(adapter_id + 1)) + yield from self._modify_vm("--nic{} null".format(adapter_id + 1)) for adapter_id in range(len(self._ethernet_adapters), self._maximum_adapters): log.debug("disabling remaining adapter {}".format(adapter_id)) - self._modify_vm("--nic{} none".format(adapter_id + 1)) + yield from self._modify_vm("--nic{} none".format(adapter_id + 1)) + @asyncio.coroutine def _create_linked_clone(self): """ Creates a new linked clone. """ gns3_snapshot_exists = False - vm_info = self._get_vm_info() + vm_info = yield from self._get_vm_info() for entry, value in vm_info.items(): if entry.startswith("SnapshotName") and value == "GNS3 Linked Base for clones": gns3_snapshot_exists = True if not gns3_snapshot_exists: - result = self._execute("snapshot", [self._vmname, "take", "GNS3 Linked Base for clones"]) + result = yield from self._execute("snapshot", [self._vmname, "take", "GNS3 Linked Base for clones"]) log.debug("GNS3 snapshot created: {}".format(result)) args = [self._vmname, @@ -767,15 +765,15 @@ class VirtualBoxVM(BaseVM): self._working_dir, "--register"] - result = self._execute("clonevm", args) + result = yield from self._execute("clonevm", args) log.debug("cloned VirtualBox VM: {}".format(result)) self._vmname = self._name - self._execute("setextradata", [self._vmname, "GNS3/Clone", "yes"]) + yield from self._execute("setextradata", [self._vmname, "GNS3/Clone", "yes"]) args = [self._name, "take", "reset"] - result = self._execute("snapshot", args) - log.debug("snapshot reset created: {}".format(result)) + result = yield from self._execute("snapshot", args) + log.debug("Snapshot reset created: {}".format(result)) def _start_remote_console(self): """ @@ -784,7 +782,7 @@ class VirtualBoxVM(BaseVM): # starts the Telnet to pipe thread pipe_name = self._get_pipe_name() - if sys.platform.startswith('win'): + if sys.platform.startswith("win"): try: self._serial_pipe = open(pipe_name, "a+b") except OSError as e: @@ -809,7 +807,7 @@ class VirtualBoxVM(BaseVM): self._telnet_server_thread.stop() self._telnet_server_thread.join(timeout=3) if self._telnet_server_thread.isAlive(): - log.warn("Serial pire thread is still alive!") + log.warn("Serial pipe thread is still alive!") self._telnet_server_thread = None if self._serial_pipe: @@ -819,93 +817,7 @@ class VirtualBoxVM(BaseVM): self._serial_pipe.close() self._serial_pipe = None - def old_start(self): - """ - Starts this VirtualBox VM. - """ - - # resume the VM if it is paused - vm_state = self._get_vm_state() - if vm_state == "paused": - self.resume() - return - - # VM must be powered off and in saved state to start it - if vm_state != "poweroff" and vm_state != "saved": - raise VirtualBoxError("VirtualBox VM not powered off or saved") - - self._set_network_options() - self._set_serial_console() - - args = [self._vmname] - if self._headless: - args.extend(["--type", "headless"]) - result = self._execute("startvm", args) - log.debug("started VirtualBox VM: {}".format(result)) - - # add a guest property to let the VM know about the GNS3 name - self._execute("guestproperty", ["set", self._vmname, "NameInGNS3", self._name]) - - # add a guest property to let the VM know about the GNS3 project directory - self._execute("guestproperty", ["set", self._vmname, "ProjectDirInGNS3", self._working_dir]) - - if self._enable_remote_console: - self._start_remote_console() - - def old_stop(self): - """ - Stops this VirtualBox VM. - """ - - self._stop_remote_console() - vm_state = self._get_vm_state() - if vm_state == "running" or vm_state == "paused" or vm_state == "stuck": - # power off the VM - result = self._control_vm("poweroff") - log.debug("VirtualBox VM has been stopped: {}".format(result)) - - time.sleep(0.5) # give some time for VirtualBox to unlock the VM - # deactivate the first serial port - try: - self._modify_vm("--uart1 off") - except VirtualBoxError as e: - log.warn("Could not deactivate the first serial port: {}".format(e)) - - for adapter_id in range(0, len(self._ethernet_adapters)): - if self._ethernet_adapters[adapter_id] is None: - continue - self._modify_vm("--nictrace{} off".format(adapter_id + 1)) - self._modify_vm("--cableconnected{} off".format(adapter_id + 1)) - self._modify_vm("--nic{} null".format(adapter_id + 1)) - - def suspend(self): - """ - Suspends this VirtualBox VM. - """ - - vm_state = self._get_vm_state() - if vm_state == "running": - result = self._control_vm("pause") - log.debug("VirtualBox VM has been suspended: {}".format(result)) - else: - log.info("VirtualBox VM is not running to be suspended, current state is {}".format(vm_state)) - - def resume(self): - """ - Resumes this VirtualBox VM. - """ - - result = self._control_vm("resume") - log.debug("VirtualBox VM has been resumed: {}".format(result)) - - def reload(self): - """ - Reloads this VirtualBox VM. - """ - - result = self._control_vm("reset") - log.debug("VirtualBox VM has been reset: {}".format(result)) - + @asyncio.coroutine def port_add_nio_binding(self, adapter_id, nio): """ Adds a port NIO binding. @@ -917,24 +829,25 @@ class VirtualBoxVM(BaseVM): try: adapter = self._ethernet_adapters[adapter_id] except IndexError: - raise VirtualBoxError("Adapter {adapter_id} doesn't exist on VirtualBox VM {name}".format(name=self._name, - adapter_id=adapter_id)) + raise VirtualBoxError("Adapter {adapter_id} doesn't exist on VirtualBox VM '{name}'".format(name=self.name, + adapter_id=adapter_id)) - vm_state = self._get_vm_state() + vm_state = yield from self._get_vm_state() if vm_state == "running": # dynamically configure an UDP tunnel on the VirtualBox adapter - self._control_vm("nic{} generic UDPTunnel".format(adapter_id + 1)) - self._control_vm("nicproperty{} sport={}".format(adapter_id + 1, nio.lport)) - self._control_vm("nicproperty{} dest={}".format(adapter_id + 1, nio.rhost)) - self._control_vm("nicproperty{} dport={}".format(adapter_id + 1, nio.rport)) - self._control_vm("setlinkstate{} on".format(adapter_id + 1)) + yield from self._control_vm("nic{} generic UDPTunnel".format(adapter_id + 1)) + yield from self._control_vm("nicproperty{} sport={}".format(adapter_id + 1, nio.lport)) + yield from self._control_vm("nicproperty{} dest={}".format(adapter_id + 1, nio.rhost)) + yield from self._control_vm("nicproperty{} dport={}".format(adapter_id + 1, nio.rport)) + yield from self._control_vm("setlinkstate{} on".format(adapter_id + 1)) adapter.add_nio(0, nio) - log.info("VirtualBox VM {name} [id={id}]: {nio} added to adapter {adapter_id}".format(name=self._name, - id=self._id, - nio=nio, - adapter_id=adapter_id)) + log.info("VirtualBox VM '{name}' [{uuid}]: {nio} added to adapter {adapter_id}".format(name=self.name, + uuid=self.uuid, + nio=nio, + adapter_id=adapter_id)) + @asyncio.coroutine def port_remove_nio_binding(self, adapter_id): """ Removes a port NIO binding. @@ -947,21 +860,21 @@ class VirtualBoxVM(BaseVM): try: adapter = self._ethernet_adapters[adapter_id] except IndexError: - raise VirtualBoxError("Adapter {adapter_id} doesn't exist on VirtualBox VM {name}".format(name=self._name, - adapter_id=adapter_id)) + raise VirtualBoxError("Adapter {adapter_id} doesn't exist on VirtualBox VM '{name}'".format(name=self.name, + adapter_id=adapter_id)) - vm_state = self._get_vm_state() + vm_state = yield from self._get_vm_state() if vm_state == "running": # dynamically disable the VirtualBox adapter - self._control_vm("setlinkstate{} off".format(adapter_id + 1)) - self._control_vm("nic{} null".format(adapter_id + 1)) + yield from self._control_vm("setlinkstate{} off".format(adapter_id + 1)) + yield from self._control_vm("nic{} null".format(adapter_id + 1)) nio = adapter.get_nio(0) adapter.remove_nio(0) - log.info("VirtualBox VM {name} [id={id}]: {nio} removed from adapter {adapter_id}".format(name=self._name, - id=self._id, - nio=nio, - adapter_id=adapter_id)) + log.info("VirtualBox VM '{name}' [{uuid}]: {nio} removed from adapter {adapter_id}".format(name=self.name, + uuid=self.uuid, + nio=nio, + adapter_id=adapter_id)) return nio def start_capture(self, adapter_id, output_file): @@ -975,25 +888,23 @@ class VirtualBoxVM(BaseVM): try: adapter = self._ethernet_adapters[adapter_id] except IndexError: - raise VirtualBoxError("Adapter {adapter_id} doesn't exist on VirtualBox VM {name}".format(name=self._name, - adapter_id=adapter_id)) + raise VirtualBoxError("Adapter {adapter_id} doesn't exist on VirtualBox VM '{name}'".format(name=self.name, + adapter_id=adapter_id)) nio = adapter.get_nio(0) if nio.capturing: raise VirtualBoxError("Packet capture is already activated on adapter {adapter_id}".format(adapter_id=adapter_id)) try: - os.makedirs(os.path.dirname(output_file)) - except FileExistsError: - pass + os.makedirs(os.path.dirname(output_file), exist_ok=True) except OSError as e: raise VirtualBoxError("Could not create captures directory {}".format(e)) nio.startPacketCapture(output_file) - log.info("VirtualBox VM {name} [id={id}]: starting packet capture on adapter {adapter_id}".format(name=self._name, - id=self._id, - adapter_id=adapter_id)) + log.info("VirtualBox VM '{name}' [{uuid}]: starting packet capture on adapter {adapter_id}".format(name=self.name, + uuid=self.uuid, + adapter_id=adapter_id)) def stop_capture(self, adapter_id): """ @@ -1005,12 +916,12 @@ class VirtualBoxVM(BaseVM): try: adapter = self._ethernet_adapters[adapter_id] except IndexError: - raise VirtualBoxError("Adapter {adapter_id} doesn't exist on VirtualBox VM {name}".format(name=self._name, - adapter_id=adapter_id)) + raise VirtualBoxError("Adapter {adapter_id} doesn't exist on VirtualBox VM '{name}'".format(name=self.name, + adapter_id=adapter_id)) nio = adapter.get_nio(0) nio.stopPacketCapture() - log.info("VirtualBox VM {name} [id={id}]: stopping packet capture on adapter {adapter_id}".format(name=self._name, - id=self._id, - adapter_id=adapter_id)) + log.info("VirtualBox VM '{name}' [{uuid}]: stopping packet capture on adapter {adapter_id}".format(name=self.name, + uuid=self.uuid, + adapter_id=adapter_id)) From 3b7d08a80ed26a665734ad9f245b6765043bf69d Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 21 Jan 2015 19:28:52 -0700 Subject: [PATCH 088/485] Suspend and resume for VirtualBox. --- gns3server/handlers/virtualbox_handler.py | 38 +++++++++++++++++++++++ gns3server/modules/project.py | 8 ++--- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/gns3server/handlers/virtualbox_handler.py b/gns3server/handlers/virtualbox_handler.py index 209c68d3..8b7125a6 100644 --- a/gns3server/handlers/virtualbox_handler.py +++ b/gns3server/handlers/virtualbox_handler.py @@ -87,3 +87,41 @@ class VirtualBoxHandler: vm = vbox_manager.get_vm(request.match_info["uuid"]) yield from vm.stop() response.set_status(204) + + @classmethod + @Route.post( + r"/virtualbox/{uuid}/suspend", + parameters={ + "uuid": "VirtualBox VM instance UUID" + }, + status_codes={ + 204: "VirtualBox VM instance suspended", + 400: "Invalid VirtualBox VM instance UUID", + 404: "VirtualBox VM instance doesn't exist" + }, + description="Suspend a VirtualBox VM instance") + def suspend(request, response): + + vbox_manager = VirtualBox.instance() + vm = vbox_manager.get_vm(request.match_info["uuid"]) + yield from vm.suspend() + response.set_status(204) + + @classmethod + @Route.post( + r"/virtualbox/{uuid}/resume", + parameters={ + "uuid": "VirtualBox VM instance UUID" + }, + status_codes={ + 204: "VirtualBox VM instance resumed", + 400: "Invalid VirtualBox VM instance UUID", + 404: "VirtualBox VM instance doesn't exist" + }, + description="Resume a suspended VirtualBox VM instance") + def suspend(request, response): + + vbox_manager = VirtualBox.instance() + vm = vbox_manager.get_vm(request.match_info["uuid"]) + yield from vm.resume() + response.set_status(204) diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index 31e00171..9e83ffae 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -75,14 +75,12 @@ class Project: :param vm_uuid: VM UUID """ - p = os.path.join(self._path, module, vm_uuid) + workdir = os.path.join(self._path, module, vm_uuid) try: - os.makedirs(p, exist_ok=True) - except FileExistsError: - pass + os.makedirs(workdir, exist_ok=True) except OSError as e: raise aiohttp.web.HTTPInternalServerError(text="Could not create VM working directory: {}".format(e)) - return p + return workdir def __json__(self): From 1a43ff118c3b3d0e635866de629c96f1463fa2e3 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 21 Jan 2015 19:30:24 -0700 Subject: [PATCH 089/485] Fix tests and clean. --- tests/api/test_project.py | 4 --- tests/api/test_version.py | 1 - tests/api/test_virtualbox.py | 43 +++++++++++++++++++++++------- tests/api/test_vpcs.py | 3 +-- tests/modules/test_project.py | 2 +- tests/modules/vpcs/test_vpcs_vm.py | 2 -- 6 files changed, 36 insertions(+), 19 deletions(-) diff --git a/tests/api/test_project.py b/tests/api/test_project.py index 7c7d1c6b..1c42bd63 100644 --- a/tests/api/test_project.py +++ b/tests/api/test_project.py @@ -20,10 +20,6 @@ This test suite check /project endpoint """ -from tests.utils import asyncio_patch -from gns3server.version import __version__ - - def test_create_project_with_dir(server, tmpdir): response = server.post("/project", {"location": str(tmpdir)}) assert response.status == 200 diff --git a/tests/api/test_version.py b/tests/api/test_version.py index 76e7db72..1e9260f2 100644 --- a/tests/api/test_version.py +++ b/tests/api/test_version.py @@ -20,7 +20,6 @@ This test suite check /version endpoint It's also used for unittest the HTTP implementation. """ -from tests.utils import asyncio_patch from gns3server.version import __version__ diff --git a/tests/api/test_virtualbox.py b/tests/api/test_virtualbox.py index ed5ae696..05dbfc0e 100644 --- a/tests/api/test_virtualbox.py +++ b/tests/api/test_virtualbox.py @@ -15,14 +15,25 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import pytest from tests.utils import asyncio_patch +@pytest.fixture(scope="module") +def vm(server, project): + with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.create", return_value=True) as mock: + response = server.post("/virtualbox", {"name": "VM1", + "vmname": "VM1", + "linked_clone": False, + "project_uuid": project.uuid}) + assert mock.called + assert response.status == 201 + return response.json + + def test_vbox_create(server, project): - with asyncio_patch("gns3server.modules.VirtualBox.create_vm", return_value={"name": "VM1", - "uuid": "61d61bdd-aa7d-4912-817f-65a9eb54d3ab", - "project_uuid": project.uuid}): + with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.create", return_value=True): response = server.post("/virtualbox", {"name": "VM1", "vmname": "VM1", "linked_clone": False, @@ -33,15 +44,29 @@ def test_vbox_create(server, project): assert response.json["project_uuid"] == project.uuid -def test_vbox_start(server): - with asyncio_patch("gns3server.modules.VirtualBox.start_vm", return_value=True) as mock: - response = server.post("/virtualbox/61d61bdd-aa7d-4912-817f-65a9eb54d3ab/start", {}, example=True) +def test_vbox_start(server, vm): + with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.start", return_value=True) as mock: + response = server.post("/virtualbox/{}/start".format(vm["uuid"])) + assert mock.called + assert response.status == 204 + + +def test_vbox_stop(server, vm): + with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.stop", return_value=True) as mock: + response = server.post("/virtualbox/{}/stop".format(vm["uuid"])) + assert mock.called + assert response.status == 204 + + +def test_vbox_suspend(server, vm): + with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.suspend", return_value=True) as mock: + response = server.post("/virtualbox/{}/suspend".format(vm["uuid"])) assert mock.called assert response.status == 204 -def test_vbox_stop(server): - with asyncio_patch("gns3server.modules.VirtualBox.stop_vm", return_value=True) as mock: - response = server.post("/virtualbox/61d61bdd-aa7d-4912-817f-65a9eb54d3ab/stop", {}, example=True) +def test_vbox_resume(server, vm): + with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.resume", return_value=True) as mock: + response = server.post("/virtualbox/{}/resume".format(vm["uuid"])) assert mock.called assert response.status == 204 diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index 5fed9aa9..527738c4 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -18,8 +18,7 @@ import pytest import os from tests.utils import asyncio_patch -from unittest.mock import patch, Mock -from gns3server.modules.vpcs.vpcs_vm import VPCSVM +from unittest.mock import patch @pytest.fixture(scope="module") diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py index cb980b76..d641a37e 100644 --- a/tests/modules/test_project.py +++ b/tests/modules/test_project.py @@ -48,4 +48,4 @@ def test_json(tmpdir): def test_vm_working_directory(tmpdir): p = Project(location=str(tmpdir)) assert os.path.exists(p.vm_working_directory('vpcs', '00010203-0405-0607-0809-0a0b0c0d0e0f')) - assert os.path.exists(os.path.join(str(tmpdir), p.uuid, 'vms', 'vpcs', '00010203-0405-0607-0809-0a0b0c0d0e0f')) + assert os.path.exists(os.path.join(str(tmpdir), p.uuid, 'vpcs', '00010203-0405-0607-0809-0a0b0c0d0e0f')) diff --git a/tests/modules/vpcs/test_vpcs_vm.py b/tests/modules/vpcs/test_vpcs_vm.py index 9887c97b..635b3834 100644 --- a/tests/modules/vpcs/test_vpcs_vm.py +++ b/tests/modules/vpcs/test_vpcs_vm.py @@ -21,12 +21,10 @@ import os from tests.utils import asyncio_patch -from asyncio.subprocess import Process from unittest.mock import patch, MagicMock from gns3server.modules.vpcs.vpcs_vm import VPCSVM from gns3server.modules.vpcs.vpcs_error import VPCSError from gns3server.modules.vpcs import VPCS -from gns3server.modules.port_manager import PortManager @pytest.fixture(scope="module") From 8d3ea604044e09530d5b5abb425617e8d4341b87 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 22 Jan 2015 10:55:11 +0100 Subject: [PATCH 090/485] VPCS reload --- docs/api/examples/get_vpcsuuid.txt | 2 +- docs/api/examples/post_virtualbox.txt | 5 +++-- docs/api/examples/post_vpcs.txt | 2 +- docs/api/project.rst | 2 +- docs/api/version.rst | 4 ++-- docs/api/virtualbox.rst | 8 ++++---- docs/api/virtualboxuuidstart.rst | 2 +- docs/api/virtualboxuuidstop.rst | 2 +- docs/api/vpcs.rst | 2 +- docs/api/vpcsuuid.rst | 4 ++-- docs/api/vpcsuuidportsportidnio.rst | 4 ++-- docs/api/vpcsuuidstart.rst | 2 +- docs/api/vpcsuuidstop.rst | 2 +- gns3server/handlers/vpcs_handler.py | 18 ++++++++++++++++++ gns3server/modules/vpcs/vpcs_vm.py | 10 +++++++++- gns3server/web/documentation.py | 2 +- tests/api/test_vpcs.py | 7 +++++++ tests/modules/vpcs/test_vpcs_vm.py | 13 +++++++++++++ 18 files changed, 69 insertions(+), 22 deletions(-) diff --git a/docs/api/examples/get_vpcsuuid.txt b/docs/api/examples/get_vpcsuuid.txt index f0eb88f9..23e0c6af 100644 --- a/docs/api/examples/get_vpcsuuid.txt +++ b/docs/api/examples/get_vpcsuuid.txt @@ -18,5 +18,5 @@ X-ROUTE: /vpcs/{uuid} "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, "startup_script": null, - "uuid": "a0ecc3ea-907f-4751-9415-9f6d5da4dc3a" + "uuid": "925c4d08-58a5-4078-9e77-a6875e0c28dc" } diff --git a/docs/api/examples/post_virtualbox.txt b/docs/api/examples/post_virtualbox.txt index efc04fdb..fdb19bc6 100644 --- a/docs/api/examples/post_virtualbox.txt +++ b/docs/api/examples/post_virtualbox.txt @@ -1,7 +1,8 @@ -curl -i -X POST 'http://localhost:8000/virtualbox' -d '{"name": "VM1", "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "vmname": "VM1"}' +curl -i -X POST 'http://localhost:8000/virtualbox' -d '{"linked_clone": false, "name": "VM1", "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "vmname": "VM1"}' POST /virtualbox HTTP/1.1 { + "linked_clone": false, "name": "VM1", "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "vmname": "VM1" @@ -19,5 +20,5 @@ X-ROUTE: /virtualbox { "name": "VM1", "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "uuid": "0f7b32bb-13e1-4c3f-8176-bbf277672b58" + "uuid": "c220788f-ee1e-491c-b318-6542d2f130bf" } diff --git a/docs/api/examples/post_vpcs.txt b/docs/api/examples/post_vpcs.txt index 67c5e242..b66cac02 100644 --- a/docs/api/examples/post_vpcs.txt +++ b/docs/api/examples/post_vpcs.txt @@ -21,5 +21,5 @@ X-ROUTE: /vpcs "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, "startup_script": null, - "uuid": "f4f04818-610c-4e95-aa0e-6d29afa72fc7" + "uuid": "4d670947-44a8-4156-8626-adce3faa5ae6" } diff --git a/docs/api/project.rst b/docs/api/project.rst index 00e354f2..22b8dcfb 100644 --- a/docs/api/project.rst +++ b/docs/api/project.rst @@ -4,7 +4,7 @@ .. contents:: POST /project -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Create a project on the server Response status codes diff --git a/docs/api/version.rst b/docs/api/version.rst index 66da7325..f98b52f5 100644 --- a/docs/api/version.rst +++ b/docs/api/version.rst @@ -4,7 +4,7 @@ .. contents:: GET /version -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Retrieve the server version number Response status codes @@ -28,7 +28,7 @@ Sample session POST /version -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Check if version is the same as the server Response status codes diff --git a/docs/api/virtualbox.rst b/docs/api/virtualbox.rst index 9a6cb762..3f96363c 100644 --- a/docs/api/virtualbox.rst +++ b/docs/api/virtualbox.rst @@ -4,7 +4,7 @@ .. contents:: POST /virtualbox -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Create a new VirtualBox VM instance Response status codes @@ -19,9 +19,9 @@ Input - + - + @@ -35,7 +35,7 @@ Output - +
Name Mandatory Type Description
linked_clone boolean either the VM is a linked clone or not
linked_clone boolean either the VM is a linked clone or not
name string VirtualBox VM instance name
project_uuid string Project UUID
project_uuid string Project UUID
uuid string VirtualBox VM instance UUID
vbox_id integer VirtualBox VM instance ID (for project created before GNS3 1.3)
vmname string VirtualBox VM name (in VirtualBox itself)
Name Mandatory Type Description
console integer console TCP port
name string VirtualBox VM instance name
project_uuid string Project UUID
project_uuid string Project UUID
uuid string VirtualBox VM instance UUID
diff --git a/docs/api/virtualboxuuidstart.rst b/docs/api/virtualboxuuidstart.rst index 570a64f7..f60d7c22 100644 --- a/docs/api/virtualboxuuidstart.rst +++ b/docs/api/virtualboxuuidstart.rst @@ -4,7 +4,7 @@ .. contents:: POST /virtualbox/**{uuid}**/start -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Start a VirtualBox VM instance Parameters diff --git a/docs/api/virtualboxuuidstop.rst b/docs/api/virtualboxuuidstop.rst index 21fd9809..d8a58373 100644 --- a/docs/api/virtualboxuuidstop.rst +++ b/docs/api/virtualboxuuidstop.rst @@ -4,7 +4,7 @@ .. contents:: POST /virtualbox/**{uuid}**/stop -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Stop a VirtualBox VM instance Parameters diff --git a/docs/api/vpcs.rst b/docs/api/vpcs.rst index 4790ad9e..c525760a 100644 --- a/docs/api/vpcs.rst +++ b/docs/api/vpcs.rst @@ -4,7 +4,7 @@ .. contents:: POST /vpcs -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Create a new VPCS instance Response status codes diff --git a/docs/api/vpcsuuid.rst b/docs/api/vpcsuuid.rst index bba2e33f..43bca0e5 100644 --- a/docs/api/vpcsuuid.rst +++ b/docs/api/vpcsuuid.rst @@ -4,7 +4,7 @@ .. contents:: GET /vpcs/**{uuid}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Get a VPCS instance Parameters @@ -24,7 +24,7 @@ Sample session PUT /vpcs/**{uuid}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Update a VPCS instance Response status codes diff --git a/docs/api/vpcsuuidportsportidnio.rst b/docs/api/vpcsuuidportsportidnio.rst index c186c9bb..fd5090de 100644 --- a/docs/api/vpcsuuidportsportidnio.rst +++ b/docs/api/vpcsuuidportsportidnio.rst @@ -4,7 +4,7 @@ .. contents:: POST /vpcs/**{uuid}**/ports/**{port_id}**/nio -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Add a NIO to a VPCS Parameters @@ -26,7 +26,7 @@ Sample session DELETE /vpcs/**{uuid}**/ports/**{port_id}**/nio -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Remove a NIO from a VPCS Parameters diff --git a/docs/api/vpcsuuidstart.rst b/docs/api/vpcsuuidstart.rst index bcf1f8ea..4acce2fc 100644 --- a/docs/api/vpcsuuidstart.rst +++ b/docs/api/vpcsuuidstart.rst @@ -4,7 +4,7 @@ .. contents:: POST /vpcs/**{uuid}**/start -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Start a VPCS instance Parameters diff --git a/docs/api/vpcsuuidstop.rst b/docs/api/vpcsuuidstop.rst index 16702e07..3b6e76fe 100644 --- a/docs/api/vpcsuuidstop.rst +++ b/docs/api/vpcsuuidstop.rst @@ -4,7 +4,7 @@ .. contents:: POST /vpcs/**{uuid}**/stop -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Stop a VPCS instance Parameters diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index 86ed9312..18e69709 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -169,3 +169,21 @@ class VPCSHandler: vm = vpcs_manager.get_vm(request.match_info["uuid"]) nio = vm.port_remove_nio_binding(int(request.match_info["port_id"])) response.set_status(204) + + @classmethod + @Route.post( + r"/vpcs/{uuid}/reload", + parameters={ + "uuid": "VPCS instance UUID", + }, + status_codes={ + 204: "VPCS reloaded", + 404: "VPCS instance doesn't exist" + }, + description="Remove a NIO from a VPCS") + def reload(request, response): + + vpcs_manager = VPCS.instance() + vm = vpcs_manager.get_vm(request.match_info["uuid"]) + nio = vm.reload() + response.set_status(204) diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index 35c61407..8b43d9d0 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -252,6 +252,15 @@ class VPCSVM(BaseVM): self._process = None self._started = False + @asyncio.coroutine + def reload(self): + """ + Reload the VPCS process. (Stop / Start) + """ + + yield from self.stop() + yield from self.start() + def _kill_process(self): """Kill the process if running""" @@ -271,7 +280,6 @@ class VPCSVM(BaseVM): Reads the standard output of the VPCS process. Only use when the process has been stopped or has crashed. """ - # TODO: should be async output = "" if self._vpcs_stdout_file: try: diff --git a/gns3server/web/documentation.py b/gns3server/web/documentation.py index 35aee835..66ddf1f0 100644 --- a/gns3server/web/documentation.py +++ b/gns3server/web/documentation.py @@ -38,7 +38,7 @@ class Documentation(object): f.write('.. contents::\n') for method in handler_doc["methods"]: f.write('\n{} {}\n'.format(method["method"], path.replace("{", '**{').replace("}", "}**"))) - f.write('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n') + f.write('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n') f.write('{}\n\n'.format(method["description"])) if len(method["parameters"]) > 0: diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index 527738c4..e8b61981 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -119,6 +119,13 @@ def test_vpcs_stop(server, vm): assert response.status == 204 +def test_vpcs_reload(server, vm): + with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM.reload", return_value=True) as mock: + response = server.post("/vpcs/{}/reload".format(vm["uuid"])) + assert mock.called + assert response.status == 204 + + def test_vpcs_update(server, vm, tmpdir, free_console_port): path = os.path.join(str(tmpdir), 'startup2.vpcs') with open(path, 'w+') as f: diff --git a/tests/modules/vpcs/test_vpcs_vm.py b/tests/modules/vpcs/test_vpcs_vm.py index 635b3834..c7c8518f 100644 --- a/tests/modules/vpcs/test_vpcs_vm.py +++ b/tests/modules/vpcs/test_vpcs_vm.py @@ -87,6 +87,19 @@ def test_stop(loop, vm): process.terminate.assert_called_with() +def test_reload(loop, vm): + process = MagicMock() + with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM._check_requirements", return_value=True): + with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): + nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) + + loop.run_until_complete(asyncio.async(vm.start())) + assert vm.is_running() + loop.run_until_complete(asyncio.async(vm.reload())) + assert vm.is_running() is True + process.terminate.assert_called_with() + + def test_add_nio_binding_udp(vm): nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) assert nio.lport == 4242 From 55052c9bca53692f30d23550d1b5b8439cd59cc6 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 22 Jan 2015 10:56:02 +0100 Subject: [PATCH 091/485] Add missing documentations --- docs/api/virtualboxuuidresume.rst | 19 +++++++++++++++++++ docs/api/virtualboxuuidsuspend.rst | 19 +++++++++++++++++++ docs/api/vpcsuuidreload.rst | 18 ++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 docs/api/virtualboxuuidresume.rst create mode 100644 docs/api/virtualboxuuidsuspend.rst create mode 100644 docs/api/vpcsuuidreload.rst diff --git a/docs/api/virtualboxuuidresume.rst b/docs/api/virtualboxuuidresume.rst new file mode 100644 index 00000000..5d45900b --- /dev/null +++ b/docs/api/virtualboxuuidresume.rst @@ -0,0 +1,19 @@ +/virtualbox/{uuid}/resume +--------------------------------------------- + +.. contents:: + +POST /virtualbox/**{uuid}**/resume +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Resume a suspended VirtualBox VM instance + +Parameters +********** +- **uuid**: VirtualBox VM instance UUID + +Response status codes +********************** +- **400**: Invalid VirtualBox VM instance UUID +- **404**: VirtualBox VM instance doesn't exist +- **204**: VirtualBox VM instance resumed + diff --git a/docs/api/virtualboxuuidsuspend.rst b/docs/api/virtualboxuuidsuspend.rst new file mode 100644 index 00000000..abb9a98c --- /dev/null +++ b/docs/api/virtualboxuuidsuspend.rst @@ -0,0 +1,19 @@ +/virtualbox/{uuid}/suspend +--------------------------------------------- + +.. contents:: + +POST /virtualbox/**{uuid}**/suspend +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Suspend a VirtualBox VM instance + +Parameters +********** +- **uuid**: VirtualBox VM instance UUID + +Response status codes +********************** +- **400**: Invalid VirtualBox VM instance UUID +- **404**: VirtualBox VM instance doesn't exist +- **204**: VirtualBox VM instance suspended + diff --git a/docs/api/vpcsuuidreload.rst b/docs/api/vpcsuuidreload.rst new file mode 100644 index 00000000..263afc08 --- /dev/null +++ b/docs/api/vpcsuuidreload.rst @@ -0,0 +1,18 @@ +/vpcs/{uuid}/reload +--------------------------------------------- + +.. contents:: + +POST /vpcs/**{uuid}**/reload +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Remove a NIO from a VPCS + +Parameters +********** +- **uuid**: VPCS instance UUID + +Response status codes +********************** +- **404**: VPCS instance doesn't exist +- **204**: VPCS reloaded + From 545a3d2b58bad8f086c58b551a466e12f2904f42 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 22 Jan 2015 10:57:08 +0100 Subject: [PATCH 092/485] PEP8 --- gns3server/modules/virtualbox/virtualbox_vm.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index b44c9107..6ef5c2d2 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -70,12 +70,12 @@ class VirtualBoxVM(BaseVM): self._adapter_start_index = 0 self._adapter_type = "Intel PRO/1000 MT Desktop (82540EM)" - #TODO: finish adapters support - #self.adapters = 2 # creates 2 adapters by default + # TODO: finish adapters support + # self.adapters = 2 # creates 2 adapters by default def __json__(self): - #TODO: send more info + # TODO: send more info # {"name": self._name, # "vmname": self._vmname, # "adapters": self.adapters, @@ -96,7 +96,7 @@ class VirtualBoxVM(BaseVM): command.extend(args) try: if self._vbox_user and self._vbox_user.strip(): - #TODO: test & review this part + # TODO: test & review this part sudo_command = "sudo -i -u {}".format(self._vbox_user.strip()) + " ".join(command) process = yield from asyncio.create_subprocess_shell(sudo_command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) else: @@ -194,7 +194,7 @@ class VirtualBoxVM(BaseVM): log.info("VirtualBox VM '{name}' [{uuid}] created".format(name=self.name, uuid=self.uuid)) if self._linked_clone: - #TODO: finish linked clone support + # TODO: finish linked clone support if self.uuid and os.path.isdir(os.path.join(self.working_dir, self._vmname)): vbox_file = os.path.join(self.working_dir, self._vmname, self._vmname + ".vbox") self._execute("registervm", [vbox_file]) From e12e6044dc0521b7fc6169bcf42feebc20878fb2 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 22 Jan 2015 11:34:10 +0100 Subject: [PATCH 093/485] Delete VPCS --- gns3server/handlers/vpcs_handler.py | 15 ++++++++++++++- gns3server/modules/base_manager.py | 15 +++++++++++++++ gns3server/modules/vpcs/vpcs_vm.py | 6 ++++++ tests/api/test_vpcs.py | 7 +++++++ tests/modules/vpcs/test_vpcs_vm.py | 11 +++++++++++ 5 files changed, 53 insertions(+), 1 deletion(-) diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index 18e69709..e97e24bc 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -74,6 +74,7 @@ class VPCSHandler: r"/vpcs/{uuid}", status_codes={ 200: "VPCS instance updated", + 404: "VPCS instance doesn't exist", 409: "Conflict" }, description="Update a VPCS instance", @@ -87,9 +88,21 @@ class VPCSHandler: vm.console = request.json.get("console", vm.console) vm.script_file = request.json.get("script_file", vm.script_file) vm.startup_script = request.json.get("startup_script", vm.startup_script) - response.json(vm) + @classmethod + @Route.delete( + r"/vpcs/{uuid}", + status_codes={ + 204: "VPCS instance updated", + 404: "VPCS instance doesn't exist" + }, + description="Delete a VPCS instance") + def delete(request, response): + + yield from VPCS.instance().delete_vm(request.match_info["uuid"]) + response.set_status(204) + @classmethod @Route.post( r"/vpcs/{uuid}/start", diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 402b9422..992a1b29 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -132,3 +132,18 @@ class BaseManager: vm.create() self._vms[vm.uuid] = vm return vm + + @asyncio.coroutine + def delete_vm(self, uuid): + """ + Delete a VM + + :param uuid: VM UUID + """ + + vm = self.get_vm(uuid) + if asyncio.iscoroutinefunction(vm.destroy): + yield from vm.destroy() + else: + vm.destroy() + del self._vms[vm.uuid] diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index 8b43d9d0..d7ef3d3c 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -82,7 +82,13 @@ class VPCSVM(BaseVM): self._console = self._manager.port_manager.get_free_console_port() def __del__(self): + self.destroy() + + def destroy(self): self._kill_process() + if self._console: + self._manager.port_manager.release_console_port(self._console) + self._console = None @asyncio.coroutine def _check_requirements(self): diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index e8b61981..3c59c585 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -126,6 +126,13 @@ def test_vpcs_reload(server, vm): assert response.status == 204 +def test_vpcs_delete(server, vm): + with asyncio_patch("gns3server.modules.vpcs.VPCS.delete_vm", return_value=True) as mock: + response = server.delete("/vpcs/{}".format(vm["uuid"])) + assert mock.called + assert response.status == 204 + + def test_vpcs_update(server, vm, tmpdir, free_console_port): path = os.path.join(str(tmpdir), 'startup2.vpcs') with open(path, 'w+') as f: diff --git a/tests/modules/vpcs/test_vpcs_vm.py b/tests/modules/vpcs/test_vpcs_vm.py index c7c8518f..f3ac4dc3 100644 --- a/tests/modules/vpcs/test_vpcs_vm.py +++ b/tests/modules/vpcs/test_vpcs_vm.py @@ -177,3 +177,14 @@ def test_change_script_file(vm, tmpdir): path = os.path.join(str(tmpdir), 'startup2.vpcs') vm.script_file = path assert vm.script_file == path + + +def test_destroy(vm, port_manager): + with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM._check_requirements", return_value=True): + with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): + vm.start() + port = vm.console + vm.destroy() + # Raise an exception if the port is not free + port_manager.reserve_console_port(port) + assert vm.is_running() is False From 08b2dc6369baa22a142118c6a0bb984c34762f21 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 22 Jan 2015 11:49:22 +0100 Subject: [PATCH 094/485] Cleanup VMS when leaving --- gns3server/modules/base_manager.py | 7 +++++++ gns3server/modules/base_vm.py | 10 +++++++++- gns3server/modules/vpcs/vpcs_vm.py | 3 --- gns3server/server.py | 11 +++++------ 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 992a1b29..33206f37 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -49,6 +49,13 @@ class BaseManager: cls._instance = cls() return cls._instance + def __del__(self): + self.destroy() + + def destroy(): + """Cleanup the VMS. Call this before closing the server""" + cls._instance() + @property def module_name(self): """ diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index cd6a7aa4..4f13ca04 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -32,7 +32,8 @@ class BaseVM: name=self.name, uuid=self.uuid)) - # TODO: When delete release console ports + def __del__(self): + self.destroy() @property def project(self): @@ -118,3 +119,10 @@ class BaseVM: """ raise NotImplementedError + + def destroy(self): + """ + Destroy the VM process. + """ + + raise NotImplementedError diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index d7ef3d3c..fa80e5e9 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -81,9 +81,6 @@ class VPCSVM(BaseVM): else: self._console = self._manager.port_manager.get_free_console_port() - def __del__(self): - self.destroy() - def destroy(self): self._kill_process() if self._console: diff --git a/gns3server/server.py b/gns3server/server.py index a776a71e..4161a917 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -77,7 +77,10 @@ class Server: Cleanup the modules (shutdown running emulators etc.) """ - # TODO: clean everything from here + for module in MODULES: + log.debug("Unloading module {}".format(module.__name__)) + m = module.instance() + m.destroy() self._loop.stop() def _signal_handling(self): @@ -152,8 +155,4 @@ class Server: # FIXME: remove it in production or in tests self._loop.call_later(1, self._reload_hook) - try: - self._loop.run_forever() - except KeyboardInterrupt: - log.info("\nExiting...") - self._cleanup() + self._loop.run_forever() From 6644c640db390c20c91119698f1d804b6fe62de4 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 22 Jan 2015 16:12:21 +0100 Subject: [PATCH 095/485] Attribute mac address --- gns3server/modules/base_manager.py | 7 ---- gns3server/modules/vpcs/__init__.py | 36 ++++++++++++++++ gns3server/modules/vpcs/vpcs_vm.py | 3 +- tests/conftest.py | 8 ++++ tests/modules/vpcs/test_vpcs_manager.py | 56 +++++++++++++++++++++++++ 5 files changed, 101 insertions(+), 9 deletions(-) create mode 100644 tests/modules/vpcs/test_vpcs_manager.py diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 33206f37..992a1b29 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -49,13 +49,6 @@ class BaseManager: cls._instance = cls() return cls._instance - def __del__(self): - self.destroy() - - def destroy(): - """Cleanup the VMS. Call this before closing the server""" - cls._instance() - @property def module_name(self): """ diff --git a/gns3server/modules/vpcs/__init__.py b/gns3server/modules/vpcs/__init__.py index 6ad5d8cc..dbabf8ca 100644 --- a/gns3server/modules/vpcs/__init__.py +++ b/gns3server/modules/vpcs/__init__.py @@ -19,9 +19,45 @@ VPCS server module. """ +import asyncio + from ..base_manager import BaseManager +from .vpcs_error import VPCSError from .vpcs_vm import VPCSVM class VPCS(BaseManager): _VM_CLASS = VPCSVM + + def __init__(self): + super().__init__() + self._free_mac_ids = list(range(0, 255)) + self._used_mac_ids = {} + + @asyncio.coroutine + def create_vm(self, *args, **kwargs): + + vm = yield from super().create_vm(*args, **kwargs) + try: + self._used_mac_ids[vm.uuid] = self._free_mac_ids.pop(0) + except IndexError: + raise VPCSError("No mac address available") + return vm + + @asyncio.coroutine + def delete_vm(self, uuid, *args, **kwargs): + + i = self._used_mac_ids[uuid] + self._free_mac_ids.insert(0, i) + del self._used_mac_ids[uuid] + yield from super().delete_vm(uuid, *args, **kwargs) + + def get_mac_id(self, vm_uuid): + """ + Get an unique VPCS mac id + + :param vm_uuid: UUID of the VPCS vm + :returns: VPCS Mac id + """ + + return self._used_mac_ids.get(vm_uuid, 1) diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index fa80e5e9..1e60b348 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -415,8 +415,7 @@ class VPCSVM(BaseVM): command.extend(["-e"]) command.extend(["-d", nio.tap_vm]) - # FIXME: find workaround - # command.extend(["-m", str(self._id)]) # the unique ID is used to set the MAC address offset + command.extend(["-m", str(self._manager.get_mac_id(self._uuid))]) # the unique ID is used to set the MAC address offset command.extend(["-i", "1"]) # option to start only one VPC instance command.extend(["-F"]) # option to avoid the daemonization of VPCS if self._script_file: diff --git a/tests/conftest.py b/tests/conftest.py index 572e3f00..537b1663 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -53,6 +53,8 @@ def _get_unused_port(): @pytest.fixture(scope="session") def server(request, loop, port_manager): + """A GNS 3 server""" + port = _get_unused_port() host = "localhost" app = web.Application() @@ -75,16 +77,22 @@ def server(request, loop, port_manager): @pytest.fixture(scope="module") def project(): + """A GNS3 lab""" + return ProjectManager.instance().create_project(uuid="a1e920ca-338a-4e9f-b363-aa607b09dd80") @pytest.fixture(scope="session") def port_manager(): + """An instance of port manager""" + return PortManager("127.0.0.1", False) @pytest.fixture(scope="function") def free_console_port(request, port_manager): + """Get a free TCP port""" + # In case of already use ports we will raise an exception port = port_manager.get_free_console_port() # We release the port immediately in order to allow diff --git a/tests/modules/vpcs/test_vpcs_manager.py b/tests/modules/vpcs/test_vpcs_manager.py new file mode 100644 index 00000000..a239c1c1 --- /dev/null +++ b/tests/modules/vpcs/test_vpcs_manager.py @@ -0,0 +1,56 @@ +# -*- 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 pytest +import asyncio +import uuid + + +from gns3server.modules.vpcs import VPCS +from gns3server.modules.vpcs.vpcs_error import VPCSError +from gns3server.modules.project_manager import ProjectManager + + +def test_get_mac_id(loop, project, port_manager): + # Cleanup the VPCS object + VPCS._instance = None + vpcs = VPCS.instance() + vpcs.port_manager = port_manager + vm1_uuid = str(uuid.uuid4()) + vm2_uuid = str(uuid.uuid4()) + vm3_uuid = str(uuid.uuid4()) + loop.run_until_complete(vpcs.create_vm("PC 1", project.uuid, vm1_uuid)) + loop.run_until_complete(vpcs.create_vm("PC 2", project.uuid, vm2_uuid)) + assert vpcs.get_mac_id(vm1_uuid) == 0 + assert vpcs.get_mac_id(vm1_uuid) == 0 + assert vpcs.get_mac_id(vm2_uuid) == 1 + loop.run_until_complete(vpcs.delete_vm(vm1_uuid)) + loop.run_until_complete(vpcs.create_vm("PC 3", project.uuid, vm3_uuid)) + assert vpcs.get_mac_id(vm3_uuid) == 0 + + +def test_get_mac_id_no_id_available(loop, project, port_manager): + # Cleanup the VPCS object + VPCS._instance = None + vpcs = VPCS.instance() + vpcs.port_manager = port_manager + with pytest.raises(VPCSError): + for i in range(0, 256): + vm_uuid = str(uuid.uuid4()) + loop.run_until_complete(vpcs.create_vm("PC {}".format(i), project.uuid, vm_uuid)) + assert vpcs.get_mac_id(vm_uuid) == i From 72c6182062fb0f458189b59f2515ba92f6ee868b Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 22 Jan 2015 17:04:14 +0100 Subject: [PATCH 096/485] Typo --- gns3server/handlers/vpcs_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index e97e24bc..83ffc479 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -94,7 +94,7 @@ class VPCSHandler: @Route.delete( r"/vpcs/{uuid}", status_codes={ - 204: "VPCS instance updated", + 204: "VPCS instance deleted", 404: "VPCS instance doesn't exist" }, description="Delete a VPCS instance") From 2c50bb607f7794431f35b9e0748d432f36bb71b2 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 22 Jan 2015 18:47:27 +0100 Subject: [PATCH 097/485] VPCS Mac address / project --- gns3server/modules/vpcs/__init__.py | 8 +++++--- tests/modules/vpcs/test_vpcs_manager.py | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/gns3server/modules/vpcs/__init__.py b/gns3server/modules/vpcs/__init__.py index dbabf8ca..046eff7f 100644 --- a/gns3server/modules/vpcs/__init__.py +++ b/gns3server/modules/vpcs/__init__.py @@ -31,15 +31,16 @@ class VPCS(BaseManager): def __init__(self): super().__init__() - self._free_mac_ids = list(range(0, 255)) + self._free_mac_ids = {} self._used_mac_ids = {} @asyncio.coroutine def create_vm(self, *args, **kwargs): vm = yield from super().create_vm(*args, **kwargs) + self._free_mac_ids.setdefault(vm.project.uuid, list(range(0, 255))) try: - self._used_mac_ids[vm.uuid] = self._free_mac_ids.pop(0) + self._used_mac_ids[vm.uuid] = self._free_mac_ids[vm.project.uuid].pop(0) except IndexError: raise VPCSError("No mac address available") return vm @@ -47,8 +48,9 @@ class VPCS(BaseManager): @asyncio.coroutine def delete_vm(self, uuid, *args, **kwargs): + vm = self.get_vm(uuid) i = self._used_mac_ids[uuid] - self._free_mac_ids.insert(0, i) + self._free_mac_ids[vm.project.uuid].insert(0, i) del self._used_mac_ids[uuid] yield from super().delete_vm(uuid, *args, **kwargs) diff --git a/tests/modules/vpcs/test_vpcs_manager.py b/tests/modules/vpcs/test_vpcs_manager.py index a239c1c1..7632ef4e 100644 --- a/tests/modules/vpcs/test_vpcs_manager.py +++ b/tests/modules/vpcs/test_vpcs_manager.py @@ -44,6 +44,24 @@ def test_get_mac_id(loop, project, port_manager): assert vpcs.get_mac_id(vm3_uuid) == 0 +def test_get_mac_id_multiple_project(loop, port_manager): + # Cleanup the VPCS object + VPCS._instance = None + vpcs = VPCS.instance() + vpcs.port_manager = port_manager + vm1_uuid = str(uuid.uuid4()) + vm2_uuid = str(uuid.uuid4()) + vm3_uuid = str(uuid.uuid4()) + project1 = ProjectManager.instance().create_project() + project2 = ProjectManager.instance().create_project() + loop.run_until_complete(vpcs.create_vm("PC 1", project1.uuid, vm1_uuid)) + loop.run_until_complete(vpcs.create_vm("PC 2", project1.uuid, vm2_uuid)) + loop.run_until_complete(vpcs.create_vm("PC 2", project2.uuid, vm3_uuid)) + assert vpcs.get_mac_id(vm1_uuid) == 0 + assert vpcs.get_mac_id(vm2_uuid) == 1 + assert vpcs.get_mac_id(vm3_uuid) == 0 + + def test_get_mac_id_no_id_available(loop, project, port_manager): # Cleanup the VPCS object VPCS._instance = None From 1fea7593ef5d3c7ce97549007ac0722555d9dcb3 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 22 Jan 2015 14:39:20 -0700 Subject: [PATCH 098/485] Update README --- README.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index a91baeed..5cea41ce 100644 --- a/README.rst +++ b/README.rst @@ -17,9 +17,10 @@ You must be connected to the Internet in order to install the dependencies. Dependencies: - Python 3.3 or above -- Setuptools -- Netifaces library -- Jsonschema +- aiohttp +- setuptools +- netifaces +- jsonschema The following commands will install some of these dependencies: From 6ec4dea9b9a96a8be6b9ebdde97af5983f8e9414 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 22 Jan 2015 15:04:44 -0700 Subject: [PATCH 099/485] Fixes reload call in VPCS handler. --- gns3server/handlers/vpcs_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index 83ffc479..78bbbd45 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -198,5 +198,5 @@ class VPCSHandler: vpcs_manager = VPCS.instance() vm = vpcs_manager.get_vm(request.match_info["uuid"]) - nio = vm.reload() + yield from vm.reload() response.set_status(204) From 2681defe27d520a20f548028ff9901a90a23462a Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 22 Jan 2015 18:04:24 -0700 Subject: [PATCH 100/485] Moves NIO creation to the base manager. --- gns3server/handlers/vpcs_handler.py | 4 +- gns3server/modules/attic.py | 102 ---------------------------- gns3server/modules/base_manager.py | 75 +++++++++++++++++++- gns3server/modules/nios/nio_tap.py | 3 +- gns3server/modules/nios/nio_udp.py | 5 +- gns3server/modules/vpcs/vpcs_vm.py | 42 ++++-------- tests/api/test_vpcs.py | 14 ++-- tests/modules/vpcs/test_vpcs_vm.py | 37 ++++++---- 8 files changed, 126 insertions(+), 156 deletions(-) delete mode 100644 gns3server/modules/attic.py diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index 78bbbd45..f457b8c7 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -159,7 +159,8 @@ class VPCSHandler: vpcs_manager = VPCS.instance() vm = vpcs_manager.get_vm(request.match_info["uuid"]) - nio = vm.port_add_nio_binding(int(request.match_info["port_id"]), request.json) + nio = vpcs_manager.create_nio(vm.vpcs_path, request.json) + vm.port_add_nio_binding(int(request.match_info["port_id"]), nio) response.set_status(201) response.json(nio) @@ -191,6 +192,7 @@ class VPCSHandler: }, status_codes={ 204: "VPCS reloaded", + 400: "Invalid VPCS instance UUID", 404: "VPCS instance doesn't exist" }, description="Remove a NIO from a VPCS") diff --git a/gns3server/modules/attic.py b/gns3server/modules/attic.py deleted file mode 100644 index 98e2413b..00000000 --- a/gns3server/modules/attic.py +++ /dev/null @@ -1,102 +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 . - -""" -Useful functions... in the attic ;) -""" - -import sys -import os -import struct -import socket -import stat -import time -import aiohttp - -import logging -log = logging.getLogger(__name__) - - -def wait_socket_is_ready(host, port, wait=2.0, socket_timeout=10): - """ - Waits for a socket to be ready for wait time. - - :param host: host/address to connect to - :param port: port to connect to - :param wait: maximum wait time - :param socket_timeout: timeout for the socket - - :returns: tuple with boolean indicating if the socket is ready and the last exception - that occurred when connecting to the socket - """ - - # connect to a local address by default - # if listening to all addresses (IPv4 or IPv6) - if host == "0.0.0.0": - host = "127.0.0.1" - elif host == "::": - host = "::1" - - connection_success = False - begin = time.time() - last_exception = None - while time.time() - begin < wait: - time.sleep(0.01) - try: - with socket.create_connection((host, port), socket_timeout): - pass - except OSError as e: - last_exception = e - continue - connection_success = True - break - - return connection_success, last_exception - - -def has_privileged_access(executable): - """ - Check if an executable can access Ethernet and TAP devices in - RAW mode. - - :param executable: executable path - - :returns: True or False - """ - - if sys.platform.startswith("win"): - # do not check anything on Windows - return True - - if os.geteuid() == 0: - # we are root, so we should have privileged access. - return True - if os.stat(executable).st_mode & stat.S_ISUID or os.stat(executable).st_mode & stat.S_ISGID: - # the executable has set UID bit. - return True - - # test if the executable has the CAP_NET_RAW capability (Linux only) - if sys.platform.startswith("linux") and "security.capability" in os.listxattr(executable): - try: - caps = os.getxattr(executable, "security.capability") - # test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set - if struct.unpack(". - +import sys +import os +import struct +import stat import asyncio import aiohttp +import socket + +import logging +log = logging.getLogger(__name__) from uuid import UUID, uuid4 from ..config import Config from .project_manager import ProjectManager +from .nios.nio_udp import NIO_UDP +from .nios.nio_tap import NIO_TAP + class BaseManager: @@ -147,3 +157,66 @@ class BaseManager: else: vm.destroy() del self._vms[vm.uuid] + + @staticmethod + def _has_privileged_access(executable): + """ + Check if an executable can access Ethernet and TAP devices in + RAW mode. + + :param executable: executable path + + :returns: True or False + """ + + if sys.platform.startswith("win"): + # do not check anything on Windows + return True + + if os.geteuid() == 0: + # we are root, so we should have privileged access. + return True + if os.stat(executable).st_mode & stat.S_ISUID or os.stat(executable).st_mode & stat.S_ISGID: + # the executable has set UID bit. + return True + + # test if the executable has the CAP_NET_RAW capability (Linux only) + if sys.platform.startswith("linux") and "security.capability" in os.listxattr(executable): + try: + caps = os.getxattr(executable, "security.capability") + # test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set + if struct.unpack(". import pytest +import aiohttp import asyncio import os from tests.utils import asyncio_patch @@ -49,7 +50,8 @@ def test_vm_invalid_vpcs_version(loop, project, manager): with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM._get_vpcs_welcome", return_value="Welcome to Virtual PC Simulator, version 0.1"): with pytest.raises(VPCSError): vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) - vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) + nio = manager.create_nio(vm.vpcs_path, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) + vm.port_add_nio_binding(0, nio) loop.run_until_complete(asyncio.async(vm.start())) assert vm.name == "test" assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0f" @@ -59,7 +61,8 @@ def test_vm_invalid_vpcs_version(loop, project, manager): def test_vm_invalid_vpcs_path(project, manager, loop): with pytest.raises(VPCSError): vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0e", project, manager) - vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) + nio = manager.create_nio(vm.vpcs_path, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) + vm.port_add_nio_binding(0, nio) loop.run_until_complete(asyncio.async(vm.start())) assert vm.name == "test" assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0e" @@ -68,8 +71,8 @@ def test_vm_invalid_vpcs_path(project, manager, loop): def test_start(loop, vm): with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM._check_requirements", return_value=True): with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): - nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) - + nio = VPCS.instance().create_nio(vm.vpcs_path, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) + vm.port_add_nio_binding(0, nio) loop.run_until_complete(asyncio.async(vm.start())) assert vm.is_running() @@ -78,8 +81,8 @@ def test_stop(loop, vm): process = MagicMock() with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM._check_requirements", return_value=True): with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): - nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) - + nio = VPCS.instance().create_nio(vm.vpcs_path, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) + vm.port_add_nio_binding(0, nio) loop.run_until_complete(asyncio.async(vm.start())) assert vm.is_running() loop.run_until_complete(asyncio.async(vm.stop())) @@ -91,8 +94,8 @@ def test_reload(loop, vm): process = MagicMock() with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM._check_requirements", return_value=True): with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): - nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) - + nio = VPCS.instance().create_nio(vm.vpcs_path, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) + vm.port_add_nio_binding(0, nio) loop.run_until_complete(asyncio.async(vm.start())) assert vm.is_running() loop.run_until_complete(asyncio.async(vm.reload())) @@ -101,25 +104,29 @@ def test_reload(loop, vm): def test_add_nio_binding_udp(vm): - nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) + nio = VPCS.instance().create_nio(vm.vpcs_path, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) + vm.port_add_nio_binding(0, nio) assert nio.lport == 4242 def test_add_nio_binding_tap(vm): - with patch("gns3server.modules.vpcs.vpcs_vm.has_privileged_access", return_value=True): - nio = vm.port_add_nio_binding(0, {"type": "nio_tap", "tap_device": "test"}) + with patch("gns3server.modules.base_manager.BaseManager._has_privileged_access", return_value=True): + nio = VPCS.instance().create_nio(vm.vpcs_path, {"type": "nio_tap", "tap_device": "test"}) + vm.port_add_nio_binding(0, nio) assert nio.tap_device == "test" def test_add_nio_binding_tap_no_privileged_access(vm): - with patch("gns3server.modules.vpcs.vpcs_vm.has_privileged_access", return_value=False): - with pytest.raises(VPCSError): - vm.port_add_nio_binding(0, {"type": "nio_tap", "tap_device": "test"}) + with patch("gns3server.modules.base_manager.BaseManager._has_privileged_access", return_value=False): + with pytest.raises(aiohttp.web.HTTPForbidden): + nio = VPCS.instance().create_nio(vm.vpcs_path, {"type": "nio_tap", "tap_device": "test"}) + vm.port_add_nio_binding(0, nio) assert vm._ethernet_adapter.ports[0] is None def test_port_remove_nio_binding(vm): - nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) + nio = VPCS.instance().create_nio(vm.vpcs_path, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) + vm.port_add_nio_binding(0, nio) vm.port_remove_nio_binding(0) assert vm._ethernet_adapter.ports[0] is None From d9b02efbfa90606d8f29855634e0f971417a73b1 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 22 Jan 2015 19:06:17 -0700 Subject: [PATCH 101/485] Rename destroy to close or unload (more friendly). --- gns3server/handlers/virtualbox_handler.py | 19 +++++++++++++++++++ gns3server/modules/base_manager.py | 10 +++++----- gns3server/modules/base_vm.py | 7 ++++--- gns3server/modules/vpcs/vpcs_vm.py | 3 ++- gns3server/server.py | 2 +- tests/conftest.py | 2 +- tests/modules/vpcs/test_vpcs_vm.py | 4 ++-- 7 files changed, 34 insertions(+), 13 deletions(-) diff --git a/gns3server/handlers/virtualbox_handler.py b/gns3server/handlers/virtualbox_handler.py index 8b7125a6..03a2e99e 100644 --- a/gns3server/handlers/virtualbox_handler.py +++ b/gns3server/handlers/virtualbox_handler.py @@ -125,3 +125,22 @@ class VirtualBoxHandler: vm = vbox_manager.get_vm(request.match_info["uuid"]) yield from vm.resume() response.set_status(204) + + @classmethod + @Route.post( + r"/virtualbox/{uuid}/reload", + parameters={ + "uuid": "VirtualBox VM instance UUID" + }, + status_codes={ + 204: "VirtualBox VM instance reloaded", + 400: "Invalid VirtualBox VM instance UUID", + 404: "VirtualBox VM instance doesn't exist" + }, + description="Reload a VirtualBox VM instance") + def suspend(request, response): + + vbox_manager = VirtualBox.instance() + vm = vbox_manager.get_vm(request.match_info["uuid"]) + yield from vm.reload() + response.set_status(204) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index d40c5c7a..5c5d9de4 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -95,9 +95,9 @@ class BaseManager: return self._config @classmethod - @asyncio.coroutine # FIXME: why coroutine? - def destroy(cls): + def unload(cls): + # TODO: close explicitly all the VMs here? cls._instance = None def get_vm(self, uuid): @@ -152,10 +152,10 @@ class BaseManager: """ vm = self.get_vm(uuid) - if asyncio.iscoroutinefunction(vm.destroy): - yield from vm.destroy() + if asyncio.iscoroutinefunction(vm.close): + yield from vm.close() else: - vm.destroy() + vm.close() del self._vms[vm.uuid] @staticmethod diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index 4f13ca04..51fd0461 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -33,7 +33,8 @@ class BaseVM: uuid=self.uuid)) def __del__(self): - self.destroy() + + self.close() @property def project(self): @@ -120,9 +121,9 @@ class BaseVM: raise NotImplementedError - def destroy(self): + def close(self): """ - Destroy the VM process. + Close the VM process. """ raise NotImplementedError diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index cdfabf70..00c20847 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -77,7 +77,8 @@ class VPCSVM(BaseVM): else: self._console = self._manager.port_manager.get_free_console_port() - def destroy(self): + def close(self): + self._kill_process() if self._console: self._manager.port_manager.release_console_port(self._console) diff --git a/gns3server/server.py b/gns3server/server.py index 4161a917..b8a847f2 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -80,7 +80,7 @@ class Server: for module in MODULES: log.debug("Unloading module {}".format(module.__name__)) m = module.instance() - m.destroy() + m.unload() self._loop.stop() def _signal_handling(self): diff --git a/tests/conftest.py b/tests/conftest.py index 537b1663..cbb0a758 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -68,7 +68,7 @@ def server(request, loop, port_manager): def tear_down(): for module in MODULES: - loop.run_until_complete(module.destroy()) + loop.run_until_complete(module.unload()) srv.close() srv.wait_closed() request.addfinalizer(tear_down) diff --git a/tests/modules/vpcs/test_vpcs_vm.py b/tests/modules/vpcs/test_vpcs_vm.py index 9c1d7a40..231bf0ae 100644 --- a/tests/modules/vpcs/test_vpcs_vm.py +++ b/tests/modules/vpcs/test_vpcs_vm.py @@ -186,12 +186,12 @@ def test_change_script_file(vm, tmpdir): assert vm.script_file == path -def test_destroy(vm, port_manager): +def test_close(vm, port_manager): with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM._check_requirements", return_value=True): with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): vm.start() port = vm.console - vm.destroy() + vm.close() # Raise an exception if the port is not free port_manager.reserve_console_port(port) assert vm.is_running() is False From 05c0efe39b66bb5856acf6196bbb105cab50d130 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 22 Jan 2015 19:07:09 -0700 Subject: [PATCH 102/485] More VirtualBox work. --- .../modules/virtualbox/virtualbox_vm.py | 89 +++++++++++-------- gns3server/schemas/virtualbox.py | 17 ++++ tests/api/test_virtualbox.py | 7 ++ .../modules/virtualbox/test_virtualbox_vm.py | 77 ++++++++++++++++ 4 files changed, 151 insertions(+), 39 deletions(-) create mode 100644 tests/modules/virtualbox/test_virtualbox_vm.py diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 6ef5c2d2..47c5d8e3 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -75,19 +75,17 @@ class VirtualBoxVM(BaseVM): def __json__(self): - # TODO: send more info - # {"name": self._name, - # "vmname": self._vmname, - # "adapters": self.adapters, + # TODO: send adapters info # "adapter_start_index": self._adapter_start_index, # "adapter_type": "Intel PRO/1000 MT Desktop (82540EM)", - # "console": self._console, - # "enable_remote_console": self._enable_remote_console, - # "headless": self._headless} return {"name": self.name, "uuid": self.uuid, - "project_uuid": self.project.uuid} + "project_uuid": self.project.uuid, + "vmname": self.vmname, + "linked_clone": self.linked_clone, + "headless": self.headless, + "enable_remote_console": self.enable_remote_console} @asyncio.coroutine def _execute(self, subcommand, args, timeout=60): @@ -167,8 +165,7 @@ class VirtualBoxVM(BaseVM): args = shlex.split(params) yield from self._execute("modifyvm", [self._vmname] + args) - @asyncio.coroutine - def create(self): + def _find_vboxmanage(self): # look for VBoxManage self._vboxmanage_path = self.manager.config.get_section_config("VirtualBox").get("vboxmanage_path") @@ -185,22 +182,27 @@ class VirtualBoxVM(BaseVM): if not self._vboxmanage_path: raise VirtualBoxError("Could not find VBoxManage") + if not os.path.isfile(self._vboxmanage_path): + raise VirtualBoxError("VBoxManage {} is not accessible".format(self._vboxmanage_path)) if not os.access(self._vboxmanage_path, os.X_OK): raise VirtualBoxError("VBoxManage is not executable") + @asyncio.coroutine + def create(self): + + self._find_vboxmanage() yield from self._get_system_properties() if parse_version(self._system_properties["API version"]) < parse_version("4_3"): raise VirtualBoxError("The VirtualBox API version is lower than 4.3") log.info("VirtualBox VM '{name}' [{uuid}] created".format(name=self.name, uuid=self.uuid)) if self._linked_clone: - # TODO: finish linked clone support if self.uuid and os.path.isdir(os.path.join(self.working_dir, self._vmname)): vbox_file = os.path.join(self.working_dir, self._vmname, self._vmname + ".vbox") - self._execute("registervm", [vbox_file]) - self._reattach_hdds() + yield from self._execute("registervm", [vbox_file]) + yield from self._reattach_hdds() else: - self._create_linked_clone() + yield from self._create_linked_clone() @asyncio.coroutine def start(self): @@ -323,14 +325,15 @@ class VirtualBoxVM(BaseVM): self._console = console self._allocated_console_ports.append(self._console) - log.info("VirtualBox VM {name} [id={id}]: console port set to {port}".format(name=self._name, - id=self._id, - port=console)) + log.info("VirtualBox VM '{name}' [{uuid}]: console port set to {port}".format(name=self.name, + uuid=self.uuid, + port=console)) + @asyncio.coroutine def _get_all_hdd_files(self): hdds = [] - properties = self._execute("list", ["hdds"]) + properties = yield from self._execute("list", ["hdds"]) for prop in properties: try: name, value = prop.split(':', 1) @@ -340,33 +343,32 @@ class VirtualBoxVM(BaseVM): hdds.append(value.strip()) return hdds + @asyncio.coroutine def _reattach_hdds(self): - hdd_info_file = os.path.join(self._working_dir, self._vmname, "hdd_info.json") + hdd_info_file = os.path.join(self.working_dir, self._vmname, "hdd_info.json") try: with open(hdd_info_file, "r") as f: - # log.info("loading project: {}".format(path)) hdd_table = json.load(f) except OSError as e: raise VirtualBoxError("Could not read HDD info file: {}".format(e)) for hdd_info in hdd_table: - hdd_file = os.path.join(self._working_dir, self._vmname, "Snapshots", hdd_info["hdd"]) + hdd_file = os.path.join(self.working_dir, self._vmname, "Snapshots", hdd_info["hdd"]) if os.path.exists(hdd_file): log.debug("reattaching hdd {}".format(hdd_file)) - self._storage_attach('--storagectl "{}" --port {} --device {} --type hdd --medium "{}"'.format(hdd_info["controller"], - hdd_info["port"], - hdd_info["device"], - hdd_file)) + yield from self._storage_attach('--storagectl "{}" --port {} --device {} --type hdd --medium "{}"'.format(hdd_info["controller"], + hdd_info["port"], + hdd_info["device"], + hdd_file)) - def delete(self): + @asyncio.coroutine + def close(self): """ - Deletes this VirtualBox VM. + Closes this VirtualBox VM. """ self.stop() - if self._id in self._instances: - self._instances.remove(self._id) if self.console and self.console in self._allocated_console_ports: self._allocated_console_ports.remove(self.console) @@ -374,7 +376,7 @@ class VirtualBoxVM(BaseVM): if self._linked_clone: hdd_table = [] if os.path.exists(self._working_dir): - hdd_files = self._get_all_hdd_files() + hdd_files = yield from self._get_all_hdd_files() vm_info = self._get_vm_info() for entry, value in vm_info.items(): match = re.search("^([\s\w]+)\-(\d)\-(\d)$", entry) @@ -383,7 +385,7 @@ class VirtualBoxVM(BaseVM): port = match.group(2) device = match.group(3) if value in hdd_files: - self._storage_attach('--storagectl "{}" --port {} --device {} --type hdd --medium none'.format(controller, port, device)) + yield from self._storage_attach('--storagectl "{}" --port {} --device {} --type hdd --medium none'.format(controller, port, device)) hdd_table.append( { "hdd": os.path.basename(value), @@ -404,17 +406,15 @@ class VirtualBoxVM(BaseVM): except OSError as e: raise VirtualBoxError("Could not write HDD info file: {}".format(e)) - log.info("VirtualBox VM {name} [id={id}] has been deleted".format(name=self._name, - id=self._id)) + log.info("VirtualBox VM '{name}' [{uuid}] closed".format(name=self.name, + uuid=self.uuid)) - def clean_delete(self): + def delete(self): """ Deletes this VirtualBox VM & all files. """ self.stop() - if self._id in self._instances: - self._instances.remove(self._id) if self.console: self._allocated_console_ports.remove(self.console) @@ -506,6 +506,16 @@ class VirtualBoxVM(BaseVM): self._modify_vm('--name "{}"'.format(vmname)) self._vmname = vmname + @property + def linked_clone(self): + """ + Returns either the VM is a linked clone. + + :returns: boolean + """ + + return self._linked_clone + @property def adapters(self): """ @@ -651,6 +661,7 @@ class VirtualBoxVM(BaseVM): args = [self._vmname, "--uartmode1", "server", pipe_name] yield from self._execute("modifyvm", args) + @asyncio.coroutine def _storage_attach(self, params): """ Change storage medium in this VM. @@ -659,7 +670,7 @@ class VirtualBoxVM(BaseVM): """ args = shlex.split(params) - self._execute("storageattach", [self._vmname] + args) + yield from self._execute("storageattach", [self._vmname] + args) @asyncio.coroutine def _get_nic_attachements(self, maximum_adapters): @@ -760,9 +771,9 @@ class VirtualBoxVM(BaseVM): "--options", "link", "--name", - self._name, + self.name, "--basefolder", - self._working_dir, + self.working_dir, "--register"] result = yield from self._execute("clonevm", args) diff --git a/gns3server/schemas/virtualbox.py b/gns3server/schemas/virtualbox.py index d272efe1..760516f6 100644 --- a/gns3server/schemas/virtualbox.py +++ b/gns3server/schemas/virtualbox.py @@ -82,6 +82,23 @@ VBOX_OBJECT_SCHEMA = { "maxLength": 36, "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" }, + "vmname": { + "description": "VirtualBox VM name (in VirtualBox itself)", + "type": "string", + "minLength": 1, + }, + "linked_clone": { + "description": "either the VM is a linked clone or not", + "type": "boolean" + }, + "enable_remote_console": { + "description": "enable the remote console", + "type": "boolean" + }, + "headless": { + "description": "headless mode", + "type": "boolean" + }, "console": { "description": "console TCP port", "minimum": 1, diff --git a/tests/api/test_virtualbox.py b/tests/api/test_virtualbox.py index 05dbfc0e..5b148b3d 100644 --- a/tests/api/test_virtualbox.py +++ b/tests/api/test_virtualbox.py @@ -70,3 +70,10 @@ def test_vbox_resume(server, vm): response = server.post("/virtualbox/{}/resume".format(vm["uuid"])) assert mock.called assert response.status == 204 + + +def test_vbox_reload(server, vm): + with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.reload", return_value=True) as mock: + response = server.post("/virtualbox/{}/reload".format(vm["uuid"])) + assert mock.called + assert response.status == 204 diff --git a/tests/modules/virtualbox/test_virtualbox_vm.py b/tests/modules/virtualbox/test_virtualbox_vm.py new file mode 100644 index 00000000..a81e8cf3 --- /dev/null +++ b/tests/modules/virtualbox/test_virtualbox_vm.py @@ -0,0 +1,77 @@ +# -*- 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 pytest +import asyncio +import tempfile +from tests.utils import asyncio_patch + +from unittest.mock import patch, MagicMock +from gns3server.modules.virtualbox.virtualbox_vm import VirtualBoxVM +from gns3server.modules.virtualbox.virtualbox_error import VirtualBoxError +from gns3server.modules.virtualbox import VirtualBox + + +@pytest.fixture(scope="module") +def manager(port_manager): + m = VirtualBox.instance() + m.port_manager = port_manager + return m + + +@pytest.fixture(scope="function") +def vm(project, manager): + return VirtualBoxVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager, "test", False) + + +def test_vm(project, manager): + vm = VirtualBoxVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager, "test", False) + assert vm.name == "test" + assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0f" + assert vm.vmname == "test" + assert vm.linked_clone is False + + +@patch("gns3server.config.Config.get_section_config", return_value={"vboxmanage_path": "/bin/test_fake"}) +def test_vm_invalid_vboxmanage_path(project, manager): + with pytest.raises(VirtualBoxError): + vm = VirtualBoxVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0e", project, manager, "test", False) + vm._find_vboxmanage() + + +tmpfile = tempfile.NamedTemporaryFile() +@patch("gns3server.config.Config.get_section_config", return_value={"vboxmanage_path": tmpfile.name}) +def test_vm_non_executable_vboxmanage_path(project, manager, loop): + with pytest.raises(VirtualBoxError): + vm = VirtualBoxVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0e", project, manager, "test", False) + vm._find_vboxmanage() + + +def test_vm_valid_virtualbox_api_version(loop, project, manager): + with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM._execute", return_value=["API version: 4_3"]): + vm = VirtualBoxVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager, "test", False) + loop.run_until_complete(asyncio.async(vm.create())) + + +def test_vm_invalid_virtualbox_api_version(loop, project, manager): + with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM._execute", return_value=["API version: 4_2"]): + with pytest.raises(VirtualBoxError): + vm = VirtualBoxVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager, "test", False) + loop.run_until_complete(asyncio.async(vm.create())) + + +# TODO: find a way to test start, stop, suspend, resume and reload From 2a8823b856edca5068438aea99431c69bcc8ddff Mon Sep 17 00:00:00 2001 From: grossmj Date: Thu, 22 Jan 2015 21:11:57 -0700 Subject: [PATCH 103/485] Use the Proactor event loop on Windows. --- gns3server/server.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gns3server/server.py b/gns3server/server.py index b8a847f2..1fdeb876 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -138,6 +138,10 @@ class Server: logger = logging.getLogger("asyncio") logger.setLevel(logging.WARNING) + if sys.platform.startswith("win"): + # use the Proactor event loop on Windows + asyncio.set_event_loop(asyncio.ProactorEventLoop()) + # TODO: SSL support for Rackspace cloud integration (here or with nginx for instance). self._loop = asyncio.get_event_loop() app = aiohttp.web.Application() From e61e976368b44d624de2e0902d24ae2e5d82319e Mon Sep 17 00:00:00 2001 From: grossmj Date: Thu, 22 Jan 2015 21:31:26 -0700 Subject: [PATCH 104/485] Adapters support for VirtualBox. --- gns3server/modules/base_manager.py | 1 + .../modules/virtualbox/virtualbox_vm.py | 23 +++++++++---------- gns3server/schemas/virtualbox.py | 17 ++++++++++++++ 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 5c5d9de4..f4186b2b 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -95,6 +95,7 @@ class BaseManager: return self._config @classmethod + @asyncio.coroutine def unload(cls): # TODO: close explicitly all the VMs here? diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 47c5d8e3..2bbe91c0 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -33,7 +33,7 @@ import shutil from pkg_resources import parse_version from .virtualbox_error import VirtualBoxError from ..adapters.ethernet_adapter import EthernetAdapter -from .telnet_server import TelnetServer +from .telnet_server import TelnetServer # port TelnetServer to asyncio from ..base_vm import BaseVM if sys.platform.startswith('win'): @@ -70,22 +70,18 @@ class VirtualBoxVM(BaseVM): self._adapter_start_index = 0 self._adapter_type = "Intel PRO/1000 MT Desktop (82540EM)" - # TODO: finish adapters support - # self.adapters = 2 # creates 2 adapters by default - def __json__(self): - # TODO: send adapters info - # "adapter_start_index": self._adapter_start_index, - # "adapter_type": "Intel PRO/1000 MT Desktop (82540EM)", - return {"name": self.name, "uuid": self.uuid, "project_uuid": self.project.uuid, "vmname": self.vmname, "linked_clone": self.linked_clone, "headless": self.headless, - "enable_remote_console": self.enable_remote_console} + "enable_remote_console": self.enable_remote_console, + "adapters": self.adapters, + "adapter_type": self.adapter_type, + "adapter_start_index": self.adapter_start_index} @asyncio.coroutine def _execute(self, subcommand, args, timeout=60): @@ -204,6 +200,9 @@ class VirtualBoxVM(BaseVM): else: yield from self._create_linked_clone() + # set 2 adapters by default + # yield from self.set_adapters(2) + @asyncio.coroutine def start(self): """ @@ -526,8 +525,8 @@ class VirtualBoxVM(BaseVM): return len(self._ethernet_adapters) - @adapters.setter - def adapters(self, adapters): + @asyncio.coroutine + def set_adapters(self, adapters): """ Sets the number of Ethernet adapters for this VirtualBox VM instance. @@ -535,7 +534,7 @@ class VirtualBoxVM(BaseVM): """ # check for the maximum adapters supported by the VM - self._maximum_adapters = self._get_maximum_supported_adapters() + self._maximum_adapters = yield from self._get_maximum_supported_adapters() if len(self._ethernet_adapters) > self._maximum_adapters: raise VirtualBoxError("Number of adapters above the maximum supported of {}".format(self._maximum_adapters)) diff --git a/gns3server/schemas/virtualbox.py b/gns3server/schemas/virtualbox.py index 760516f6..3beb7262 100644 --- a/gns3server/schemas/virtualbox.py +++ b/gns3server/schemas/virtualbox.py @@ -99,6 +99,23 @@ VBOX_OBJECT_SCHEMA = { "description": "headless mode", "type": "boolean" }, + "adapters": { + "description": "number of adapters", + "type": "integer", + "minimum": 0, + "maximum": 36, # maximum given by the ICH9 chipset in VirtualBox + }, + "adapter_start_index": { + "description": "adapter index from which to start using adapters", + "type": "integer", + "minimum": 0, + "maximum": 35, # maximum given by the ICH9 chipset in VirtualBox + }, + "adapter_type": { + "description": "VirtualBox adapter type", + "type": "string", + "minLength": 1, + }, "console": { "description": "console TCP port", "minimum": 1, From 0d503b779e015ec3b6e763abd225cc8bcda2bd3f Mon Sep 17 00:00:00 2001 From: grossmj Date: Thu, 22 Jan 2015 23:40:51 -0700 Subject: [PATCH 105/485] Explicitly close VM when the server is shutdown. --- gns3server/modules/base_manager.py | 15 +++++++++++---- tests/conftest.py | 3 ++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index f4186b2b..9710ace8 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -94,12 +94,17 @@ class BaseManager: return self._config - @classmethod @asyncio.coroutine - def unload(cls): + def unload(self): + + for uuid in self._vms.keys(): + try: + self.delete_vm(uuid) + except Exception as e: + log.warn("Could not delete VM {}: {}".format(uuid, e)) - # TODO: close explicitly all the VMs here? - cls._instance = None + if hasattr(BaseManager, "_instance"): + BaseManager._instance = None def get_vm(self, uuid): """ @@ -144,6 +149,8 @@ class BaseManager: self._vms[vm.uuid] = vm return vm + # FIXME: should be named close_vm and we should have a + # delete_vm when a user deletes a VM (including files in workdir) @asyncio.coroutine def delete_vm(self, uuid): """ diff --git a/tests/conftest.py b/tests/conftest.py index cbb0a758..1a80a456 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -68,7 +68,8 @@ def server(request, loop, port_manager): def tear_down(): for module in MODULES: - loop.run_until_complete(module.unload()) + instance = module.instance() + loop.run_until_complete(instance.unload()) srv.close() srv.wait_closed() request.addfinalizer(tear_down) From 28308b10bca41b93574ed321a5c2f9e4fcf0b039 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 23 Jan 2015 10:11:40 +0100 Subject: [PATCH 106/485] Add missing documentation --- gns3server/handlers/vpcs_handler.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index f457b8c7..64da6047 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -72,6 +72,9 @@ class VPCSHandler: @classmethod @Route.put( r"/vpcs/{uuid}", + parameters={ + "uuid": "VPCS instance UUID" + }, status_codes={ 200: "VPCS instance updated", 404: "VPCS instance doesn't exist", @@ -93,6 +96,9 @@ class VPCSHandler: @classmethod @Route.delete( r"/vpcs/{uuid}", + parameters={ + "uuid": "VPCS instance UUID" + }, status_codes={ 204: "VPCS instance deleted", 404: "VPCS instance doesn't exist" From f97c2b2cbe94631175e7afe127d8fd262d0dd606 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 23 Jan 2015 11:28:58 +0100 Subject: [PATCH 107/485] Delete a VM, mark it as destroyable --- gns3server/handlers/project_handler.py | 19 +++++++++++++ gns3server/modules/base_manager.py | 20 ++++++++++++-- gns3server/modules/base_vm.py | 2 +- gns3server/modules/project.py | 25 ++++++++++++++--- tests/api/test_project.py | 12 ++++++++ tests/modules/test_project.py | 38 ++++++++++++++++++++++++-- 6 files changed, 105 insertions(+), 11 deletions(-) diff --git a/gns3server/handlers/project_handler.py b/gns3server/handlers/project_handler.py index 79d1b68e..caf3524f 100644 --- a/gns3server/handlers/project_handler.py +++ b/gns3server/handlers/project_handler.py @@ -30,9 +30,28 @@ class ProjectHandler: output=PROJECT_OBJECT_SCHEMA, input=PROJECT_OBJECT_SCHEMA) def create_project(request, response): + pm = ProjectManager.instance() p = pm.create_project( location=request.json.get("location"), uuid=request.json.get("uuid") ) response.json(p) + + @classmethod + @Route.post( + r"/project/{uuid}/commit", + description="Write changes on disk", + parameters={ + "uuid": "Project instance UUID", + }, + status_codes={ + 204: "Changes write on disk", + 404: "Project instance doesn't exist" + }) + def create_project(request, response): + + pm = ProjectManager.instance() + project = pm.get_project(request.match_info["uuid"]) + project.commit() + response.set_status(204) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 9710ace8..641ed790 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -149,14 +149,13 @@ class BaseManager: self._vms[vm.uuid] = vm return vm - # FIXME: should be named close_vm and we should have a - # delete_vm when a user deletes a VM (including files in workdir) @asyncio.coroutine - def delete_vm(self, uuid): + def close_vm(self, uuid): """ Delete a VM :param uuid: VM UUID + :returns: VM instance """ vm = self.get_vm(uuid) @@ -164,7 +163,22 @@ class BaseManager: yield from vm.close() else: vm.close() + return vm + + @asyncio.coroutine + def delete_vm(self, uuid): + """ + Delete a VM. VM working directory will be destroy when + we receive a commit. + + :param uuid: VM UUID + :returns: VM instance + """ + + vm = yield from self.close_vm(uuid) + vm.project.mark_vm_for_destruction(vm) del self._vms[vm.uuid] + return vm @staticmethod def _has_privileged_access(executable): diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index 51fd0461..52c003b2 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -96,7 +96,7 @@ class BaseVM: Return VM working directory """ - return self._project.vm_working_directory(self.manager.module_name.lower(), self._uuid) + return self._project.vm_working_directory(self) def create(self): """ diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index 9e83ffae..00159fbf 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -18,6 +18,7 @@ import aiohttp import os import tempfile +import shutil from uuid import UUID, uuid4 @@ -45,6 +46,7 @@ class Project: if location is None: self._location = tempfile.mkdtemp() + self._vms_to_destroy = set() self._path = os.path.join(self._location, self._uuid) try: os.makedirs(os.path.join(self._path, "vms"), exist_ok=True) @@ -66,25 +68,40 @@ class Project: return self._path - def vm_working_directory(self, module, vm_uuid): + def vm_working_directory(self, vm): """ Return a working directory for a specific VM. If the directory doesn't exist, the directory is created. - :param module: The module name (vpcs, dynamips...) - :param vm_uuid: VM UUID + :param vm: An instance of VM + :returns: A string with a VM working directory """ - workdir = os.path.join(self._path, module, vm_uuid) + workdir = os.path.join(self._path, vm.manager.module_name.lower(), vm.uuid) try: os.makedirs(workdir, exist_ok=True) except OSError as e: raise aiohttp.web.HTTPInternalServerError(text="Could not create VM working directory: {}".format(e)) return workdir + def mark_vm_for_destruction(self, vm): + """ + :param vm: An instance of VM + """ + + self._vms_to_destroy.add(vm) + def __json__(self): return { "uuid": self._uuid, "location": self._location } + + def commit(self): + """Write project changes on disk""" + while self._vms_to_destroy: + vm = self._vms_to_destroy.pop() + directory = self.vm_working_directory(vm) + if os.path.exists(directory): + shutil.rmtree(directory) diff --git a/tests/api/test_project.py b/tests/api/test_project.py index 1c42bd63..d8312cb0 100644 --- a/tests/api/test_project.py +++ b/tests/api/test_project.py @@ -19,6 +19,8 @@ This test suite check /project endpoint """ +import uuid + def test_create_project_with_dir(server, tmpdir): response = server.post("/project", {"location": str(tmpdir)}) @@ -46,3 +48,13 @@ def test_create_project_with_uuid(server): assert response.status == 200 assert response.json["uuid"] == "00010203-0405-0607-0809-0a0b0c0d0e0f" assert response.json["location"] == "/tmp" + + +def test_commit_project(server, project): + response = server.post("/project/{uuid}/commit".format(uuid=project.uuid)) + assert response.status == 204 + + +def test_commit_project_invalid_project_uuid(server, project): + response = server.post("/project/{uuid}/commit".format(uuid=uuid.uuid4())) + assert response.status == 404 diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py index d641a37e..fa269d28 100644 --- a/tests/modules/test_project.py +++ b/tests/modules/test_project.py @@ -17,7 +17,21 @@ # along with this program. If not, see . import os +import pytest from gns3server.modules.project import Project +from gns3server.modules.vpcs import VPCS, VPCSVM + + +@pytest.fixture(scope="module") +def manager(port_manager): + m = VPCS.instance() + m.port_manager = port_manager + return m + + +@pytest.fixture(scope="function") +def vm(project, manager): + return VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) def test_affect_uuid(): @@ -45,7 +59,25 @@ def test_json(tmpdir): assert p.__json__() == {"location": p.location, "uuid": p.uuid} -def test_vm_working_directory(tmpdir): +def test_vm_working_directory(tmpdir, vm): p = Project(location=str(tmpdir)) - assert os.path.exists(p.vm_working_directory('vpcs', '00010203-0405-0607-0809-0a0b0c0d0e0f')) - assert os.path.exists(os.path.join(str(tmpdir), p.uuid, 'vpcs', '00010203-0405-0607-0809-0a0b0c0d0e0f')) + assert os.path.exists(p.vm_working_directory(vm)) + assert os.path.exists(os.path.join(str(tmpdir), p.uuid, vm.module_name, vm.uuid)) + + +def test_mark_vm_for_destruction(tmpdir, vm): + p = Project(location=str(tmpdir)) + p.mark_vm_for_destruction(vm) + assert len(p._vms_to_destroy) == 1 + + +def test_commit(tmpdir, manager): + project = Project(location=str(tmpdir)) + vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + directory = project.vm_working_directory(vm) + project.mark_vm_for_destruction(vm) + assert len(project._vms_to_destroy) == 1 + assert os.path.exists(directory) + project.commit() + assert len(project._vms_to_destroy) == 0 + assert os.path.exists(directory) is False From 9a0b260c56e59f5a7241892dc7264ed7ae4adc2a Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 23 Jan 2015 11:30:49 +0100 Subject: [PATCH 108/485] Small change in order to avoid a PEP8 warning --- tests/modules/virtualbox/test_virtualbox_vm.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/modules/virtualbox/test_virtualbox_vm.py b/tests/modules/virtualbox/test_virtualbox_vm.py index a81e8cf3..6a80d77e 100644 --- a/tests/modules/virtualbox/test_virtualbox_vm.py +++ b/tests/modules/virtualbox/test_virtualbox_vm.py @@ -53,12 +53,12 @@ def test_vm_invalid_vboxmanage_path(project, manager): vm._find_vboxmanage() -tmpfile = tempfile.NamedTemporaryFile() -@patch("gns3server.config.Config.get_section_config", return_value={"vboxmanage_path": tmpfile.name}) def test_vm_non_executable_vboxmanage_path(project, manager, loop): - with pytest.raises(VirtualBoxError): - vm = VirtualBoxVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0e", project, manager, "test", False) - vm._find_vboxmanage() + tmpfile = tempfile.NamedTemporaryFile() + with patch("gns3server.config.Config.get_section_config", return_value={"vboxmanage_path": tmpfile.name}): + with pytest.raises(VirtualBoxError): + vm = VirtualBoxVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0e", project, manager, "test", False) + vm._find_vboxmanage() def test_vm_valid_virtualbox_api_version(loop, project, manager): From 986a7f55ef0a9f9410ba82daedc23561b3689005 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 23 Jan 2015 11:48:20 +0100 Subject: [PATCH 109/485] Delete a project --- gns3server/handlers/project_handler.py | 20 +++++++++++++++++++- gns3server/modules/project.py | 13 +++++++++++++ tests/api/test_project.py | 15 ++++++++++++++- tests/modules/test_project.py | 8 ++++++++ 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/gns3server/handlers/project_handler.py b/gns3server/handlers/project_handler.py index caf3524f..74c150f1 100644 --- a/gns3server/handlers/project_handler.py +++ b/gns3server/handlers/project_handler.py @@ -49,9 +49,27 @@ class ProjectHandler: 204: "Changes write on disk", 404: "Project instance doesn't exist" }) - def create_project(request, response): + def commit(request, response): pm = ProjectManager.instance() project = pm.get_project(request.match_info["uuid"]) project.commit() response.set_status(204) + + @classmethod + @Route.delete( + r"/project/{uuid}", + description="Delete a project from disk", + parameters={ + "uuid": "Project instance UUID", + }, + status_codes={ + 204: "Changes write on disk", + 404: "Project instance doesn't exist" + }) + def delete(request, response): + + pm = ProjectManager.instance() + project = pm.get_project(request.match_info["uuid"]) + project.delete() + response.set_status(204) diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index 00159fbf..adbb4eba 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -98,10 +98,23 @@ class Project: "location": self._location } + def close(self): + """Close the project, but keep informations on disk""" + + pass + def commit(self): """Write project changes on disk""" + while self._vms_to_destroy: vm = self._vms_to_destroy.pop() directory = self.vm_working_directory(vm) if os.path.exists(directory): shutil.rmtree(directory) + + def delete(self): + """Remove project from disk""" + + self.close() + if os.path.exists(self.path): + shutil.rmtree(self.path) diff --git a/tests/api/test_project.py b/tests/api/test_project.py index d8312cb0..87b8cdfb 100644 --- a/tests/api/test_project.py +++ b/tests/api/test_project.py @@ -51,10 +51,23 @@ def test_create_project_with_uuid(server): def test_commit_project(server, project): - response = server.post("/project/{uuid}/commit".format(uuid=project.uuid)) + response = server.post("/project/{uuid}/commit".format(uuid=project.uuid), example=True) assert response.status == 204 def test_commit_project_invalid_project_uuid(server, project): response = server.post("/project/{uuid}/commit".format(uuid=uuid.uuid4())) assert response.status == 404 + + +def test_delete_project(server): + query = {"uuid": "00010203-0405-0607-0809-0a0b0c0d0e0f"} + response = server.post("/project", query) + assert response.status == 200 + response = server.delete("/project/00010203-0405-0607-0809-0a0b0c0d0e0f") + assert response.status == 204 + + +def test_delete_project_invalid_uuid(server, project): + response = server.delete("/project/{uuid}".format(uuid=uuid.uuid4())) + assert response.status == 404 diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py index fa269d28..d6aa2509 100644 --- a/tests/modules/test_project.py +++ b/tests/modules/test_project.py @@ -81,3 +81,11 @@ def test_commit(tmpdir, manager): project.commit() assert len(project._vms_to_destroy) == 0 assert os.path.exists(directory) is False + + +def test_project_delete(tmpdir): + project = Project(location=str(tmpdir)) + directory = project.path + assert os.path.exists(directory) + project.delete() + assert os.path.exists(directory) is False From 3f5c2390cd7ecc6daa6647f78382012350d7e28a Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 23 Jan 2015 14:07:10 +0100 Subject: [PATCH 110/485] Close a project --- gns3server/handlers/project_handler.py | 18 +++++++++++++++++ gns3server/modules/base_vm.py | 1 + gns3server/modules/project.py | 19 ++++++++++++++++- tests/api/test_project.py | 28 +++++++++++++++++++------- tests/modules/test_project.py | 18 +++++++++++++++++ 5 files changed, 76 insertions(+), 8 deletions(-) diff --git a/gns3server/handlers/project_handler.py b/gns3server/handlers/project_handler.py index 74c150f1..e6fdbc59 100644 --- a/gns3server/handlers/project_handler.py +++ b/gns3server/handlers/project_handler.py @@ -56,6 +56,24 @@ class ProjectHandler: project.commit() response.set_status(204) + @classmethod + @Route.post( + r"/project/{uuid}/close", + description="Close project", + parameters={ + "uuid": "Project instance UUID", + }, + status_codes={ + 204: "Project closed", + 404: "Project instance doesn't exist" + }) + def close(request, response): + + pm = ProjectManager.instance() + project = pm.get_project(request.match_info["uuid"]) + project.close() + response.set_status(204) + @classmethod @Route.delete( r"/project/{uuid}", diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index 52c003b2..42deb98f 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -27,6 +27,7 @@ class BaseVM: self._uuid = uuid self._project = project self._manager = manager + project.add_vm(self) log.debug("{module}: {name} [{uuid}] initialized".format(module=self.manager.module_name, name=self.name, diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index adbb4eba..a3e0319f 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -46,6 +46,7 @@ class Project: if location is None: self._location = tempfile.mkdtemp() + self._vms = set() self._vms_to_destroy = set() self._path = os.path.join(self._location, self._uuid) try: @@ -68,6 +69,11 @@ class Project: return self._path + @property + def vms(self): + + return self._vms + def vm_working_directory(self, vm): """ Return a working directory for a specific VM. @@ -98,10 +104,21 @@ class Project: "location": self._location } + def add_vm(self, vm): + """ + Add a VM to the project. In theory this should be called by + the VM initializer. + + :params vm: A VM instance + """ + + self._vms.add(vm) + def close(self): """Close the project, but keep informations on disk""" - pass + for vm in self._vms: + vm.close() def commit(self): """Write project changes on disk""" diff --git a/tests/api/test_project.py b/tests/api/test_project.py index 87b8cdfb..e343b735 100644 --- a/tests/api/test_project.py +++ b/tests/api/test_project.py @@ -20,6 +20,7 @@ This test suite check /project endpoint """ import uuid +from tests.utils import asyncio_patch def test_create_project_with_dir(server, tmpdir): @@ -51,8 +52,10 @@ def test_create_project_with_uuid(server): def test_commit_project(server, project): - response = server.post("/project/{uuid}/commit".format(uuid=project.uuid), example=True) + with asyncio_patch("gns3server.modules.project.Project.commit", return_value=True) as mock: + response = server.post("/project/{uuid}/commit".format(uuid=project.uuid), example=True) assert response.status == 204 + assert mock.called def test_commit_project_invalid_project_uuid(server, project): @@ -60,14 +63,25 @@ def test_commit_project_invalid_project_uuid(server, project): assert response.status == 404 -def test_delete_project(server): - query = {"uuid": "00010203-0405-0607-0809-0a0b0c0d0e0f"} - response = server.post("/project", query) - assert response.status == 200 - response = server.delete("/project/00010203-0405-0607-0809-0a0b0c0d0e0f") - assert response.status == 204 +def test_delete_project(server, project): + with asyncio_patch("gns3server.modules.project.Project.delete", return_value=True) as mock: + response = server.delete("/project/{uuid}".format(uuid=project.uuid), example=True) + assert response.status == 204 + assert mock.called def test_delete_project_invalid_uuid(server, project): response = server.delete("/project/{uuid}".format(uuid=uuid.uuid4())) assert response.status == 404 + + +def test_close_project(server, project): + with asyncio_patch("gns3server.modules.project.Project.close", return_value=True) as mock: + response = server.post("/project/{uuid}/close".format(uuid=project.uuid), example=True) + assert response.status == 204 + assert mock.called + + +def test_close_project_invalid_uuid(server, project): + response = server.post("/project/{uuid}/close".format(uuid=uuid.uuid4())) + assert response.status == 404 diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py index d6aa2509..2956adaf 100644 --- a/tests/modules/test_project.py +++ b/tests/modules/test_project.py @@ -18,6 +18,8 @@ import os import pytest +from unittest.mock import patch + from gns3server.modules.project import Project from gns3server.modules.vpcs import VPCS, VPCSVM @@ -89,3 +91,19 @@ def test_project_delete(tmpdir): assert os.path.exists(directory) project.delete() assert os.path.exists(directory) is False + + +def test_project_add_vm(tmpdir, manager): + project = Project(location=str(tmpdir)) + # The VM initalizer call the add_vm method + vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + assert len(project.vms) == 1 + + +def test_project_close(tmpdir, manager): + project = Project(location=str(tmpdir)) + vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + project.add_vm(vm) + with patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM.close") as mock: + project.close() + assert mock.called From 7bf121c6da5b1b401d91b3e444e001caad88b0f4 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 23 Jan 2015 14:34:50 +0100 Subject: [PATCH 111/485] When we remove a VM, the VM is removed from the project. --- gns3server/modules/base_manager.py | 1 + gns3server/modules/base_vm.py | 1 - gns3server/modules/project.py | 17 +++++++++++++++-- tests/modules/test_project.py | 12 ++++++++---- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 641ed790..aef1579a 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -142,6 +142,7 @@ class BaseManager: uuid = str(uuid4()) vm = self._VM_CLASS(name, uuid, project, self, *args, **kwargs) + project.add_vm(vm) if asyncio.iscoroutinefunction(vm.create): yield from vm.create() else: diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index 42deb98f..52c003b2 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -27,7 +27,6 @@ class BaseVM: self._uuid = uuid self._project = project self._manager = manager - project.add_vm(self) log.debug("{module}: {name} [{uuid}] initialized".format(module=self.manager.module_name, name=self.name, diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index a3e0319f..4e8c61df 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -95,6 +95,7 @@ class Project: :param vm: An instance of VM """ + self.remove_vm(vm) self._vms_to_destroy.add(vm) def __json__(self): @@ -106,14 +107,25 @@ class Project: def add_vm(self, vm): """ - Add a VM to the project. In theory this should be called by - the VM initializer. + Add a VM to the project. + In theory this should be called by the VM manager. :params vm: A VM instance """ self._vms.add(vm) + def remove_vm(self, vm): + """ + Remove a VM from the project. + In theory this should be called by the VM manager. + + :params vm: A VM instance + """ + + if vm in self._vms: + self._vms.remove(vm) + def close(self): """Close the project, but keep informations on disk""" @@ -128,6 +140,7 @@ class Project: directory = self.vm_working_directory(vm) if os.path.exists(directory): shutil.rmtree(directory) + self.remove_vm(vm) def delete(self): """Remove project from disk""" diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py index 2956adaf..d26488ac 100644 --- a/tests/modules/test_project.py +++ b/tests/modules/test_project.py @@ -68,14 +68,17 @@ def test_vm_working_directory(tmpdir, vm): def test_mark_vm_for_destruction(tmpdir, vm): - p = Project(location=str(tmpdir)) - p.mark_vm_for_destruction(vm) - assert len(p._vms_to_destroy) == 1 + project = Project(location=str(tmpdir)) + project.add_vm(vm) + project.mark_vm_for_destruction(vm) + assert len(project._vms_to_destroy) == 1 + assert len(project.vms) == 0 def test_commit(tmpdir, manager): project = Project(location=str(tmpdir)) vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + project.add_vm(vm) directory = project.vm_working_directory(vm) project.mark_vm_for_destruction(vm) assert len(project._vms_to_destroy) == 1 @@ -83,6 +86,7 @@ def test_commit(tmpdir, manager): project.commit() assert len(project._vms_to_destroy) == 0 assert os.path.exists(directory) is False + assert len(project.vms) == 0 def test_project_delete(tmpdir): @@ -95,8 +99,8 @@ def test_project_delete(tmpdir): def test_project_add_vm(tmpdir, manager): project = Project(location=str(tmpdir)) - # The VM initalizer call the add_vm method vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + project.add_vm(vm) assert len(project.vms) == 1 From abc885049f395e3cacbef2c67c22fd6fed6558d5 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 23 Jan 2015 16:02:26 +0100 Subject: [PATCH 112/485] Temporary project --- gns3server/handlers/project_handler.py | 7 +++--- gns3server/modules/project.py | 26 +++++++++++++++------ gns3server/schemas/project.py | 32 +++++++++++++++++++++++++- tests/api/test_project.py | 9 ++++++++ tests/modules/test_project.py | 12 +++++++++- 5 files changed, 74 insertions(+), 12 deletions(-) diff --git a/gns3server/handlers/project_handler.py b/gns3server/handlers/project_handler.py index e6fdbc59..83bfa401 100644 --- a/gns3server/handlers/project_handler.py +++ b/gns3server/handlers/project_handler.py @@ -16,7 +16,7 @@ # along with this program. If not, see . from ..web.route import Route -from ..schemas.project import PROJECT_OBJECT_SCHEMA +from ..schemas.project import PROJECT_OBJECT_SCHEMA, PROJECT_CREATE_SCHEMA from ..modules.project_manager import ProjectManager from aiohttp.web import HTTPConflict @@ -28,13 +28,14 @@ class ProjectHandler: r"/project", description="Create a project on the server", output=PROJECT_OBJECT_SCHEMA, - input=PROJECT_OBJECT_SCHEMA) + input=PROJECT_CREATE_SCHEMA) def create_project(request, response): pm = ProjectManager.instance() p = pm.create_project( location=request.json.get("location"), - uuid=request.json.get("uuid") + uuid=request.json.get("uuid"), + temporary=request.json.get("temporary", False) ) response.json(p) diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index 4e8c61df..40ba0601 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -29,9 +29,10 @@ class Project: :param uuid: Force project uuid (None by default auto generate an UUID) :param location: Parent path of the project. (None should create a tmp directory) + :param temporary: Boolean the project is a temporary project (destroy when closed) """ - def __init__(self, uuid=None, location=None): + def __init__(self, uuid=None, location=None, temporary=False): if uuid is None: self._uuid = str(uuid4()) @@ -46,6 +47,7 @@ class Project: if location is None: self._location = tempfile.mkdtemp() + self._temporary = temporary self._vms = set() self._vms_to_destroy = set() self._path = os.path.join(self._location, self._uuid) @@ -102,7 +104,8 @@ class Project: return { "uuid": self._uuid, - "location": self._location + "location": self._location, + "temporary": self._temporary } def add_vm(self, vm): @@ -110,7 +113,7 @@ class Project: Add a VM to the project. In theory this should be called by the VM manager. - :params vm: A VM instance + :param vm: A VM instance """ self._vms.add(vm) @@ -120,7 +123,7 @@ class Project: Remove a VM from the project. In theory this should be called by the VM manager. - :params vm: A VM instance + :param vm: A VM instance """ if vm in self._vms: @@ -129,8 +132,19 @@ class Project: def close(self): """Close the project, but keep informations on disk""" + self._close_and_clean(self._temporary) + + def _close_and_clean(self, cleanup): + """ + Close the project, and cleanup the disk if cleanup is True + + :param cleanup: If True drop the project directory + """ + for vm in self._vms: vm.close() + if cleanup and os.path.exists(self.path): + shutil.rmtree(self.path) def commit(self): """Write project changes on disk""" @@ -145,6 +159,4 @@ class Project: def delete(self): """Remove project from disk""" - self.close() - if os.path.exists(self.path): - shutil.rmtree(self.path) + self._close_and_clean(True) diff --git a/gns3server/schemas/project.py b/gns3server/schemas/project.py index 1e363ec0..08ef042d 100644 --- a/gns3server/schemas/project.py +++ b/gns3server/schemas/project.py @@ -16,6 +16,31 @@ # along with this program. If not, see . +PROJECT_CREATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to create a new Project instance", + "type": "object", + "properties": { + "location": { + "description": "Base directory where the project should be created on remote server", + "type": "string", + "minLength": 1 + }, + "uuid": { + "description": "Project UUID", + "type": "string", + "minLength": 36, + "maxLength": 36, + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + }, + "temporary": { + "description": "If project is a temporary project", + "type": "boolean" + }, + }, + "additionalProperties": False, +} + PROJECT_OBJECT_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", "description": "Request validation to create a new Project instance", @@ -33,6 +58,11 @@ PROJECT_OBJECT_SCHEMA = { "maxLength": 36, "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" }, + "temporary": { + "description": "If project is a temporary project", + "type": "boolean" + }, }, - "additionalProperties": False + "additionalProperties": False, + "required": ["location", "uuid", "temporary"] } diff --git a/tests/api/test_project.py b/tests/api/test_project.py index e343b735..28cbb046 100644 --- a/tests/api/test_project.py +++ b/tests/api/test_project.py @@ -34,6 +34,15 @@ def test_create_project_without_dir(server): response = server.post("/project", query) assert response.status == 200 assert response.json["uuid"] is not None + assert response.json["temporary"] is False + + +def test_create_temporary_project(server): + query = {"temporary": True} + response = server.post("/project", query) + assert response.status == 200 + assert response.json["uuid"] is not None + assert response.json["temporary"] is True def test_create_project_with_uuid(server): diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py index d26488ac..4aaee655 100644 --- a/tests/modules/test_project.py +++ b/tests/modules/test_project.py @@ -58,7 +58,7 @@ def test_temporary_path(): def test_json(tmpdir): p = Project() - assert p.__json__() == {"location": p.location, "uuid": p.uuid} + assert p.__json__() == {"location": p.location, "uuid": p.uuid, "temporary": False} def test_vm_working_directory(tmpdir, vm): @@ -111,3 +111,13 @@ def test_project_close(tmpdir, manager): with patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM.close") as mock: project.close() assert mock.called + + +def test_project_close_temporary_project(tmpdir, manager): + """A temporary project is deleted when closed""" + + project = Project(location=str(tmpdir), temporary=True) + directory = project.path + assert os.path.exists(directory) + project.close() + assert os.path.exists(directory) is False From 0e76527ce2ca73753754886446e289b44d23befe Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 23 Jan 2015 16:13:58 +0100 Subject: [PATCH 113/485] Update a project --- gns3server/handlers/project_handler.py | 22 +++++++++++++++++++++- gns3server/modules/project.py | 10 ++++++++++ gns3server/schemas/project.py | 15 ++++++++++++++- tests/api/test_project.py | 10 ++++++++++ 4 files changed, 55 insertions(+), 2 deletions(-) diff --git a/gns3server/handlers/project_handler.py b/gns3server/handlers/project_handler.py index 83bfa401..a7cbff0f 100644 --- a/gns3server/handlers/project_handler.py +++ b/gns3server/handlers/project_handler.py @@ -16,7 +16,7 @@ # along with this program. If not, see . from ..web.route import Route -from ..schemas.project import PROJECT_OBJECT_SCHEMA, PROJECT_CREATE_SCHEMA +from ..schemas.project import PROJECT_OBJECT_SCHEMA, PROJECT_CREATE_SCHEMA, PROJECT_UPDATE_SCHEMA from ..modules.project_manager import ProjectManager from aiohttp.web import HTTPConflict @@ -39,6 +39,26 @@ class ProjectHandler: ) response.json(p) + @classmethod + @Route.put( + r"/project/{uuid}", + description="Update a project", + parameters={ + "uuid": "Project instance UUID", + }, + status_codes={ + 200: "Project updated", + 404: "Project instance doesn't exist" + }, + output=PROJECT_OBJECT_SCHEMA, + input=PROJECT_UPDATE_SCHEMA) + def update(request, response): + + pm = ProjectManager.instance() + project = pm.get_project(request.match_info["uuid"]) + project.temporary = request.json.get("temporary", project.temporary) + response.json(project) + @classmethod @Route.post( r"/project/{uuid}/commit", diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index 40ba0601..b12379a7 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -76,6 +76,16 @@ class Project: return self._vms + @property + def temporary(self): + + return self._temporary + + @temporary.setter + def temporary(self, temporary): + + self._temporary = temporary + def vm_working_directory(self, vm): """ Return a working directory for a specific VM. diff --git a/gns3server/schemas/project.py b/gns3server/schemas/project.py index 08ef042d..e0c2209a 100644 --- a/gns3server/schemas/project.py +++ b/gns3server/schemas/project.py @@ -41,9 +41,22 @@ PROJECT_CREATE_SCHEMA = { "additionalProperties": False, } +PROJECT_UPDATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to update a Project instance", + "type": "object", + "properties": { + "temporary": { + "description": "If project is a temporary project", + "type": "boolean" + }, + }, + "additionalProperties": False, +} + PROJECT_OBJECT_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to create a new Project instance", + "description": "Project instance", "type": "object", "properties": { "location": { diff --git a/tests/api/test_project.py b/tests/api/test_project.py index 28cbb046..4c2c6d22 100644 --- a/tests/api/test_project.py +++ b/tests/api/test_project.py @@ -60,6 +60,16 @@ def test_create_project_with_uuid(server): assert response.json["location"] == "/tmp" +def test_update_temporary_project(server): + query = {"temporary": True} + response = server.post("/project", query) + assert response.status == 200 + query = {"temporary": False} + response = server.put("/project/{uuid}".format(uuid=response.json["uuid"]), query) + assert response.status == 200 + assert response.json["temporary"] is False + + def test_commit_project(server, project): with asyncio_patch("gns3server.modules.project.Project.commit", return_value=True) as mock: response = server.post("/project/{uuid}/commit".format(uuid=project.uuid), example=True) From 547adf0dc6ab4bc4a0a6c3a728bde6a7ac9fe20b Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 23 Jan 2015 16:18:40 +0100 Subject: [PATCH 114/485] Get project --- gns3server/handlers/project_handler.py | 18 ++++++++++++++++++ tests/api/test_project.py | 13 +++++++++++++ 2 files changed, 31 insertions(+) diff --git a/gns3server/handlers/project_handler.py b/gns3server/handlers/project_handler.py index a7cbff0f..cc393c6c 100644 --- a/gns3server/handlers/project_handler.py +++ b/gns3server/handlers/project_handler.py @@ -39,6 +39,24 @@ class ProjectHandler: ) response.json(p) + @classmethod + @Route.get( + r"/project/{uuid}", + description="Get project informations", + parameters={ + "uuid": "Project instance UUID", + }, + status_codes={ + 200: "OK", + 404: "Project instance doesn't exist" + }, + output=PROJECT_OBJECT_SCHEMA) + def show(request, response): + + pm = ProjectManager.instance() + project = pm.get_project(request.match_info["uuid"]) + response.json(project) + @classmethod @Route.put( r"/project/{uuid}", diff --git a/tests/api/test_project.py b/tests/api/test_project.py index 4c2c6d22..eaa70982 100644 --- a/tests/api/test_project.py +++ b/tests/api/test_project.py @@ -60,6 +60,19 @@ def test_create_project_with_uuid(server): assert response.json["location"] == "/tmp" +def test_show_project(server): + query = {"uuid": "00010203-0405-0607-0809-0a0b0c0d0e0f", "location": "/tmp", "temporary": False} + response = server.post("/project", query) + assert response.status == 200 + response = server.get("/project/00010203-0405-0607-0809-0a0b0c0d0e0f") + assert response.json == query + + +def test_show_project_invalid_uuid(server): + response = server.get("/project/00010203-0405-0607-0809-0a0b0c0d0e42") + assert response.status == 404 + + def test_update_temporary_project(server): query = {"temporary": True} response = server.post("/project", query) From 977ff0fb57041d4ae352956dba8179667ae34571 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 23 Jan 2015 16:19:17 +0100 Subject: [PATCH 115/485] Build documentation --- docs/api/examples/delete_projectuuid.txt | 13 +++ docs/api/examples/get_vpcsuuid.txt | 2 +- docs/api/examples/post_project.txt | 3 +- docs/api/examples/post_projectuuidclose.txt | 13 +++ docs/api/examples/post_projectuuidcommit.txt | 13 +++ docs/api/examples/post_virtualbox.txt | 11 ++- docs/api/examples/post_vpcs.txt | 2 +- docs/api/project.rst | 6 +- docs/api/projectuuid.rst | 83 ++++++++++++++++++++ docs/api/projectuuidclose.rst | 24 ++++++ docs/api/projectuuidcommit.rst | 24 ++++++ docs/api/virtualbox.rst | 7 ++ docs/api/virtualboxuuidreload.rst | 19 +++++ docs/api/vpcsuuid.rst | 19 +++++ docs/api/vpcsuuidreload.rst | 1 + 15 files changed, 233 insertions(+), 7 deletions(-) create mode 100644 docs/api/examples/delete_projectuuid.txt create mode 100644 docs/api/examples/post_projectuuidclose.txt create mode 100644 docs/api/examples/post_projectuuidcommit.txt create mode 100644 docs/api/projectuuid.rst create mode 100644 docs/api/projectuuidclose.rst create mode 100644 docs/api/projectuuidcommit.rst create mode 100644 docs/api/virtualboxuuidreload.rst diff --git a/docs/api/examples/delete_projectuuid.txt b/docs/api/examples/delete_projectuuid.txt new file mode 100644 index 00000000..68989437 --- /dev/null +++ b/docs/api/examples/delete_projectuuid.txt @@ -0,0 +1,13 @@ +curl -i -X DELETE 'http://localhost:8000/project/{uuid}' + +DELETE /project/{uuid} HTTP/1.1 + + + +HTTP/1.1 204 +CONNECTION: close +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 aiohttp/0.13.1 +X-ROUTE: /project/{uuid} + diff --git a/docs/api/examples/get_vpcsuuid.txt b/docs/api/examples/get_vpcsuuid.txt index 23e0c6af..a414aadd 100644 --- a/docs/api/examples/get_vpcsuuid.txt +++ b/docs/api/examples/get_vpcsuuid.txt @@ -18,5 +18,5 @@ X-ROUTE: /vpcs/{uuid} "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, "startup_script": null, - "uuid": "925c4d08-58a5-4078-9e77-a6875e0c28dc" + "uuid": "fbbcc900-7fd1-4fcc-bc70-5f7eee8397b9" } diff --git a/docs/api/examples/post_project.txt b/docs/api/examples/post_project.txt index 68d9d07c..4f40b3a4 100644 --- a/docs/api/examples/post_project.txt +++ b/docs/api/examples/post_project.txt @@ -9,7 +9,7 @@ POST /project HTTP/1.1 HTTP/1.1 200 CONNECTION: close -CONTENT-LENGTH: 78 +CONTENT-LENGTH: 102 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT SERVER: Python/3.4 aiohttp/0.13.1 @@ -17,5 +17,6 @@ X-ROUTE: /project { "location": "/tmp", + "temporary": false, "uuid": "00010203-0405-0607-0809-0a0b0c0d0e0f" } diff --git a/docs/api/examples/post_projectuuidclose.txt b/docs/api/examples/post_projectuuidclose.txt new file mode 100644 index 00000000..d038c178 --- /dev/null +++ b/docs/api/examples/post_projectuuidclose.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/project/{uuid}/close' -d '{}' + +POST /project/{uuid}/close HTTP/1.1 +{} + + +HTTP/1.1 204 +CONNECTION: close +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 aiohttp/0.13.1 +X-ROUTE: /project/{uuid}/close + diff --git a/docs/api/examples/post_projectuuidcommit.txt b/docs/api/examples/post_projectuuidcommit.txt new file mode 100644 index 00000000..b5d0c2d9 --- /dev/null +++ b/docs/api/examples/post_projectuuidcommit.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/project/{uuid}/commit' -d '{}' + +POST /project/{uuid}/commit HTTP/1.1 +{} + + +HTTP/1.1 204 +CONNECTION: close +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 aiohttp/0.13.1 +X-ROUTE: /project/{uuid}/commit + diff --git a/docs/api/examples/post_virtualbox.txt b/docs/api/examples/post_virtualbox.txt index fdb19bc6..53195c53 100644 --- a/docs/api/examples/post_virtualbox.txt +++ b/docs/api/examples/post_virtualbox.txt @@ -11,14 +11,21 @@ POST /virtualbox HTTP/1.1 HTTP/1.1 201 CONNECTION: close -CONTENT-LENGTH: 133 +CONTENT-LENGTH: 348 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /virtualbox { + "adapter_start_index": 0, + "adapter_type": "Intel PRO/1000 MT Desktop (82540EM)", + "adapters": 0, + "enable_remote_console": false, + "headless": false, + "linked_clone": false, "name": "VM1", "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "uuid": "c220788f-ee1e-491c-b318-6542d2f130bf" + "uuid": "2908d568-4a42-49e1-9628-d914f2fd545d", + "vmname": "VM1" } diff --git a/docs/api/examples/post_vpcs.txt b/docs/api/examples/post_vpcs.txt index b66cac02..186c71ea 100644 --- a/docs/api/examples/post_vpcs.txt +++ b/docs/api/examples/post_vpcs.txt @@ -21,5 +21,5 @@ X-ROUTE: /vpcs "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, "startup_script": null, - "uuid": "4d670947-44a8-4156-8626-adce3faa5ae6" + "uuid": "5a2735f9-c38a-459c-8b7d-70ec56b62a7f" } diff --git a/docs/api/project.rst b/docs/api/project.rst index 22b8dcfb..15a2aa08 100644 --- a/docs/api/project.rst +++ b/docs/api/project.rst @@ -18,6 +18,7 @@ Input +
Name Mandatory Type Description
location string Base directory where the project should be created on remote server
temporary boolean If project is a temporary project
uuid string Project UUID
@@ -27,8 +28,9 @@ Output - - + + +
Name Mandatory Type Description
location string Base directory where the project should be created on remote server
uuid string Project UUID
location string Base directory where the project should be created on remote server
temporary boolean If project is a temporary project
uuid string Project UUID
Sample session diff --git a/docs/api/projectuuid.rst b/docs/api/projectuuid.rst new file mode 100644 index 00000000..68680d7d --- /dev/null +++ b/docs/api/projectuuid.rst @@ -0,0 +1,83 @@ +/project/{uuid} +--------------------------------------------- + +.. contents:: + +GET /project/**{uuid}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Get project informations + +Parameters +********** +- **uuid**: Project instance UUID + +Response status codes +********************** +- **200**: OK +- **404**: Project instance doesn't exist + +Output +******* +.. raw:: html + + + + + + +
Name Mandatory Type Description
location string Base directory where the project should be created on remote server
temporary boolean If project is a temporary project
uuid string Project UUID
+ + +PUT /project/**{uuid}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Update a project + +Parameters +********** +- **uuid**: Project instance UUID + +Response status codes +********************** +- **200**: Project updated +- **404**: Project instance doesn't exist + +Input +******* +.. raw:: html + + + + +
Name Mandatory Type Description
temporary boolean If project is a temporary project
+ +Output +******* +.. raw:: html + + + + + + +
Name Mandatory Type Description
location string Base directory where the project should be created on remote server
temporary boolean If project is a temporary project
uuid string Project UUID
+ + +DELETE /project/**{uuid}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Delete a project from disk + +Parameters +********** +- **uuid**: Project instance UUID + +Response status codes +********************** +- **404**: Project instance doesn't exist +- **204**: Changes write on disk + +Sample session +*************** + + +.. literalinclude:: examples/delete_projectuuid.txt + diff --git a/docs/api/projectuuidclose.rst b/docs/api/projectuuidclose.rst new file mode 100644 index 00000000..dffca28c --- /dev/null +++ b/docs/api/projectuuidclose.rst @@ -0,0 +1,24 @@ +/project/{uuid}/close +--------------------------------------------- + +.. contents:: + +POST /project/**{uuid}**/close +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Close project + +Parameters +********** +- **uuid**: Project instance UUID + +Response status codes +********************** +- **404**: Project instance doesn't exist +- **204**: Project closed + +Sample session +*************** + + +.. literalinclude:: examples/post_projectuuidclose.txt + diff --git a/docs/api/projectuuidcommit.rst b/docs/api/projectuuidcommit.rst new file mode 100644 index 00000000..6bbd35f1 --- /dev/null +++ b/docs/api/projectuuidcommit.rst @@ -0,0 +1,24 @@ +/project/{uuid}/commit +--------------------------------------------- + +.. contents:: + +POST /project/**{uuid}**/commit +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Write changes on disk + +Parameters +********** +- **uuid**: Project instance UUID + +Response status codes +********************** +- **404**: Project instance doesn't exist +- **204**: Changes write on disk + +Sample session +*************** + + +.. literalinclude:: examples/post_projectuuidcommit.txt + diff --git a/docs/api/virtualbox.rst b/docs/api/virtualbox.rst index 3f96363c..b880491b 100644 --- a/docs/api/virtualbox.rst +++ b/docs/api/virtualbox.rst @@ -33,10 +33,17 @@ Output + + + + + + +
Name Mandatory Type Description
adapter_start_index integer adapter index from which to start using adapters
adapter_type string VirtualBox adapter type
adapters integer number of adapters
console integer console TCP port
enable_remote_console boolean enable the remote console
headless boolean headless mode
linked_clone boolean either the VM is a linked clone or not
name string VirtualBox VM instance name
project_uuid string Project UUID
uuid string VirtualBox VM instance UUID
vmname string VirtualBox VM name (in VirtualBox itself)
Sample session diff --git a/docs/api/virtualboxuuidreload.rst b/docs/api/virtualboxuuidreload.rst new file mode 100644 index 00000000..49fc6fab --- /dev/null +++ b/docs/api/virtualboxuuidreload.rst @@ -0,0 +1,19 @@ +/virtualbox/{uuid}/reload +--------------------------------------------- + +.. contents:: + +POST /virtualbox/**{uuid}**/reload +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Reload a VirtualBox VM instance + +Parameters +********** +- **uuid**: VirtualBox VM instance UUID + +Response status codes +********************** +- **400**: Invalid VirtualBox VM instance UUID +- **404**: VirtualBox VM instance doesn't exist +- **204**: VirtualBox VM instance reloaded + diff --git a/docs/api/vpcsuuid.rst b/docs/api/vpcsuuid.rst index 43bca0e5..664df1bf 100644 --- a/docs/api/vpcsuuid.rst +++ b/docs/api/vpcsuuid.rst @@ -27,10 +27,15 @@ PUT /vpcs/**{uuid}** ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Update a VPCS instance +Parameters +********** +- **uuid**: VPCS instance UUID + Response status codes ********************** - **200**: VPCS instance updated - **409**: Conflict +- **404**: VPCS instance doesn't exist Input ******* @@ -58,3 +63,17 @@ Output uuid ✔ string VPCS device UUID + +DELETE /vpcs/**{uuid}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Delete a VPCS instance + +Parameters +********** +- **uuid**: VPCS instance UUID + +Response status codes +********************** +- **404**: VPCS instance doesn't exist +- **204**: VPCS instance deleted + diff --git a/docs/api/vpcsuuidreload.rst b/docs/api/vpcsuuidreload.rst index 263afc08..728a34cf 100644 --- a/docs/api/vpcsuuidreload.rst +++ b/docs/api/vpcsuuidreload.rst @@ -13,6 +13,7 @@ Parameters Response status codes ********************** +- **400**: Invalid VPCS instance UUID - **404**: VPCS instance doesn't exist - **204**: VPCS reloaded From 77ee6501b9034d5ca628f39faba4497f354f59ae Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 23 Jan 2015 16:20:12 +0100 Subject: [PATCH 116/485] Update documentation documentation --- docs/development.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/development.rst b/docs/development.rst index 28e7f47a..b2a1be80 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -17,7 +17,7 @@ In the project root folder: .. code-block:: bash - ./documentation.sh + ./scripts/documentation.sh The output is available inside *docs/_build/html* From 4848eeabadbef01b39c6f7fae936b134ef728b9e Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 23 Jan 2015 16:21:26 +0100 Subject: [PATCH 117/485] Add missing curl example --- docs/api/examples/get_projectuuid.txt | 19 +++++++++++++++++++ docs/api/examples/get_vpcsuuid.txt | 2 +- docs/api/examples/post_virtualbox.txt | 2 +- docs/api/examples/post_vpcs.txt | 2 +- docs/api/examples/put_projectuuid.txt | 21 +++++++++++++++++++++ docs/api/projectuuid.rst | 12 ++++++++++++ tests/api/test_project.py | 4 ++-- 7 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 docs/api/examples/get_projectuuid.txt create mode 100644 docs/api/examples/put_projectuuid.txt diff --git a/docs/api/examples/get_projectuuid.txt b/docs/api/examples/get_projectuuid.txt new file mode 100644 index 00000000..bed046ad --- /dev/null +++ b/docs/api/examples/get_projectuuid.txt @@ -0,0 +1,19 @@ +curl -i -X GET 'http://localhost:8000/project/{uuid}' + +GET /project/{uuid} HTTP/1.1 + + + +HTTP/1.1 200 +CONNECTION: close +CONTENT-LENGTH: 102 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 aiohttp/0.13.1 +X-ROUTE: /project/{uuid} + +{ + "location": "/tmp", + "temporary": false, + "uuid": "00010203-0405-0607-0809-0a0b0c0d0e0f" +} diff --git a/docs/api/examples/get_vpcsuuid.txt b/docs/api/examples/get_vpcsuuid.txt index a414aadd..616653fe 100644 --- a/docs/api/examples/get_vpcsuuid.txt +++ b/docs/api/examples/get_vpcsuuid.txt @@ -18,5 +18,5 @@ X-ROUTE: /vpcs/{uuid} "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, "startup_script": null, - "uuid": "fbbcc900-7fd1-4fcc-bc70-5f7eee8397b9" + "uuid": "b37ef237-15aa-46a7-bdc5-8fa8657056c6" } diff --git a/docs/api/examples/post_virtualbox.txt b/docs/api/examples/post_virtualbox.txt index 53195c53..7b051df4 100644 --- a/docs/api/examples/post_virtualbox.txt +++ b/docs/api/examples/post_virtualbox.txt @@ -26,6 +26,6 @@ X-ROUTE: /virtualbox "linked_clone": false, "name": "VM1", "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "uuid": "2908d568-4a42-49e1-9628-d914f2fd545d", + "uuid": "767b6b21-2209-4d73-aec8-49e4a332709d", "vmname": "VM1" } diff --git a/docs/api/examples/post_vpcs.txt b/docs/api/examples/post_vpcs.txt index 186c71ea..2c8403fb 100644 --- a/docs/api/examples/post_vpcs.txt +++ b/docs/api/examples/post_vpcs.txt @@ -21,5 +21,5 @@ X-ROUTE: /vpcs "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, "startup_script": null, - "uuid": "5a2735f9-c38a-459c-8b7d-70ec56b62a7f" + "uuid": "076902d4-97d2-4243-b4fb-374a381d4bc5" } diff --git a/docs/api/examples/put_projectuuid.txt b/docs/api/examples/put_projectuuid.txt new file mode 100644 index 00000000..90ba05a1 --- /dev/null +++ b/docs/api/examples/put_projectuuid.txt @@ -0,0 +1,21 @@ +curl -i -X PUT 'http://localhost:8000/project/{uuid}' -d '{"temporary": false}' + +PUT /project/{uuid} HTTP/1.1 +{ + "temporary": false +} + + +HTTP/1.1 200 +CONNECTION: close +CONTENT-LENGTH: 158 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 aiohttp/0.13.1 +X-ROUTE: /project/{uuid} + +{ + "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmps4qnfnar", + "temporary": false, + "uuid": "b3eccaca-af01-4244-a3fd-da1fb98d04c9" +} diff --git a/docs/api/projectuuid.rst b/docs/api/projectuuid.rst index 68680d7d..5eebd0bf 100644 --- a/docs/api/projectuuid.rst +++ b/docs/api/projectuuid.rst @@ -27,6 +27,12 @@ Output uuid ✔ string Project UUID +Sample session +*************** + + +.. literalinclude:: examples/get_projectuuid.txt + PUT /project/**{uuid}** ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -61,6 +67,12 @@ Output uuid ✔ string Project UUID +Sample session +*************** + + +.. literalinclude:: examples/put_projectuuid.txt + DELETE /project/**{uuid}** ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/api/test_project.py b/tests/api/test_project.py index eaa70982..cfbe5f81 100644 --- a/tests/api/test_project.py +++ b/tests/api/test_project.py @@ -64,7 +64,7 @@ def test_show_project(server): query = {"uuid": "00010203-0405-0607-0809-0a0b0c0d0e0f", "location": "/tmp", "temporary": False} response = server.post("/project", query) assert response.status == 200 - response = server.get("/project/00010203-0405-0607-0809-0a0b0c0d0e0f") + response = server.get("/project/00010203-0405-0607-0809-0a0b0c0d0e0f", example=True) assert response.json == query @@ -78,7 +78,7 @@ def test_update_temporary_project(server): response = server.post("/project", query) assert response.status == 200 query = {"temporary": False} - response = server.put("/project/{uuid}".format(uuid=response.json["uuid"]), query) + response = server.put("/project/{uuid}".format(uuid=response.json["uuid"]), query, example=True) assert response.status == 200 assert response.json["temporary"] is False From 4f2764c0b4c6d2b29c0e8bf508c97f9b3ad7bbec Mon Sep 17 00:00:00 2001 From: grossmj Date: Fri, 23 Jan 2015 08:44:00 -0700 Subject: [PATCH 118/485] Fixes module unload. --- gns3server/modules/base_manager.py | 3 ++- gns3server/server.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index aef1579a..8788e5f1 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -99,9 +99,10 @@ class BaseManager: for uuid in self._vms.keys(): try: - self.delete_vm(uuid) + yield from self.delete_vm(uuid) except Exception as e: log.warn("Could not delete VM {}: {}".format(uuid, e)) + continue if hasattr(BaseManager, "_instance"): BaseManager._instance = None diff --git a/gns3server/server.py b/gns3server/server.py index 1fdeb876..f0c885a9 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -80,7 +80,7 @@ class Server: for module in MODULES: log.debug("Unloading module {}".format(module.__name__)) m = module.instance() - m.unload() + self._loop.run_until_complete(m.unload()) self._loop.stop() def _signal_handling(self): From 017c5ac9f6d1e5c3699d388417e9a25ef7daab65 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 23 Jan 2015 16:57:41 +0100 Subject: [PATCH 119/485] Allow changing location only for local usage --- gns3server/modules/project.py | 5 +++++ tests/api/test_project.py | 21 ++++++++------------- tests/modules/test_project.py | 33 +++++++++++++++++++++------------ 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index b12379a7..aceb2a2b 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -20,6 +20,7 @@ import os import tempfile import shutil from uuid import UUID, uuid4 +from ..config import Config class Project: @@ -46,6 +47,10 @@ class Project: self._location = location if location is None: self._location = tempfile.mkdtemp() + else: + config = Config.instance().get_section_config("Server") + if config.get("local", False) is False: + raise aiohttp.web.HTTPForbidden(text="You are not allowed to modifiy the project directory location") self._temporary = temporary self._vms = set() diff --git a/tests/api/test_project.py b/tests/api/test_project.py index cfbe5f81..ee57954c 100644 --- a/tests/api/test_project.py +++ b/tests/api/test_project.py @@ -20,13 +20,15 @@ This test suite check /project endpoint """ import uuid +from unittest.mock import patch from tests.utils import asyncio_patch def test_create_project_with_dir(server, tmpdir): - response = server.post("/project", {"location": str(tmpdir)}) - assert response.status == 200 - assert response.json["location"] == str(tmpdir) + with patch("gns3server.config.Config.get_section_config", return_value={"local": True}): + response = server.post("/project", {"location": str(tmpdir)}) + assert response.status == 200 + assert response.json["location"] == str(tmpdir) def test_create_project_without_dir(server): @@ -52,18 +54,11 @@ def test_create_project_with_uuid(server): assert response.json["uuid"] == "00010203-0405-0607-0809-0a0b0c0d0e0f" -def test_create_project_with_uuid(server): - query = {"uuid": "00010203-0405-0607-0809-0a0b0c0d0e0f", "location": "/tmp"} - response = server.post("/project", query, example=True) - assert response.status == 200 - assert response.json["uuid"] == "00010203-0405-0607-0809-0a0b0c0d0e0f" - assert response.json["location"] == "/tmp" - - def test_show_project(server): query = {"uuid": "00010203-0405-0607-0809-0a0b0c0d0e0f", "location": "/tmp", "temporary": False} - response = server.post("/project", query) - assert response.status == 200 + with patch("gns3server.config.Config.get_section_config", return_value={"local": True}): + response = server.post("/project", query) + assert response.status == 200 response = server.get("/project/00010203-0405-0607-0809-0a0b0c0d0e0f", example=True) assert response.json == query diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py index 4aaee655..bc93b04c 100644 --- a/tests/modules/test_project.py +++ b/tests/modules/test_project.py @@ -18,6 +18,7 @@ import os import pytest +import aiohttp from unittest.mock import patch from gns3server.modules.project import Project @@ -44,6 +45,7 @@ def test_affect_uuid(): assert p.uuid == '00010203-0405-0607-0809-0a0b0c0d0e0f' +@patch("gns3server.config.Config.get_section_config", return_value={"local": True}) def test_path(tmpdir): p = Project(location=str(tmpdir)) assert p.path == os.path.join(str(tmpdir), p.uuid) @@ -56,27 +58,34 @@ def test_temporary_path(): assert os.path.exists(p.path) +@patch("gns3server.config.Config.get_section_config", return_value={"local": False}) +def test_changing_location_not_allowed(mock, tmpdir): + with pytest.raises(aiohttp.web.HTTPForbidden): + p = Project(location=str(tmpdir)) + + def test_json(tmpdir): p = Project() assert p.__json__() == {"location": p.location, "uuid": p.uuid, "temporary": False} +@patch("gns3server.config.Config.get_section_config", return_value={"local": True}) def test_vm_working_directory(tmpdir, vm): p = Project(location=str(tmpdir)) assert os.path.exists(p.vm_working_directory(vm)) assert os.path.exists(os.path.join(str(tmpdir), p.uuid, vm.module_name, vm.uuid)) -def test_mark_vm_for_destruction(tmpdir, vm): - project = Project(location=str(tmpdir)) +def test_mark_vm_for_destruction(vm): + project = Project() project.add_vm(vm) project.mark_vm_for_destruction(vm) assert len(project._vms_to_destroy) == 1 assert len(project.vms) == 0 -def test_commit(tmpdir, manager): - project = Project(location=str(tmpdir)) +def test_commit(manager): + project = Project() vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) project.add_vm(vm) directory = project.vm_working_directory(vm) @@ -89,23 +98,23 @@ def test_commit(tmpdir, manager): assert len(project.vms) == 0 -def test_project_delete(tmpdir): - project = Project(location=str(tmpdir)) +def test_project_delete(): + project = Project() directory = project.path assert os.path.exists(directory) project.delete() assert os.path.exists(directory) is False -def test_project_add_vm(tmpdir, manager): - project = Project(location=str(tmpdir)) +def test_project_add_vm(manager): + project = Project() vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) project.add_vm(vm) assert len(project.vms) == 1 -def test_project_close(tmpdir, manager): - project = Project(location=str(tmpdir)) +def test_project_close(manager): + project = Project() vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) project.add_vm(vm) with patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM.close") as mock: @@ -113,10 +122,10 @@ def test_project_close(tmpdir, manager): assert mock.called -def test_project_close_temporary_project(tmpdir, manager): +def test_project_close_temporary_project(manager): """A temporary project is deleted when closed""" - project = Project(location=str(tmpdir), temporary=True) + project = Project(temporary=True) directory = project.path assert os.path.exists(directory) project.close() From 7bed9f56bc304cd483534f54066a83c8a85fee8e Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 23 Jan 2015 17:33:58 +0100 Subject: [PATCH 120/485] Avoid crash when closing vms Otherwise the size of dict change and Python raise an exception. --- gns3server/modules/base_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 8788e5f1..8913b21f 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -97,7 +97,7 @@ class BaseManager: @asyncio.coroutine def unload(self): - for uuid in self._vms.keys(): + for uuid in list(self._vms.keys()): try: yield from self.delete_vm(uuid) except Exception as e: From 8e249b670d751189e966a0956e9deae088a764d5 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 23 Jan 2015 17:39:17 +0100 Subject: [PATCH 121/485] Set a location by default --- gns3server/config.py | 11 +++++++++++ gns3server/modules/project.py | 17 +++++++++++++++-- tests/conftest.py | 18 ++++++++++++++++++ tests/modules/test_project.py | 8 ++++++++ 4 files changed, 52 insertions(+), 2 deletions(-) diff --git a/gns3server/config.py b/gns3server/config.py index f2ef0353..4dfefbcf 100644 --- a/gns3server/config.py +++ b/gns3server/config.py @@ -122,6 +122,17 @@ class Config(object): return self._config["DEFAULT"] return self._config[section] + def set_section_config(self, section, content): + """ + Set a specific configuration section. It's not + dumped on the disk. + + :param section: Section name + :param content: A dictonary with section content + """ + + self._config[section] = content + @staticmethod def instance(): """ diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index aceb2a2b..e6de01c5 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -44,11 +44,11 @@ class Project: raise aiohttp.web.HTTPBadRequest(text="{} is not a valid UUID".format(uuid)) self._uuid = uuid + config = Config.instance().get_section_config("Server") self._location = location if location is None: - self._location = tempfile.mkdtemp() + self._location = config.get("project_directory", self._get_default_project_directory()) else: - config = Config.instance().get_section_config("Server") if config.get("local", False) is False: raise aiohttp.web.HTTPForbidden(text="You are not allowed to modifiy the project directory location") @@ -61,6 +61,19 @@ class Project: except OSError as e: raise aiohttp.web.HTTPInternalServerError(text="Could not create project directory: {}".format(e)) + def _get_default_project_directory(self): + """ + Return the default location for the project directory + depending of the operating system + """ + + path = os.path.normpath(os.path.expanduser("~/GNS3/projects")) + try: + os.makedirs(path, exist_ok=True) + except OSError as e: + raise aiohttp.web.HTTPInternalServerError(text="Could not create project directory: {}".format(e)) + return path + @property def uuid(self): diff --git a/tests/conftest.py b/tests/conftest.py index 1a80a456..41682bb3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,7 +18,11 @@ import pytest import socket import asyncio +import tempfile +import shutil from aiohttp import web + +from gns3server.config import Config from gns3server.web.route import Route # TODO: get rid of * from gns3server.handlers import * @@ -100,3 +104,17 @@ def free_console_port(request, port_manager): # the test do whatever the test want port_manager.release_console_port(port) return port + + +@pytest.yield_fixture(autouse=True) +def run_around_tests(): + tmppath = tempfile.mkdtemp() + + config = Config.instance() + server_section = config.get_section_config("Server") + server_section["project_directory"] = tmppath + config.set_section_config("Server", server_section) + + yield + + shutil.rmtree(tmppath) diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py index bc93b04c..ee20d306 100644 --- a/tests/modules/test_project.py +++ b/tests/modules/test_project.py @@ -130,3 +130,11 @@ def test_project_close_temporary_project(manager): assert os.path.exists(directory) project.close() assert os.path.exists(directory) is False + + +def test_get_default_project_directory(): + + project = Project() + path = os.path.normpath(os.path.expanduser("~/GNS3/projects")) + assert project._get_default_project_directory() == path + assert os.path.exists(path) From 59f940625afb53fd4ded471c467aec183980fb5e Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 23 Jan 2015 18:37:29 +0100 Subject: [PATCH 122/485] Flag --local --- gns3server/main.py | 27 +++++++++++++++++++++++++++ gns3server/modules/project.py | 5 +++++ 2 files changed, 32 insertions(+) diff --git a/gns3server/main.py b/gns3server/main.py index 70bdacf1..ee3c3c4a 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -20,10 +20,12 @@ import os import datetime import sys import locale +import argparse from gns3server.server import Server from gns3server.web.logger import init_logger from gns3server.version import __version__ +from gns3server.config import Config import logging log = logging.getLogger(__name__) @@ -68,6 +70,24 @@ def locale_check(): log.info("current locale is {}.{}".format(language, encoding)) +def parse_arguments(): + + parser = argparse.ArgumentParser(description='GNS 3 server') + parser.add_argument('--local', + action="store_true", + dest='local', + help='Local mode (allow some insecure operations)') + args = parser.parse_args() + + config = Config.instance() + server_config = config.get_section_config("Server") + + if args.local: + server_config["local"] = "true" + + config.set_section_config("Server", server_config) + + def main(): """ Entry point for GNS3 server @@ -89,8 +109,15 @@ def main(): user_log = init_logger(logging.DEBUG, quiet=False) # FIXME END Temporary + parse_arguments() + user_log.info("GNS3 server version {}".format(__version__)) user_log.info("Copyright (c) 2007-{} GNS3 Technologies Inc.".format(current_year)) + + server_config = Config.instance().get_section_config("Server") + if server_config["local"]: + log.warning("Local mode is enabled. Beware it's allow a full control on your filesystem") + # TODO: end todo # we only support Python 3 version >= 3.3 diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index e6de01c5..14de142b 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -23,6 +23,10 @@ from uuid import UUID, uuid4 from ..config import Config +import logging +log = logging.getLogger(__name__) + + class Project: """ A project contains a list of VM. @@ -60,6 +64,7 @@ class Project: os.makedirs(os.path.join(self._path, "vms"), exist_ok=True) except OSError as e: raise aiohttp.web.HTTPInternalServerError(text="Could not create project directory: {}".format(e)) + log.debug("Create project {uuid} in directory {path}".format(path=self._path, uuid=self._uuid)) def _get_default_project_directory(self): """ From 39e3ca91a99f978d27800bb7b4c59ec9b2669d7b Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 23 Jan 2015 13:01:23 -0700 Subject: [PATCH 123/485] Fixes module unload & adds host, port and allow-remote-console command line args. --- gns3server/config.py | 2 +- gns3server/main.py | 34 ++++++++++++++++++------------ gns3server/modules/base_manager.py | 1 + gns3server/modules/port_manager.py | 14 ++++++++---- gns3server/server.py | 6 +++--- tests/conftest.py | 2 +- 6 files changed, 36 insertions(+), 23 deletions(-) diff --git a/gns3server/config.py b/gns3server/config.py index 4dfefbcf..56e26f4b 100644 --- a/gns3server/config.py +++ b/gns3server/config.py @@ -128,7 +128,7 @@ class Config(object): dumped on the disk. :param section: Section name - :param content: A dictonary with section content + :param content: A dictionary with section content """ self._config[section] = content diff --git a/gns3server/main.py b/gns3server/main.py index ee3c3c4a..91d0d2dd 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -72,11 +72,12 @@ def locale_check(): def parse_arguments(): - parser = argparse.ArgumentParser(description='GNS 3 server') - parser.add_argument('--local', - action="store_true", - dest='local', - help='Local mode (allow some insecure operations)') + parser = argparse.ArgumentParser(description="GNS3 server version {}".format(__version__)) + parser.add_argument("-l", "--host", help="run on the given host/IP address", default="127.0.0.1", nargs="?") + parser.add_argument("-p", "--port", type=int, help="run on the given port", default=8000, nargs="?") + parser.add_argument("-v", "--version", help="show the version", action="version", version=__version__) + parser.add_argument("-L", "--local", action="store_true", help="Local mode (allow some insecure operations)") + parser.add_argument("-A", "--allow-remote-console", dest="allow", action="store_true", help="Allow remote connections to console ports") args = parser.parse_args() config = Config.instance() @@ -84,7 +85,16 @@ def parse_arguments(): if args.local: server_config["local"] = "true" + else: + server_config["local"] = "false" + + if args.allow: + server_config["allow_remote_console"] = "true" + else: + server_config["allow_remote_console"] = "false" + server_config["host"] = args.host + server_config["port"] = str(args.port) config.set_section_config("Server", server_config) @@ -96,7 +106,6 @@ def main(): # TODO: migrate command line options to argparse (don't forget the quiet mode). current_year = datetime.date.today().year - # TODO: Renable the test when we will have command line # user_log = logging.getLogger('user_facing') # if not options.quiet: @@ -107,7 +116,6 @@ def main(): # user_log.propagate = False # END OLD LOG CODE user_log = init_logger(logging.DEBUG, quiet=False) - # FIXME END Temporary parse_arguments() @@ -115,10 +123,8 @@ def main(): user_log.info("Copyright (c) 2007-{} GNS3 Technologies Inc.".format(current_year)) server_config = Config.instance().get_section_config("Server") - if server_config["local"]: - log.warning("Local mode is enabled. Beware it's allow a full control on your filesystem") - - # TODO: end todo + if server_config.getboolean("local"): + log.warning("Local mode is enabled. Beware, clients will have full control on your filesystem") # we only support Python 3 version >= 3.3 if sys.version_info < (3, 3): @@ -137,9 +143,9 @@ def main(): log.critical("The current working directory doesn't exist") return - # TODO: Renable console_bind_to_any when we will have command line parsing - # server = Server(options.host, options.port, options.console_bind_to_any) - server = Server("127.0.0.1", 8000, False) + host = server_config["host"] + port = int(server_config["port"]) + server = Server(host, port) server.run() if __name__ == '__main__': diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 8913b21f..2c25c9bf 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -106,6 +106,7 @@ class BaseManager: if hasattr(BaseManager, "_instance"): BaseManager._instance = None + log.debug("Module {} unloaded".format(self.module_name)) def get_vm(self, uuid): """ diff --git a/gns3server/modules/port_manager.py b/gns3server/modules/port_manager.py index ed4404dc..b468dfce 100644 --- a/gns3server/modules/port_manager.py +++ b/gns3server/modules/port_manager.py @@ -17,17 +17,19 @@ import socket import ipaddress -import asyncio from aiohttp.web import HTTPConflict +from gns3server.config import Config +import logging +log = logging.getLogger(__name__) -class PortManager: +class PortManager: """ :param host: IP address to bind for console connections """ - def __init__(self, host="127.0.0.1", console_bind_to_any=False): + def __init__(self, host="127.0.0.1"): self._console_host = host self._udp_host = host @@ -37,7 +39,11 @@ class PortManager: self._used_tcp_ports = set() self._used_udp_ports = set() - if console_bind_to_any: + server_config = Config.instance().get_section_config("Server") + remote_console_connections = server_config.getboolean("allow_remote_console") + + if remote_console_connections: + log.warning("Remote console connections are allowed") if ipaddress.ip_address(host).version == 6: self._console_host = "::" else: diff --git a/gns3server/server.py b/gns3server/server.py index f0c885a9..f1dd6685 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -42,13 +42,13 @@ log = logging.getLogger(__name__) class Server: - def __init__(self, host, port, console_bind_to_any): + def __init__(self, host, port): self._host = host self._port = port self._loop = None self._start_time = time.time() - self._port_manager = PortManager(host, console_bind_to_any) + self._port_manager = PortManager(host) # TODO: server config file support, to be reviewed # # get the projects and temp directories from the configuration file (passed to the modules) @@ -80,7 +80,7 @@ class Server: for module in MODULES: log.debug("Unloading module {}".format(module.__name__)) m = module.instance() - self._loop.run_until_complete(m.unload()) + asyncio.async(m.unload()) self._loop.stop() def _signal_handling(self): diff --git a/tests/conftest.py b/tests/conftest.py index 41682bb3..630322ca 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -57,7 +57,7 @@ def _get_unused_port(): @pytest.fixture(scope="session") def server(request, loop, port_manager): - """A GNS 3 server""" + """A GNS3 server""" port = _get_unused_port() host = "localhost" From 6e7a5ca8bd225f4339894bad7c370e4b37908a6e Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 23 Jan 2015 13:10:57 -0700 Subject: [PATCH 124/485] Adds debug and quiet command line args. --- gns3server/main.py | 35 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/gns3server/main.py b/gns3server/main.py index 91d0d2dd..b9003bef 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -51,23 +51,23 @@ def locale_check(): try: language, encoding = locale.getlocale() except ValueError as e: - log.error("could not determine the current locale: {}".format(e)) + log.error("Could not determine the current locale: {}".format(e)) if not language and not encoding: try: - log.warn("could not find a default locale, switching to C.UTF-8...") + log.warn("Could not find a default locale, switching to C.UTF-8...") locale.setlocale(locale.LC_ALL, ("C", "UTF-8")) except locale.Error as e: - log.error("could not switch to the C.UTF-8 locale: {}".format(e)) + log.error("Could not switch to the C.UTF-8 locale: {}".format(e)) raise SystemExit elif encoding != "UTF-8": - log.warn("your locale {}.{} encoding is not UTF-8, switching to the UTF-8 version...".format(language, encoding)) + log.warn("Your locale {}.{} encoding is not UTF-8, switching to the UTF-8 version...".format(language, encoding)) try: locale.setlocale(locale.LC_ALL, (language, "UTF-8")) except locale.Error as e: - log.error("could not set an UTF-8 encoding for the {} locale: {}".format(language, e)) + log.error("Could not set an UTF-8 encoding for the {} locale: {}".format(language, e)) raise SystemExit else: - log.info("current locale is {}.{}".format(language, encoding)) + log.info("Current locale is {}.{}".format(language, encoding)) def parse_arguments(): @@ -76,7 +76,10 @@ def parse_arguments(): parser.add_argument("-l", "--host", help="run on the given host/IP address", default="127.0.0.1", nargs="?") parser.add_argument("-p", "--port", type=int, help="run on the given port", default=8000, nargs="?") parser.add_argument("-v", "--version", help="show the version", action="version", version=__version__) + parser.add_argument("-q", "--quiet", action="store_true", help="Do not show logs on stdout") + parser.add_argument("-d", "--debug", action="store_true", help="Show debug logs") parser.add_argument("-L", "--local", action="store_true", help="Local mode (allow some insecure operations)") + parser.add_argument("-A", "--allow-remote-console", dest="allow", action="store_true", help="Allow remote connections to console ports") args = parser.parse_args() @@ -97,27 +100,19 @@ def parse_arguments(): server_config["port"] = str(args.port) config.set_section_config("Server", server_config) + return args def main(): """ Entry point for GNS3 server """ - # TODO: migrate command line options to argparse (don't forget the quiet mode). - current_year = datetime.date.today().year - # TODO: Renable the test when we will have command line - # user_log = logging.getLogger('user_facing') - # if not options.quiet: - # # Send user facing messages to stdout. - # stream_handler = logging.StreamHandler(sys.stdout) - # stream_handler.addFilter(logging.Filter(name='user_facing')) - # user_log.addHandler(stream_handler) - # user_log.propagate = False - # END OLD LOG CODE - user_log = init_logger(logging.DEBUG, quiet=False) - - parse_arguments() + args = parse_arguments() + level = logging.INFO + if args.debug: + level = logging.DEBUG + user_log = init_logger(level, quiet=args.quiet) user_log.info("GNS3 server version {}".format(__version__)) user_log.info("Copyright (c) 2007-{} GNS3 Technologies Inc.".format(current_year)) From bc3d63081b113cd9731cc3445221c8b5495a6daa Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 23 Jan 2015 16:36:58 -0700 Subject: [PATCH 125/485] Unload should not delete VMs, just close them. --- gns3server/handlers/project_handler.py | 2 +- gns3server/handlers/vpcs_handler.py | 97 +++++++++++++------------- gns3server/main.py | 1 + gns3server/modules/base_manager.py | 4 +- 4 files changed, 53 insertions(+), 51 deletions(-) diff --git a/gns3server/handlers/project_handler.py b/gns3server/handlers/project_handler.py index cc393c6c..1ce30c18 100644 --- a/gns3server/handlers/project_handler.py +++ b/gns3server/handlers/project_handler.py @@ -42,7 +42,7 @@ class ProjectHandler: @classmethod @Route.get( r"/project/{uuid}", - description="Get project informations", + description="Get project information", parameters={ "uuid": "Project instance UUID", }, diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index 64da6047..cc90e806 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -33,7 +33,7 @@ class VPCSHandler: @Route.post( r"/vpcs", status_codes={ - 201: "VPCS instance created", + 201: "Instance created", 400: "Invalid project UUID", 409: "Conflict" }, @@ -56,13 +56,14 @@ class VPCSHandler: @Route.get( r"/vpcs/{uuid}", parameters={ - "uuid": "VPCS instance UUID" + "uuid": "Instance UUID" }, status_codes={ 200: "Success", - 404: "VPCS instance doesn't exist" + 404: "Instance doesn't exist" }, - description="Get a VPCS instance") + description="Get a VPCS instance", + output=VPCS_OBJECT_SCHEMA) def show(request, response): vpcs_manager = VPCS.instance() @@ -73,11 +74,11 @@ class VPCSHandler: @Route.put( r"/vpcs/{uuid}", parameters={ - "uuid": "VPCS instance UUID" + "uuid": "Instance UUID" }, status_codes={ - 200: "VPCS instance updated", - 404: "VPCS instance doesn't exist", + 200: "Instance updated", + 404: "Instance doesn't exist", 409: "Conflict" }, description="Update a VPCS instance", @@ -97,11 +98,11 @@ class VPCSHandler: @Route.delete( r"/vpcs/{uuid}", parameters={ - "uuid": "VPCS instance UUID" + "uuid": "Instance UUID" }, status_codes={ - 204: "VPCS instance deleted", - 404: "VPCS instance doesn't exist" + 204: "Instance deleted", + 404: "Instance doesn't exist" }, description="Delete a VPCS instance") def delete(request, response): @@ -113,12 +114,12 @@ class VPCSHandler: @Route.post( r"/vpcs/{uuid}/start", parameters={ - "uuid": "VPCS instance UUID" + "uuid": "Instance UUID" }, status_codes={ - 204: "VPCS instance started", + 204: "Instance started", 400: "Invalid VPCS instance UUID", - 404: "VPCS instance doesn't exist" + 404: "Instance doesn't exist" }, description="Start a VPCS instance") def start(request, response): @@ -132,12 +133,12 @@ class VPCSHandler: @Route.post( r"/vpcs/{uuid}/stop", parameters={ - "uuid": "VPCS instance UUID" + "uuid": "Instance UUID" }, status_codes={ - 204: "VPCS instance stopped", + 204: "Instance stopped", 400: "Invalid VPCS instance UUID", - 404: "VPCS instance doesn't exist" + 404: "Instance doesn't exist" }, description="Stop a VPCS instance") def stop(request, response): @@ -147,18 +148,37 @@ class VPCSHandler: yield from vm.stop() response.set_status(204) + @classmethod + @Route.post( + r"/vpcs/{uuid}/reload", + parameters={ + "uuid": "Instance UUID", + }, + status_codes={ + 204: "Instance reloaded", + 400: "Invalid instance UUID", + 404: "Instance doesn't exist" + }, + description="Reload a VPCS instance") + def reload(request, response): + + vpcs_manager = VPCS.instance() + vm = vpcs_manager.get_vm(request.match_info["uuid"]) + yield from vm.reload() + response.set_status(204) + @Route.post( - r"/vpcs/{uuid}/ports/{port_id}/nio", + r"/vpcs/{uuid}/ports/{port_id:\d+}/nio", parameters={ - "uuid": "VPCS instance UUID", - "port_id": "Id of the port where the nio should be add" + "uuid": "Instance UUID", + "port_id": "ID of the port where the nio should be added" }, status_codes={ 201: "NIO created", - 400: "Invalid VPCS instance UUID", - 404: "VPCS instance doesn't exist" + 400: "Invalid instance UUID", + 404: "Instance doesn't exist" }, - description="Add a NIO to a VPCS", + description="Add a NIO to a VPCS instance", input=VPCS_NIO_SCHEMA, output=VPCS_NIO_SCHEMA) def create_nio(request, response): @@ -172,39 +192,20 @@ class VPCSHandler: @classmethod @Route.delete( - r"/vpcs/{uuid}/ports/{port_id}/nio", + r"/vpcs/{uuid}/ports/{port_id:\d+}/nio", parameters={ - "uuid": "VPCS instance UUID", - "port_id": "ID of the port where the nio should be removed" + "uuid": "Instance UUID", + "port_id": "ID of the port from where the nio should be removed" }, status_codes={ 204: "NIO deleted", - 400: "Invalid VPCS instance UUID", - 404: "VPCS instance doesn't exist" + 400: "Invalid instance UUID", + 404: "Instance doesn't exist" }, - description="Remove a NIO from a VPCS") + description="Remove a NIO from a VPCS instance") def delete_nio(request, response): vpcs_manager = VPCS.instance() vm = vpcs_manager.get_vm(request.match_info["uuid"]) - nio = vm.port_remove_nio_binding(int(request.match_info["port_id"])) - response.set_status(204) - - @classmethod - @Route.post( - r"/vpcs/{uuid}/reload", - parameters={ - "uuid": "VPCS instance UUID", - }, - status_codes={ - 204: "VPCS reloaded", - 400: "Invalid VPCS instance UUID", - 404: "VPCS instance doesn't exist" - }, - description="Remove a NIO from a VPCS") - def reload(request, response): - - vpcs_manager = VPCS.instance() - vm = vpcs_manager.get_vm(request.match_info["uuid"]) - yield from vm.reload() + vm.port_remove_nio_binding(int(request.match_info["port_id"])) response.set_status(204) diff --git a/gns3server/main.py b/gns3server/main.py index b9003bef..c34bcffe 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -102,6 +102,7 @@ def parse_arguments(): return args + def main(): """ Entry point for GNS3 server diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 2c25c9bf..112f47d2 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -97,9 +97,9 @@ class BaseManager: @asyncio.coroutine def unload(self): - for uuid in list(self._vms.keys()): + for uuid in self._vms.keys(): try: - yield from self.delete_vm(uuid) + yield from self.close_vm(uuid) except Exception as e: log.warn("Could not delete VM {}: {}".format(uuid, e)) continue From 6460e94311be13098d7d8aaf409659480f2467d2 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 23 Jan 2015 16:38:46 -0700 Subject: [PATCH 126/485] More VirtualBox implementation. --- gns3server/handlers/virtualbox_handler.py | 150 +++++++++++++++--- gns3server/modules/port_manager.py | 6 +- .../modules/virtualbox/virtualbox_vm.py | 63 ++++---- gns3server/schemas/virtualbox.py | 96 +++++++++++ gns3server/schemas/vpcs.py | 4 +- 5 files changed, 256 insertions(+), 63 deletions(-) diff --git a/gns3server/handlers/virtualbox_handler.py b/gns3server/handlers/virtualbox_handler.py index 03a2e99e..b0c21334 100644 --- a/gns3server/handlers/virtualbox_handler.py +++ b/gns3server/handlers/virtualbox_handler.py @@ -17,6 +17,8 @@ from ..web.route import Route from ..schemas.virtualbox import VBOX_CREATE_SCHEMA +from ..schemas.virtualbox import VBOX_UPDATE_SCHEMA +from ..schemas.virtualbox import VBOX_NIO_SCHEMA from ..schemas.virtualbox import VBOX_OBJECT_SCHEMA from ..modules.virtualbox import VirtualBox @@ -31,7 +33,7 @@ class VirtualBoxHandler: @Route.post( r"/virtualbox", status_codes={ - 201: "VirtualBox VM instance created", + 201: "Instance created", 400: "Invalid project UUID", 409: "Conflict" }, @@ -46,20 +48,81 @@ class VirtualBoxHandler: request.json.get("uuid"), request.json["vmname"], request.json["linked_clone"], - console=request.json.get("console")) + console=request.json.get("console"), + vbox_user=request.json.get("vbox_user")) response.set_status(201) response.json(vm) + @classmethod + @Route.get( + r"/virtualbox/{uuid}", + parameters={ + "uuid": "Instance UUID" + }, + status_codes={ + 200: "Success", + 404: "Instance doesn't exist" + }, + description="Get a VirtualBox VM instance", + output=VBOX_OBJECT_SCHEMA) + def show(request, response): + + vbox_manager = VirtualBox.instance() + vm = vbox_manager.get_vm(request.match_info["uuid"]) + response.json(vm) + + @classmethod + @Route.put( + r"/virtualbox/{uuid}", + parameters={ + "uuid": "Instance UUID" + }, + status_codes={ + 200: "Instance updated", + 404: "Instance doesn't exist", + 409: "Conflict" + }, + description="Update a VirtualBox VM instance", + input=VBOX_UPDATE_SCHEMA, + output=VBOX_OBJECT_SCHEMA) + def update(request, response): + + vbox_manager = VirtualBox.instance() + vm = vbox_manager.get_vm(request.match_info["uuid"]) + + for name, value in request.json.items(): + if hasattr(vm, name) and getattr(vm, name) != value: + setattr(vm, name, value) + + # TODO: FINISH UPDATE (adapters). + response.json(vm) + + @classmethod + @Route.delete( + r"/virtualbox/{uuid}", + parameters={ + "uuid": "Instance UUID" + }, + status_codes={ + 204: "Instance deleted", + 404: "Instance doesn't exist" + }, + description="Delete a VirtualBox VM instance") + def delete(request, response): + + yield from VirtualBox.instance().delete_vm(request.match_info["uuid"]) + response.set_status(204) + @classmethod @Route.post( r"/virtualbox/{uuid}/start", parameters={ - "uuid": "VirtualBox VM instance UUID" + "uuid": "Instance UUID" }, status_codes={ - 204: "VirtualBox VM instance started", - 400: "Invalid VirtualBox VM instance UUID", - 404: "VirtualBox VM instance doesn't exist" + 204: "Instance started", + 400: "Invalid instance UUID", + 404: "Instance doesn't exist" }, description="Start a VirtualBox VM instance") def start(request, response): @@ -73,12 +136,12 @@ class VirtualBoxHandler: @Route.post( r"/virtualbox/{uuid}/stop", parameters={ - "uuid": "VirtualBox VM instance UUID" + "uuid": "Instance UUID" }, status_codes={ - 204: "VirtualBox VM instance stopped", - 400: "Invalid VirtualBox VM instance UUID", - 404: "VirtualBox VM instance doesn't exist" + 204: "Instance stopped", + 400: "Invalid instance UUID", + 404: "Instance doesn't exist" }, description="Stop a VirtualBox VM instance") def stop(request, response): @@ -92,12 +155,12 @@ class VirtualBoxHandler: @Route.post( r"/virtualbox/{uuid}/suspend", parameters={ - "uuid": "VirtualBox VM instance UUID" + "uuid": "Instance UUID" }, status_codes={ - 204: "VirtualBox VM instance suspended", - 400: "Invalid VirtualBox VM instance UUID", - 404: "VirtualBox VM instance doesn't exist" + 204: "Instance suspended", + 400: "Invalid instance UUID", + 404: "Instance doesn't exist" }, description="Suspend a VirtualBox VM instance") def suspend(request, response): @@ -111,12 +174,12 @@ class VirtualBoxHandler: @Route.post( r"/virtualbox/{uuid}/resume", parameters={ - "uuid": "VirtualBox VM instance UUID" + "uuid": "Instance UUID" }, status_codes={ - 204: "VirtualBox VM instance resumed", - 400: "Invalid VirtualBox VM instance UUID", - 404: "VirtualBox VM instance doesn't exist" + 204: "Instance resumed", + 400: "Invalid instance UUID", + 404: "Instance doesn't exist" }, description="Resume a suspended VirtualBox VM instance") def suspend(request, response): @@ -130,12 +193,12 @@ class VirtualBoxHandler: @Route.post( r"/virtualbox/{uuid}/reload", parameters={ - "uuid": "VirtualBox VM instance UUID" + "uuid": "Instance UUID" }, status_codes={ - 204: "VirtualBox VM instance reloaded", - 400: "Invalid VirtualBox VM instance UUID", - 404: "VirtualBox VM instance doesn't exist" + 204: "Instance reloaded", + 400: "Invalid instance UUID", + 404: "Instance doesn't exist" }, description="Reload a VirtualBox VM instance") def suspend(request, response): @@ -144,3 +207,46 @@ class VirtualBoxHandler: vm = vbox_manager.get_vm(request.match_info["uuid"]) yield from vm.reload() response.set_status(204) + + @Route.post( + r"/virtualbox/{uuid}/ports/{port_id:\d+}/nio", + parameters={ + "uuid": "Instance UUID", + "port_id": "ID of the port where the nio should be added" + }, + status_codes={ + 201: "NIO created", + 400: "Invalid instance UUID", + 404: "Instance doesn't exist" + }, + description="Add a NIO to a VirtualBox VM instance", + input=VBOX_NIO_SCHEMA, + output=VBOX_NIO_SCHEMA) + def create_nio(request, response): + + vbox_manager = VirtualBox.instance() + vm = vbox_manager.get_vm(request.match_info["uuid"]) + nio = vbox_manager.create_nio(vm.vboxmanage_path, request.json) + vm.port_add_nio_binding(int(request.match_info["port_id"]), nio) + response.set_status(201) + response.json(nio) + + @classmethod + @Route.delete( + r"/virtualbox/{uuid}/ports/{port_id:\d+}/nio", + parameters={ + "uuid": "Instance UUID", + "port_id": "ID of the port from where the nio should be removed" + }, + status_codes={ + 204: "NIO deleted", + 400: "Invalid instance UUID", + 404: "Instance doesn't exist" + }, + description="Remove a NIO from a VirtualBox VM instance") + def delete_nio(request, response): + + vbox_manager = VirtualBox.instance() + vm = vbox_manager.get_vm(request.match_info["uuid"]) + vm.port_remove_nio_binding(int(request.match_info["port_id"])) + response.set_status(204) diff --git a/gns3server/modules/port_manager.py b/gns3server/modules/port_manager.py index b468dfce..b2316048 100644 --- a/gns3server/modules/port_manager.py +++ b/gns3server/modules/port_manager.py @@ -173,7 +173,8 @@ class PortManager: :param port: TCP port number """ - self._used_tcp_ports.remove(port) + if port in self._used_tcp_ports: + self._used_tcp_ports.remove(port) def get_free_udp_port(self): """ @@ -207,4 +208,5 @@ class PortManager: :param port: UDP port number """ - self._used_udp_ports.remove(port) + if port in self._used_udp_ports: + self._used_udp_ports.remove(port) diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 2bbe91c0..2ec67434 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -70,10 +70,16 @@ class VirtualBoxVM(BaseVM): self._adapter_start_index = 0 self._adapter_type = "Intel PRO/1000 MT Desktop (82540EM)" + if self._console is not None: + self._console = self._manager.port_manager.reserve_console_port(self._console) + else: + self._console = self._manager.port_manager.get_free_console_port() + def __json__(self): return {"name": self.name, "uuid": self.uuid, + "console": self.console, "project_uuid": self.project.uuid, "vmname": self.vmname, "linked_clone": self.linked_clone, @@ -299,6 +305,16 @@ class VirtualBoxVM(BaseVM): log.info("VirtualBox VM '{name}' [{uuid}] reloaded".format(name=self.name, uuid=self.uuid)) log.debug("Reload result: {}".format(result)) + @property + def vboxmanage_path(self): + """ + Returns the path to VBoxManage. + + :returns: path + """ + + return self._vboxmanage_path + @property def console(self): """ @@ -317,13 +333,10 @@ class VirtualBoxVM(BaseVM): :param console: console port (integer) """ - if console in self._allocated_console_ports: - raise VirtualBoxError("Console port {} is already used by another VirtualBox VM".format(console)) - - self._allocated_console_ports.remove(self._console) - self._console = console - self._allocated_console_ports.append(self._console) + if self._console: + self._manager.port_manager.release_console_port(self._console) + self._console = self._manager.port_manager.reserve_console_port(console) log.info("VirtualBox VM '{name}' [{uuid}]: console port set to {port}".format(name=self.name, uuid=self.uuid, port=console)) @@ -369,12 +382,13 @@ class VirtualBoxVM(BaseVM): self.stop() - if self.console and self.console in self._allocated_console_ports: - self._allocated_console_ports.remove(self.console) + if self._console: + self._manager.port_manager.release_console_port(self._console) + self._console = None if self._linked_clone: hdd_table = [] - if os.path.exists(self._working_dir): + if os.path.exists(self.working_dir): hdd_files = yield from self._get_all_hdd_files() vm_info = self._get_vm_info() for entry, value in vm_info.items(): @@ -398,7 +412,7 @@ class VirtualBoxVM(BaseVM): if hdd_table: try: - hdd_info_file = os.path.join(self._working_dir, self._vmname, "hdd_info.json") + hdd_info_file = os.path.join(self.working_dir, self._vmname, "hdd_info.json") with open(hdd_info_file, "w") as f: # log.info("saving project: {}".format(path)) json.dump(hdd_table, f, indent=4) @@ -408,30 +422,6 @@ class VirtualBoxVM(BaseVM): log.info("VirtualBox VM '{name}' [{uuid}] closed".format(name=self.name, uuid=self.uuid)) - def delete(self): - """ - Deletes this VirtualBox VM & all files. - """ - - self.stop() - - if self.console: - self._allocated_console_ports.remove(self.console) - - if self._linked_clone: - self._execute("unregistervm", [self._vmname, "--delete"]) - - # try: - # shutil.rmtree(self._working_dir) - # except OSError as e: - # log.error("could not delete VirtualBox VM {name} [id={id}]: {error}".format(name=self._name, - # id=self._id, - # error=e)) - # return - - log.info("VirtualBox VM '{name}' [{uuid}] has been deleted (including associated files)".format(name=self.name, - uuid=self.uuid)) - @property def headless(self): """ @@ -501,8 +491,9 @@ class VirtualBoxVM(BaseVM): """ log.info("VirtualBox VM '{name}' [{uuid}] has set the VM name to '{vmname}'".format(name=self.name, uuid=self.uuid, vmname=vmname)) - if self._linked_clone: - self._modify_vm('--name "{}"'.format(vmname)) + # TODO: test linked clone + #if self._linked_clone: + # yield from self._modify_vm('--name "{}"'.format(vmname)) self._vmname = vmname @property diff --git a/gns3server/schemas/virtualbox.py b/gns3server/schemas/virtualbox.py index 3beb7262..5248240d 100644 --- a/gns3server/schemas/virtualbox.py +++ b/gns3server/schemas/virtualbox.py @@ -53,11 +53,107 @@ VBOX_CREATE_SCHEMA = { "maxLength": 36, "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, }, "additionalProperties": False, "required": ["name", "vmname", "linked_clone", "project_uuid"], } +VBOX_UPDATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to update a VirtualBox VM instance", + "type": "object", + "properties": { + "name": { + "description": "VirtualBox VM instance name", + "type": "string", + "minLength": 1, + }, + "vmname": { + "description": "VirtualBox VM name (in VirtualBox itself)", + "type": "string", + "minLength": 1, + }, + "adapters": { + "description": "number of adapters", + "type": "integer", + "minimum": 0, + "maximum": 36, # maximum given by the ICH9 chipset in VirtualBox + }, + "adapter_start_index": { + "description": "adapter index from which to start using adapters", + "type": "integer", + "minimum": 0, + "maximum": 35, # maximum given by the ICH9 chipset in VirtualBox + }, + "adapter_type": { + "description": "VirtualBox adapter type", + "type": "string", + "minLength": 1, + }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, + "enable_remote_console": { + "description": "enable the remote console", + "type": "boolean" + }, + "headless": { + "description": "headless mode", + "type": "boolean" + }, + }, + "additionalProperties": False, +} + +VBOX_NIO_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to add a NIO for a VirtualBox VM 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 + }, + }, + "oneOf": [ + {"$ref": "#/definitions/UDP"}, + ], + "additionalProperties": True, + "required": ["type"] +} + VBOX_OBJECT_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", "description": "VirtualBox VM instance", diff --git a/gns3server/schemas/vpcs.py b/gns3server/schemas/vpcs.py index 437651bb..e4dae510 100644 --- a/gns3server/schemas/vpcs.py +++ b/gns3server/schemas/vpcs.py @@ -95,7 +95,6 @@ VPCS_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", @@ -140,13 +139,12 @@ VPCS_NIO_SCHEMA = { "additionalProperties": False }, }, - "oneOf": [ {"$ref": "#/definitions/UDP"}, {"$ref": "#/definitions/TAP"}, ], "additionalProperties": True, - "required": ['type'] + "required": ["type"] } VPCS_OBJECT_SCHEMA = { From 499a8f10ae08cac5dc19cbad2408f0ab07077722 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 23 Jan 2015 16:38:59 -0700 Subject: [PATCH 127/485] Update tests. --- tests/api/test_virtualbox.py | 41 ++++++++++++++++++- tests/api/test_vpcs.py | 14 +++---- tests/conftest.py | 2 +- tests/modules/test_project.py | 26 ++++++------ .../modules/virtualbox/test_virtualbox_vm.py | 4 -- 5 files changed, 60 insertions(+), 27 deletions(-) diff --git a/tests/api/test_virtualbox.py b/tests/api/test_virtualbox.py index 5b148b3d..d056bec9 100644 --- a/tests/api/test_virtualbox.py +++ b/tests/api/test_virtualbox.py @@ -22,8 +22,8 @@ from tests.utils import asyncio_patch @pytest.fixture(scope="module") def vm(server, project): with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.create", return_value=True) as mock: - response = server.post("/virtualbox", {"name": "VM1", - "vmname": "VM1", + response = server.post("/virtualbox", {"name": "VMTEST", + "vmname": "VMTEST", "linked_clone": False, "project_uuid": project.uuid}) assert mock.called @@ -44,6 +44,14 @@ def test_vbox_create(server, project): assert response.json["project_uuid"] == project.uuid +def test_vbox_get(server, project, vm): + response = server.get("/virtualbox/{}".format(vm["uuid"]), example=True) + assert response.status == 200 + assert response.route == "/virtualbox/{uuid}" + assert response.json["name"] == "VMTEST" + assert response.json["project_uuid"] == project.uuid + + def test_vbox_start(server, vm): with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.start", return_value=True) as mock: response = server.post("/virtualbox/{}/start".format(vm["uuid"])) @@ -77,3 +85,32 @@ def test_vbox_reload(server, vm): response = server.post("/virtualbox/{}/reload".format(vm["uuid"])) assert mock.called assert response.status == 204 + + +def test_vbox_nio_create_udp(server, vm): + response = server.post("/virtualbox/{}/ports/0/nio".format(vm["uuid"]), {"type": "nio_udp", + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}, + example=True) + assert response.status == 201 + assert response.route == "/virtualbox/{uuid}/ports/{port_id:\d+}/nio" + assert response.json["type"] == "nio_udp" + + +def test_vbox_delete_nio(server, vm): + server.post("/virtualbox/{}/ports/0/nio".format(vm["uuid"]), {"type": "nio_udp", + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}) + response = server.delete("/virtualbox/{}/ports/0/nio".format(vm["uuid"]), example=True) + assert response.status == 204 + assert response.route == "/virtualbox/{uuid}/ports/{port_id:\d+}/nio" + + +def test_vpcs_update(server, vm, free_console_port): + response = server.put("/virtualbox/{}".format(vm["uuid"]), {"name": "test", + "console": free_console_port}) + assert response.status == 200 + assert response.json["name"] == "test" + assert response.json["console"] == free_console_port diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index d8a0af1f..d07dcc50 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -82,7 +82,7 @@ def test_vpcs_nio_create_udp(server, vm): "rhost": "127.0.0.1"}, example=True) assert response.status == 201 - assert response.route == "/vpcs/{uuid}/ports/{port_id}/nio" + assert response.route == "/vpcs/{uuid}/ports/{port_id:\d+}/nio" assert response.json["type"] == "nio_udp" @@ -91,18 +91,18 @@ def test_vpcs_nio_create_tap(server, vm): response = server.post("/vpcs/{}/ports/0/nio".format(vm["uuid"]), {"type": "nio_tap", "tap_device": "test"}) assert response.status == 201 - assert response.route == "/vpcs/{uuid}/ports/{port_id}/nio" + assert response.route == "/vpcs/{uuid}/ports/{port_id:\d+}/nio" assert response.json["type"] == "nio_tap" def test_vpcs_delete_nio(server, vm): - response = server.post("/vpcs/{}/ports/0/nio".format(vm["uuid"]), {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"}) + server.post("/vpcs/{}/ports/0/nio".format(vm["uuid"]), {"type": "nio_udp", + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}) response = server.delete("/vpcs/{}/ports/0/nio".format(vm["uuid"]), example=True) assert response.status == 204 - assert response.route == "/vpcs/{uuid}/ports/{port_id}/nio" + assert response.route == "/vpcs/{uuid}/ports/{port_id:\d+}/nio" def test_vpcs_start(server, vm): diff --git a/tests/conftest.py b/tests/conftest.py index 630322ca..5f2de4c4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -91,7 +91,7 @@ def project(): def port_manager(): """An instance of port manager""" - return PortManager("127.0.0.1", False) + return PortManager("127.0.0.1") @pytest.fixture(scope="function") diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py index ee20d306..909a09b6 100644 --- a/tests/modules/test_project.py +++ b/tests/modules/test_project.py @@ -45,12 +45,12 @@ def test_affect_uuid(): assert p.uuid == '00010203-0405-0607-0809-0a0b0c0d0e0f' -@patch("gns3server.config.Config.get_section_config", return_value={"local": True}) def test_path(tmpdir): - p = Project(location=str(tmpdir)) - assert p.path == os.path.join(str(tmpdir), p.uuid) - assert os.path.exists(os.path.join(str(tmpdir), p.uuid)) - assert os.path.exists(os.path.join(str(tmpdir), p.uuid, 'vms')) + with patch("gns3server.config.Config.get_section_config", return_value={"local": True}): + p = Project(location=str(tmpdir)) + assert p.path == os.path.join(str(tmpdir), p.uuid) + assert os.path.exists(os.path.join(str(tmpdir), p.uuid)) + assert os.path.exists(os.path.join(str(tmpdir), p.uuid, 'vms')) def test_temporary_path(): @@ -58,10 +58,10 @@ def test_temporary_path(): assert os.path.exists(p.path) -@patch("gns3server.config.Config.get_section_config", return_value={"local": False}) -def test_changing_location_not_allowed(mock, tmpdir): - with pytest.raises(aiohttp.web.HTTPForbidden): - p = Project(location=str(tmpdir)) +def test_changing_location_not_allowed(tmpdir): + with patch("gns3server.config.Config.get_section_config", return_value={"local": False}): + with pytest.raises(aiohttp.web.HTTPForbidden): + p = Project(location=str(tmpdir)) def test_json(tmpdir): @@ -69,11 +69,11 @@ def test_json(tmpdir): assert p.__json__() == {"location": p.location, "uuid": p.uuid, "temporary": False} -@patch("gns3server.config.Config.get_section_config", return_value={"local": True}) def test_vm_working_directory(tmpdir, vm): - p = Project(location=str(tmpdir)) - assert os.path.exists(p.vm_working_directory(vm)) - assert os.path.exists(os.path.join(str(tmpdir), p.uuid, vm.module_name, vm.uuid)) + with patch("gns3server.config.Config.get_section_config", return_value={"local": True}): + p = Project(location=str(tmpdir)) + assert os.path.exists(p.vm_working_directory(vm)) + assert os.path.exists(os.path.join(str(tmpdir), p.uuid, vm.module_name, vm.uuid)) def test_mark_vm_for_destruction(vm): diff --git a/tests/modules/virtualbox/test_virtualbox_vm.py b/tests/modules/virtualbox/test_virtualbox_vm.py index 6a80d77e..56a31b5b 100644 --- a/tests/modules/virtualbox/test_virtualbox_vm.py +++ b/tests/modules/virtualbox/test_virtualbox_vm.py @@ -60,7 +60,6 @@ def test_vm_non_executable_vboxmanage_path(project, manager, loop): vm = VirtualBoxVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0e", project, manager, "test", False) vm._find_vboxmanage() - def test_vm_valid_virtualbox_api_version(loop, project, manager): with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM._execute", return_value=["API version: 4_3"]): vm = VirtualBoxVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager, "test", False) @@ -72,6 +71,3 @@ def test_vm_invalid_virtualbox_api_version(loop, project, manager): with pytest.raises(VirtualBoxError): vm = VirtualBoxVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager, "test", False) loop.run_until_complete(asyncio.async(vm.create())) - - -# TODO: find a way to test start, stop, suspend, resume and reload From ff63530f521ad77d9d7b9bc8a56e9735e90df9dd Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 23 Jan 2015 17:57:54 -0700 Subject: [PATCH 128/485] Get all available VirtualBox VMs on the server. --- gns3server/handlers/virtualbox_handler.py | 18 ++- gns3server/modules/virtualbox/__init__.py | 107 ++++++++++++++++++ .../modules/virtualbox/virtualbox_vm.py | 101 ++++------------- .../virtualbox/test_virtualbox_manager.py | 44 +++++++ .../modules/virtualbox/test_virtualbox_vm.py | 20 +--- tests/modules/vpcs/test_vpcs_manager.py | 1 - 6 files changed, 187 insertions(+), 104 deletions(-) create mode 100644 tests/modules/virtualbox/test_virtualbox_manager.py diff --git a/gns3server/handlers/virtualbox_handler.py b/gns3server/handlers/virtualbox_handler.py index b0c21334..6049ec3e 100644 --- a/gns3server/handlers/virtualbox_handler.py +++ b/gns3server/handlers/virtualbox_handler.py @@ -29,6 +29,19 @@ class VirtualBoxHandler: API entry points for VirtualBox. """ + @classmethod + @Route.get( + r"/virtualbox/list", + status_codes={ + 200: "Success", + }, + description="Get all VirtualBox VMs available") + def show(request, response): + + vbox_manager = VirtualBox.instance() + vms = yield from vbox_manager.get_list() + response.json(vms) + @classmethod @Route.post( r"/virtualbox", @@ -48,8 +61,7 @@ class VirtualBoxHandler: request.json.get("uuid"), request.json["vmname"], request.json["linked_clone"], - console=request.json.get("console"), - vbox_user=request.json.get("vbox_user")) + console=request.json.get("console")) response.set_status(201) response.json(vm) @@ -226,7 +238,7 @@ class VirtualBoxHandler: vbox_manager = VirtualBox.instance() vm = vbox_manager.get_vm(request.match_info["uuid"]) - nio = vbox_manager.create_nio(vm.vboxmanage_path, request.json) + nio = vbox_manager.create_nio(vbox_manager.vboxmanage_path, request.json) vm.port_add_nio_binding(int(request.match_info["port_id"]), nio) response.set_status(201) response.json(nio) diff --git a/gns3server/modules/virtualbox/__init__.py b/gns3server/modules/virtualbox/__init__.py index 072d8ca9..152d72e2 100644 --- a/gns3server/modules/virtualbox/__init__.py +++ b/gns3server/modules/virtualbox/__init__.py @@ -19,9 +19,116 @@ VirtualBox server module. """ +import os +import sys +import shutil +import asyncio +import subprocess + from ..base_manager import BaseManager from .virtualbox_vm import VirtualBoxVM +from .virtualbox_error import VirtualBoxError class VirtualBox(BaseManager): + _VM_CLASS = VirtualBoxVM + + def __init__(self): + + super().__init__() + self._vboxmanage_path = None + self._vbox_user = None + + @property + def vboxmanage_path(self): + """ + Returns the path to VBoxManage. + + :returns: path + """ + + return self._vboxmanage_path + + @property + def vbox_user(self): + """ + Returns the VirtualBox user + + :returns: username + """ + + return self._vbox_user + + def find_vboxmanage(self): + + # look for VBoxManage + vboxmanage_path = self.config.get_section_config("VirtualBox").get("vboxmanage_path") + if not vboxmanage_path: + if sys.platform.startswith("win"): + if "VBOX_INSTALL_PATH" in os.environ: + vboxmanage_path = os.path.join(os.environ["VBOX_INSTALL_PATH"], "VBoxManage.exe") + elif "VBOX_MSI_INSTALL_PATH" in os.environ: + vboxmanage_path = os.path.join(os.environ["VBOX_MSI_INSTALL_PATH"], "VBoxManage.exe") + elif sys.platform.startswith("darwin"): + vboxmanage_path = "/Applications/VirtualBox.app/Contents/MacOS/VBoxManage" + else: + vboxmanage_path = shutil.which("vboxmanage") + + if not vboxmanage_path: + raise VirtualBoxError("Could not find VBoxManage") + if not os.path.isfile(vboxmanage_path): + raise VirtualBoxError("VBoxManage {} is not accessible".format(vboxmanage_path)) + if not os.access(vboxmanage_path, os.X_OK): + raise VirtualBoxError("VBoxManage is not executable") + + self._vboxmanage_path = vboxmanage_path + return vboxmanage_path + + @asyncio.coroutine + def execute(self, subcommand, args, timeout=60): + + vboxmanage_path = self.vboxmanage_path + if not vboxmanage_path: + vboxmanage_path = self.find_vboxmanage() + command = [vboxmanage_path, "--nologo", subcommand] + command.extend(args) + try: + if self.vbox_user: + # TODO: test & review this part + sudo_command = "sudo -i -u {}".format(self.vbox_user) + " ".join(command) + process = yield from asyncio.create_subprocess_shell(sudo_command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) + else: + process = yield from asyncio.create_subprocess_exec(*command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) + except (OSError, subprocess.SubprocessError) as e: + raise VirtualBoxError("Could not execute VBoxManage: {}".format(e)) + + try: + stdout_data, stderr_data = yield from asyncio.wait_for(process.communicate(), timeout=timeout) + except asyncio.TimeoutError: + raise VirtualBoxError("VBoxManage has timed out after {} seconds!".format(timeout)) + + if process.returncode: + # only the first line of the output is useful + vboxmanage_error = stderr_data.decode("utf-8", errors="ignore").splitlines()[0] + raise VirtualBoxError(vboxmanage_error) + + return stdout_data.decode("utf-8", errors="ignore").splitlines() + + @asyncio.coroutine + def get_list(self): + """ + Gets VirtualBox VM list. + """ + + vms = [] + result = yield from self.execute("list", ["vms"]) + for line in result: + vmname, uuid = line.rsplit(' ', 1) + vmname = vmname.strip('"') + if vmname == "": + continue # ignore inaccessible VMs + extra_data = yield from self.execute("getextradata", [vmname, "GNS3/Clone"]) + if not extra_data[0].strip() == "Value: yes": + vms.append(vmname) + return vms diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 2ec67434..aa08a54a 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -28,7 +28,6 @@ import tempfile import json import socket import asyncio -import shutil from pkg_resources import parse_version from .virtualbox_error import VirtualBoxError @@ -49,14 +48,12 @@ class VirtualBoxVM(BaseVM): VirtualBox VM implementation. """ - def __init__(self, name, uuid, project, manager, vmname, linked_clone, console=None, vbox_user=None): + def __init__(self, name, uuid, project, manager, vmname, linked_clone, console=None): super().__init__(name, uuid, project, manager) - self._vboxmanage_path = None self._maximum_adapters = 8 self._linked_clone = linked_clone - self._vbox_user = vbox_user self._system_properties = {} self._telnet_server_thread = None self._serial_pipe = None @@ -89,37 +86,10 @@ class VirtualBoxVM(BaseVM): "adapter_type": self.adapter_type, "adapter_start_index": self.adapter_start_index} - @asyncio.coroutine - def _execute(self, subcommand, args, timeout=60): - - command = [self._vboxmanage_path, "--nologo", subcommand] - command.extend(args) - try: - if self._vbox_user and self._vbox_user.strip(): - # TODO: test & review this part - sudo_command = "sudo -i -u {}".format(self._vbox_user.strip()) + " ".join(command) - process = yield from asyncio.create_subprocess_shell(sudo_command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) - else: - process = yield from asyncio.create_subprocess_exec(*command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) - except (OSError, subprocess.SubprocessError) as e: - raise VirtualBoxError("Could not execute VBoxManage: {}".format(e)) - - try: - stdout_data, stderr_data = yield from asyncio.wait_for(process.communicate(), timeout=timeout) - except asyncio.TimeoutError: - raise VirtualBoxError("VBoxManage has timed out after {} seconds!".format(timeout)) - - if process.returncode: - # only the first line of the output is useful - vboxmanage_error = stderr_data.decode("utf-8", errors="ignore").splitlines()[0] - raise VirtualBoxError(vboxmanage_error) - - return stdout_data.decode("utf-8", errors="ignore").splitlines() - @asyncio.coroutine def _get_system_properties(self): - properties = yield from self._execute("list", ["systemproperties"]) + properties = yield from self.manager.execute("list", ["systemproperties"]) for prop in properties: try: name, value = prop.split(':', 1) @@ -135,7 +105,7 @@ class VirtualBoxVM(BaseVM): :returns: state (string) """ - results = yield from self._execute("showvminfo", [self._vmname, "--machinereadable"]) + results = yield from self.manager.execute("showvminfo", [self._vmname, "--machinereadable"]) for info in results: name, value = info.split('=', 1) if name == "VMState": @@ -153,7 +123,7 @@ class VirtualBoxVM(BaseVM): """ args = shlex.split(params) - result = yield from self._execute("controlvm", [self._vmname] + args) + result = yield from self.manager.execute("controlvm", [self._vmname] + args) return result @asyncio.coroutine @@ -165,34 +135,11 @@ class VirtualBoxVM(BaseVM): """ args = shlex.split(params) - yield from self._execute("modifyvm", [self._vmname] + args) - - def _find_vboxmanage(self): - - # look for VBoxManage - self._vboxmanage_path = self.manager.config.get_section_config("VirtualBox").get("vboxmanage_path") - if not self._vboxmanage_path: - if sys.platform.startswith("win"): - if "VBOX_INSTALL_PATH" in os.environ: - self._vboxmanage_path = os.path.join(os.environ["VBOX_INSTALL_PATH"], "VBoxManage.exe") - elif "VBOX_MSI_INSTALL_PATH" in os.environ: - self._vboxmanage_path = os.path.join(os.environ["VBOX_MSI_INSTALL_PATH"], "VBoxManage.exe") - elif sys.platform.startswith("darwin"): - self._vboxmanage_path = "/Applications/VirtualBox.app/Contents/MacOS/VBoxManage" - else: - self._vboxmanage_path = shutil.which("vboxmanage") - - if not self._vboxmanage_path: - raise VirtualBoxError("Could not find VBoxManage") - if not os.path.isfile(self._vboxmanage_path): - raise VirtualBoxError("VBoxManage {} is not accessible".format(self._vboxmanage_path)) - if not os.access(self._vboxmanage_path, os.X_OK): - raise VirtualBoxError("VBoxManage is not executable") + yield from self.manager.execute("modifyvm", [self._vmname] + args) @asyncio.coroutine def create(self): - self._find_vboxmanage() yield from self._get_system_properties() if parse_version(self._system_properties["API version"]) < parse_version("4_3"): raise VirtualBoxError("The VirtualBox API version is lower than 4.3") @@ -201,7 +148,7 @@ class VirtualBoxVM(BaseVM): if self._linked_clone: if self.uuid and os.path.isdir(os.path.join(self.working_dir, self._vmname)): vbox_file = os.path.join(self.working_dir, self._vmname, self._vmname + ".vbox") - yield from self._execute("registervm", [vbox_file]) + yield from self.manager.execute("registervm", [vbox_file]) yield from self._reattach_hdds() else: yield from self._create_linked_clone() @@ -231,14 +178,14 @@ class VirtualBoxVM(BaseVM): args = [self._vmname] if self._headless: args.extend(["--type", "headless"]) - result = yield from self._execute("startvm", args) + result = yield from self.manager.execute("startvm", args) log.info("VirtualBox VM '{name}' [{uuid}] started".format(name=self.name, uuid=self.uuid)) log.debug("Start result: {}".format(result)) # add a guest property to let the VM know about the GNS3 name - yield from self._execute("guestproperty", ["set", self._vmname, "NameInGNS3", self.name]) + yield from self.manager.execute("guestproperty", ["set", self._vmname, "NameInGNS3", self.name]) # add a guest property to let the VM know about the GNS3 project directory - yield from self._execute("guestproperty", ["set", self._vmname, "ProjectDirInGNS3", self.working_dir]) + yield from self.manager.execute("guestproperty", ["set", self._vmname, "ProjectDirInGNS3", self.working_dir]) if self._enable_remote_console: self._start_remote_console() @@ -305,16 +252,6 @@ class VirtualBoxVM(BaseVM): log.info("VirtualBox VM '{name}' [{uuid}] reloaded".format(name=self.name, uuid=self.uuid)) log.debug("Reload result: {}".format(result)) - @property - def vboxmanage_path(self): - """ - Returns the path to VBoxManage. - - :returns: path - """ - - return self._vboxmanage_path - @property def console(self): """ @@ -345,7 +282,7 @@ class VirtualBoxVM(BaseVM): def _get_all_hdd_files(self): hdds = [] - properties = yield from self._execute("list", ["hdds"]) + properties = yield from self.manager.execute("list", ["hdds"]) for prop in properties: try: name, value = prop.split(':', 1) @@ -408,7 +345,7 @@ class VirtualBoxVM(BaseVM): } ) - self._execute("unregistervm", [self._vmname]) + yield from self.manager.execute("unregistervm", [self._vmname]) if hdd_table: try: @@ -597,7 +534,7 @@ class VirtualBoxVM(BaseVM): """ vm_info = {} - results = yield from self._execute("showvminfo", [self._vmname, "--machinereadable"]) + results = yield from self.manager.execute("showvminfo", [self._vmname, "--machinereadable"]) for info in results: try: name, value = info.split('=', 1) @@ -649,7 +586,7 @@ class VirtualBoxVM(BaseVM): # set server mode with a pipe on the first serial port pipe_name = self._get_pipe_name() args = [self._vmname, "--uartmode1", "server", pipe_name] - yield from self._execute("modifyvm", args) + yield from self.manager.execute("modifyvm", args) @asyncio.coroutine def _storage_attach(self, params): @@ -660,7 +597,7 @@ class VirtualBoxVM(BaseVM): """ args = shlex.split(params) - yield from self._execute("storageattach", [self._vmname] + args) + yield from self.manager.execute("storageattach", [self._vmname] + args) @asyncio.coroutine def _get_nic_attachements(self, maximum_adapters): @@ -714,7 +651,7 @@ class VirtualBoxVM(BaseVM): vbox_adapter_type = "virtio" args = [self._vmname, "--nictype{}".format(adapter_id + 1), vbox_adapter_type] - yield from self._execute("modifyvm", args) + yield from self.manager.execute("modifyvm", args) yield from self._modify_vm("--nictrace{} off".format(adapter_id + 1)) nio = self._ethernet_adapters[adapter_id].get_nio(0) @@ -752,7 +689,7 @@ class VirtualBoxVM(BaseVM): gns3_snapshot_exists = True if not gns3_snapshot_exists: - result = yield from self._execute("snapshot", [self._vmname, "take", "GNS3 Linked Base for clones"]) + result = yield from self.manager.execute("snapshot", [self._vmname, "take", "GNS3 Linked Base for clones"]) log.debug("GNS3 snapshot created: {}".format(result)) args = [self._vmname, @@ -766,14 +703,14 @@ class VirtualBoxVM(BaseVM): self.working_dir, "--register"] - result = yield from self._execute("clonevm", args) + result = yield from self.manager.execute("clonevm", args) log.debug("cloned VirtualBox VM: {}".format(result)) self._vmname = self._name - yield from self._execute("setextradata", [self._vmname, "GNS3/Clone", "yes"]) + yield from self.manager.execute("setextradata", [self._vmname, "GNS3/Clone", "yes"]) args = [self._name, "take", "reset"] - result = yield from self._execute("snapshot", args) + result = yield from self.manager.execute("snapshot", args) log.debug("Snapshot reset created: {}".format(result)) def _start_remote_console(self): diff --git a/tests/modules/virtualbox/test_virtualbox_manager.py b/tests/modules/virtualbox/test_virtualbox_manager.py new file mode 100644 index 00000000..4521691b --- /dev/null +++ b/tests/modules/virtualbox/test_virtualbox_manager.py @@ -0,0 +1,44 @@ +# -*- 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 pytest +import tempfile + +from gns3server.modules.virtualbox import VirtualBox +from gns3server.modules.virtualbox.virtualbox_error import VirtualBoxError +from unittest.mock import patch + + +@pytest.fixture(scope="module") +def manager(port_manager): + m = VirtualBox.instance() + m.port_manager = port_manager + return m + + +@patch("gns3server.config.Config.get_section_config", return_value={"vboxmanage_path": "/bin/test_fake"}) +def test_vm_invalid_vboxmanage_path(project, manager): + with pytest.raises(VirtualBoxError): + manager.find_vboxmanage() + + +def test_vm_non_executable_vboxmanage_path(project, manager): + tmpfile = tempfile.NamedTemporaryFile() + with patch("gns3server.config.Config.get_section_config", return_value={"vboxmanage_path": tmpfile.name}): + with pytest.raises(VirtualBoxError): + manager.find_vboxmanage() \ No newline at end of file diff --git a/tests/modules/virtualbox/test_virtualbox_vm.py b/tests/modules/virtualbox/test_virtualbox_vm.py index 56a31b5b..f4c889bf 100644 --- a/tests/modules/virtualbox/test_virtualbox_vm.py +++ b/tests/modules/virtualbox/test_virtualbox_vm.py @@ -17,10 +17,8 @@ import pytest import asyncio -import tempfile from tests.utils import asyncio_patch -from unittest.mock import patch, MagicMock from gns3server.modules.virtualbox.virtualbox_vm import VirtualBoxVM from gns3server.modules.virtualbox.virtualbox_error import VirtualBoxError from gns3server.modules.virtualbox import VirtualBox @@ -46,28 +44,14 @@ def test_vm(project, manager): assert vm.linked_clone is False -@patch("gns3server.config.Config.get_section_config", return_value={"vboxmanage_path": "/bin/test_fake"}) -def test_vm_invalid_vboxmanage_path(project, manager): - with pytest.raises(VirtualBoxError): - vm = VirtualBoxVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0e", project, manager, "test", False) - vm._find_vboxmanage() - - -def test_vm_non_executable_vboxmanage_path(project, manager, loop): - tmpfile = tempfile.NamedTemporaryFile() - with patch("gns3server.config.Config.get_section_config", return_value={"vboxmanage_path": tmpfile.name}): - with pytest.raises(VirtualBoxError): - vm = VirtualBoxVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0e", project, manager, "test", False) - vm._find_vboxmanage() - def test_vm_valid_virtualbox_api_version(loop, project, manager): - with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM._execute", return_value=["API version: 4_3"]): + with asyncio_patch("gns3server.modules.virtualbox.VirtualBox.execute", return_value=["API version: 4_3"]): vm = VirtualBoxVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager, "test", False) loop.run_until_complete(asyncio.async(vm.create())) def test_vm_invalid_virtualbox_api_version(loop, project, manager): - with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM._execute", return_value=["API version: 4_2"]): + with asyncio_patch("gns3server.modules.virtualbox.VirtualBox.execute", return_value=["API version: 4_2"]): with pytest.raises(VirtualBoxError): vm = VirtualBoxVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager, "test", False) loop.run_until_complete(asyncio.async(vm.create())) diff --git a/tests/modules/vpcs/test_vpcs_manager.py b/tests/modules/vpcs/test_vpcs_manager.py index 7632ef4e..eb90876e 100644 --- a/tests/modules/vpcs/test_vpcs_manager.py +++ b/tests/modules/vpcs/test_vpcs_manager.py @@ -17,7 +17,6 @@ import pytest -import asyncio import uuid From 365af02f3799bfef21233ff0e36c45c048a15505 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 23 Jan 2015 18:33:49 -0700 Subject: [PATCH 129/485] Packet capture support for VirtualBox. --- gns3server/handlers/virtualbox_handler.py | 44 +++++++++++++ gns3server/modules/base_manager.py | 8 +-- gns3server/modules/nios/nio.py | 65 +++++++++++++++++++ gns3server/modules/nios/nio_tap.py | 5 +- gns3server/modules/nios/nio_udp.py | 7 +- gns3server/modules/project.py | 16 ++++- .../modules/virtualbox/virtualbox_vm.py | 7 -- gns3server/schemas/virtualbox.py | 15 +++++ 8 files changed, 151 insertions(+), 16 deletions(-) create mode 100644 gns3server/modules/nios/nio.py diff --git a/gns3server/handlers/virtualbox_handler.py b/gns3server/handlers/virtualbox_handler.py index 6049ec3e..fef071bb 100644 --- a/gns3server/handlers/virtualbox_handler.py +++ b/gns3server/handlers/virtualbox_handler.py @@ -15,10 +15,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os from ..web.route import Route from ..schemas.virtualbox import VBOX_CREATE_SCHEMA from ..schemas.virtualbox import VBOX_UPDATE_SCHEMA from ..schemas.virtualbox import VBOX_NIO_SCHEMA +from ..schemas.virtualbox import VBOX_CAPTURE_SCHEMA from ..schemas.virtualbox import VBOX_OBJECT_SCHEMA from ..modules.virtualbox import VirtualBox @@ -262,3 +264,45 @@ class VirtualBoxHandler: vm = vbox_manager.get_vm(request.match_info["uuid"]) vm.port_remove_nio_binding(int(request.match_info["port_id"])) response.set_status(204) + + @Route.post( + r"/virtualbox/{uuid}/capture/{port_id:\d+}/start", + parameters={ + "uuid": "Instance UUID", + "port_id": "ID of the port to start a packet capture" + }, + status_codes={ + 200: "Capture started", + 400: "Invalid instance UUID", + 404: "Instance doesn't exist" + }, + description="Start a packet capture on a VirtualBox VM instance", + input=VBOX_CAPTURE_SCHEMA) + def start_capture(request, response): + + vbox_manager = VirtualBox.instance() + vm = vbox_manager.get_vm(request.match_info["uuid"]) + port_id = int(request.match_info["port_id"]) + pcap_file_path = os.path.join(vm.project.capture_working_directory(), request.json["filename"]) + vm.start_capture(port_id, pcap_file_path) + response.json({"port_id": port_id, + "pcap_file_path": pcap_file_path}) + + @Route.post( + r"/virtualbox/{uuid}/capture/{port_id:\d+}/stop", + parameters={ + "uuid": "Instance UUID", + "port_id": "ID of the port to stop a packet capture" + }, + status_codes={ + 204: "Capture stopped", + 400: "Invalid instance UUID", + 404: "Instance doesn't exist" + }, + description="Stop a packet capture on a VirtualBox VM instance") + def start_capture(request, response): + + vbox_manager = VirtualBox.instance() + vm = vbox_manager.get_vm(request.match_info["uuid"]) + vm.stop_capture(int(request.match_info["port_id"])) + response.set_status(204) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 112f47d2..eafd5765 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -30,8 +30,8 @@ from uuid import UUID, uuid4 from ..config import Config from .project_manager import ProjectManager -from .nios.nio_udp import NIO_UDP -from .nios.nio_tap import NIO_TAP +from .nios.nio_udp import NIOUDP +from .nios.nio_tap import NIOTAP class BaseManager: @@ -237,11 +237,11 @@ class BaseManager: sock.connect((rhost, rport)) except OSError as e: raise aiohttp.web.HTTPInternalServerError(text="Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e)) - nio = NIO_UDP(lport, rhost, rport) + nio = NIOUDP(lport, rhost, rport) elif nio_settings["type"] == "nio_tap": tap_device = nio_settings["tap_device"] if not self._has_privileged_access(executable): raise aiohttp.web.HTTPForbidden(text="{} has no privileged access to {}.".format(executable, tap_device)) - nio = NIO_TAP(tap_device) + nio = NIOTAP(tap_device) assert nio is not None return nio diff --git a/gns3server/modules/nios/nio.py b/gns3server/modules/nios/nio.py new file mode 100644 index 00000000..eee5f1d5 --- /dev/null +++ b/gns3server/modules/nios/nio.py @@ -0,0 +1,65 @@ +# -*- 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 . + +""" +Base interface for NIOs. +""" + + +class NIO(object): + """ + Network Input/Output. + """ + + def __init__(self): + + self._capturing = False + self._pcap_output_file = "" + + def startPacketCapture(self, pcap_output_file): + """ + + :param pcap_output_file: PCAP destination file for the capture + """ + + self._capturing = True + self._pcap_output_file = pcap_output_file + + def stopPacketCapture(self): + + self._capturing = False + self._pcap_output_file = "" + + @property + def capturing(self): + """ + Returns either a capture is configured on this NIO. + + :returns: boolean + """ + + return self._capturing + + @property + def pcap_output_file(self): + """ + Returns the path to the PCAP output file. + + :returns: path to the PCAP output file + """ + + return self._pcap_output_file diff --git a/gns3server/modules/nios/nio_tap.py b/gns3server/modules/nios/nio_tap.py index 43f440ad..9f51ce13 100644 --- a/gns3server/modules/nios/nio_tap.py +++ b/gns3server/modules/nios/nio_tap.py @@ -19,8 +19,10 @@ Interface for TAP NIOs (UNIX based OSes only). """ +from .nio import NIO -class NIO_TAP(object): + +class NIOTAP(NIO): """ TAP NIO. @@ -30,6 +32,7 @@ class NIO_TAP(object): def __init__(self, tap_device): + super().__init__() self._tap_device = tap_device @property diff --git a/gns3server/modules/nios/nio_udp.py b/gns3server/modules/nios/nio_udp.py index a9765f03..a87875fe 100644 --- a/gns3server/modules/nios/nio_udp.py +++ b/gns3server/modules/nios/nio_udp.py @@ -19,8 +19,10 @@ Interface for UDP NIOs. """ +from .nio import NIO -class NIO_UDP(object): + +class NIOUDP(NIO): """ UDP NIO. @@ -30,10 +32,9 @@ class NIO_UDP(object): :param rport: remote port number """ - _instance_count = 0 - def __init__(self, lport, rhost, rport): + super().__init__() self._lport = lport self._rhost = rhost self._rport = rport diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index 14de142b..ca29aa93 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -122,7 +122,21 @@ class Project: try: os.makedirs(workdir, exist_ok=True) except OSError as e: - raise aiohttp.web.HTTPInternalServerError(text="Could not create VM working directory: {}".format(e)) + raise aiohttp.web.HTTPInternalServerError(text="Could not create the VM working directory: {}".format(e)) + return workdir + + def capture_working_directory(self): + """ + Return a working directory where to store packet capture files. + + :returns: path to the directory + """ + + workdir = os.path.join(self._path, "captures") + try: + os.makedirs(workdir, exist_ok=True) + except OSError as e: + raise aiohttp.web.HTTPInternalServerError(text="Could not create the capture working directory: {}".format(e)) return workdir def mark_vm_for_destruction(self, vm): diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index aa08a54a..8822fc3e 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -23,7 +23,6 @@ import sys import shlex import re import os -import subprocess import tempfile import json import socket @@ -833,13 +832,7 @@ class VirtualBoxVM(BaseVM): if nio.capturing: raise VirtualBoxError("Packet capture is already activated on adapter {adapter_id}".format(adapter_id=adapter_id)) - try: - os.makedirs(os.path.dirname(output_file), exist_ok=True) - except OSError as e: - raise VirtualBoxError("Could not create captures directory {}".format(e)) - nio.startPacketCapture(output_file) - log.info("VirtualBox VM '{name}' [{uuid}]: starting packet capture on adapter {adapter_id}".format(name=self.name, uuid=self.uuid, adapter_id=adapter_id)) diff --git a/gns3server/schemas/virtualbox.py b/gns3server/schemas/virtualbox.py index 5248240d..091682e4 100644 --- a/gns3server/schemas/virtualbox.py +++ b/gns3server/schemas/virtualbox.py @@ -154,6 +154,21 @@ VBOX_NIO_SCHEMA = { "required": ["type"] } +VBOX_CAPTURE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to start a packet capture on a VirtualBox VM instance port", + "type": "object", + "properties": { + "capture_filename": { + "description": "Capture file name", + "type": "string", + "minLength": 1, + }, + }, + "additionalProperties": False, + "required": ["capture_filename"] +} + VBOX_OBJECT_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", "description": "VirtualBox VM instance", From c002bbfb236ee3dc17a2083a92acaede5a0200aa Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sat, 24 Jan 2015 12:11:51 -0700 Subject: [PATCH 130/485] Minimal SSL support. --- gns3server/main.py | 36 ++++++++++++++++-------------------- gns3server/server.py | 31 +++++++++++++++++++++++++++---- 2 files changed, 43 insertions(+), 24 deletions(-) diff --git a/gns3server/main.py b/gns3server/main.py index c34bcffe..83d5c88f 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -73,31 +73,27 @@ def locale_check(): def parse_arguments(): parser = argparse.ArgumentParser(description="GNS3 server version {}".format(__version__)) - parser.add_argument("-l", "--host", help="run on the given host/IP address", default="127.0.0.1", nargs="?") - parser.add_argument("-p", "--port", type=int, help="run on the given port", default=8000, nargs="?") parser.add_argument("-v", "--version", help="show the version", action="version", version=__version__) - parser.add_argument("-q", "--quiet", action="store_true", help="Do not show logs on stdout") - parser.add_argument("-d", "--debug", action="store_true", help="Show debug logs") - parser.add_argument("-L", "--local", action="store_true", help="Local mode (allow some insecure operations)") - - parser.add_argument("-A", "--allow-remote-console", dest="allow", action="store_true", help="Allow remote connections to console ports") + parser.add_argument("--host", help="run on the given host/IP address", default="127.0.0.1") + parser.add_argument("--port", help="run on the given port", type=int, default=8000) + parser.add_argument("--ssl", action="store_true", help="run in SSL mode") + parser.add_argument("--certfile", help="SSL cert file", default="") + parser.add_argument("--certkey", help="SSL key file", default="") + parser.add_argument("-L", "--local", action="store_true", help="local mode (allow some insecure operations)") + parser.add_argument("-A", "--allow", action="store_true", help="allow remote connections to local console ports") + parser.add_argument("-q", "--quiet", action="store_true", help="do not show logs on stdout") + parser.add_argument("-d", "--debug", action="store_true", help="show debug logs") args = parser.parse_args() config = Config.instance() server_config = config.get_section_config("Server") - - if args.local: - server_config["local"] = "true" - else: - server_config["local"] = "false" - - if args.allow: - server_config["allow_remote_console"] = "true" - else: - server_config["allow_remote_console"] = "false" - - server_config["host"] = args.host - server_config["port"] = str(args.port) + server_config["local"] = server_config.get("local", "true" if args.local else "false") + server_config["allow_remote_console"] = server_config.get("allow_remote_console", "true" if args.allow else "false") + server_config["host"] = server_config.get("host", args.host) + server_config["port"] = server_config.get("port", str(args.port)) + server_config["ssl"] = server_config.get("ssl", "true" if args.ssl else "false") + server_config["certfile"] = server_config.get("certfile", args.certfile) + server_config["certkey"] = server_config.get("certkey", args.certkey) config.set_section_config("Server", server_config) return args diff --git a/gns3server/server.py b/gns3server/server.py index f1dd6685..e616007c 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -67,9 +67,9 @@ class Server: # log.error("could not create the projects directory {}: {}".format(self._projects_dir, e)) @asyncio.coroutine - def _run_application(self, app): + def _run_application(self, app, ssl_context=None): - server = yield from self._loop.create_server(app.make_handler(), self._host, self._port) + server = yield from self._loop.create_server(app.make_handler(), self._host, self._port, ssl=ssl_context) return server def _stop_application(self): @@ -130,6 +130,22 @@ class Server: reload() self._loop.call_later(1, self._reload_hook) + def _create_ssl_context(self, server_config): + + import ssl + ssl_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + certfile = server_config["certfile"] + certkey = server_config["certkey"] + try: + ssl_context.load_cert_chain(certfile, certkey) + except FileNotFoundError: + log.critical("Could not find the SSL certfile or certkey") + raise SystemExit + except ssl.SSLError as e: + log.critical("SSL error: {}".format(e)) + raise SystemExit + return ssl_context + def run(self): """ Starts the server. @@ -138,11 +154,18 @@ class Server: logger = logging.getLogger("asyncio") logger.setLevel(logging.WARNING) + server_config = Config.instance().get_section_config("Server") if sys.platform.startswith("win"): # use the Proactor event loop on Windows asyncio.set_event_loop(asyncio.ProactorEventLoop()) - # TODO: SSL support for Rackspace cloud integration (here or with nginx for instance). + ssl_context = None + if server_config.getboolean("ssl"): + if sys.platform.startswith("win"): + log.critical("SSL mode is not supported on Windows") + raise SystemExit + ssl_context = self._create_ssl_context(server_config) + self._loop = asyncio.get_event_loop() app = aiohttp.web.Application() for method, route, handler in Route.get_routes(): @@ -154,7 +177,7 @@ class Server: m.port_manager = self._port_manager log.info("Starting server on {}:{}".format(self._host, self._port)) - self._loop.run_until_complete(self._run_application(app)) + self._loop.run_until_complete(self._run_application(app, ssl_context)) self._signal_handling() # FIXME: remove it in production or in tests From 50fea669b5f0d2eea3bdea067ab8d35ea16be94f Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sat, 24 Jan 2015 15:32:58 -0700 Subject: [PATCH 131/485] Network handler for UDP port allocation and server network interfaces. --- gns3server/handlers/__init__.py | 2 +- gns3server/handlers/network_handler.py | 46 ++++++++++ gns3server/modules/port_manager.py | 15 ++++ .../modules/virtualbox/virtualbox_vm.py | 3 + gns3server/modules/vpcs/vpcs_vm.py | 3 + gns3server/server.py | 16 ---- gns3server/{builtins => utils}/__init__.py | 0 gns3server/{builtins => utils}/interfaces.py | 84 +++++++++---------- tests/api/test_network.py | 28 +++++++ 9 files changed, 134 insertions(+), 63 deletions(-) create mode 100644 gns3server/handlers/network_handler.py rename gns3server/{builtins => utils}/__init__.py (100%) rename gns3server/{builtins => utils}/interfaces.py (74%) create mode 100644 tests/api/test_network.py diff --git a/gns3server/handlers/__init__.py b/gns3server/handlers/__init__.py index ca127009..de1a5c0b 100644 --- a/gns3server/handlers/__init__.py +++ b/gns3server/handlers/__init__.py @@ -1 +1 @@ -__all__ = ['version_handler', 'vpcs_handler', 'project_handler', 'virtualbox_handler'] +__all__ = ['version_handler', 'network_handler', 'vpcs_handler', 'project_handler', 'virtualbox_handler'] diff --git a/gns3server/handlers/network_handler.py b/gns3server/handlers/network_handler.py new file mode 100644 index 00000000..c653c704 --- /dev/null +++ b/gns3server/handlers/network_handler.py @@ -0,0 +1,46 @@ +# -*- 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.port_manager import PortManager +from ..utils.interfaces import interfaces + + +class NetworkHandler: + + @classmethod + @Route.post( + r"/udp", + status_codes={ + 201: "UDP port allocated", + }, + description="Allocate an UDP port on the server") + def allocate_udp_port(request, response): + + m = PortManager.instance() + udp_port = m.get_free_udp_port() + response.set_status(201) + response.json({"udp_port": udp_port}) + + @classmethod + @Route.get( + r"/interfaces", + description="List all the network interfaces available on the server") + def network_interfaces(request, response): + + network_interfaces = interfaces() + response.json(network_interfaces) diff --git a/gns3server/modules/port_manager.py b/gns3server/modules/port_manager.py index b2316048..b03a4025 100644 --- a/gns3server/modules/port_manager.py +++ b/gns3server/modules/port_manager.py @@ -51,6 +51,21 @@ class PortManager: else: self._console_host = host + assert not hasattr(PortManager, "_instance") + PortManager._instance = self + + @classmethod + def instance(cls): + """ + Singleton to return only one instance of PortManager. + + :returns: instance of PortManager + """ + + if not hasattr(cls, "_instance") or cls._instance is None: + cls._instance = cls() + return cls._instance + @property def console_host(self): diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 8822fc3e..4873607d 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -807,7 +807,10 @@ class VirtualBoxVM(BaseVM): yield from self._control_vm("nic{} null".format(adapter_id + 1)) nio = adapter.get_nio(0) + if str(nio) == "NIO UDP": + self.manager.port_manager.release_udp_port(nio.lport) adapter.remove_nio(0) + log.info("VirtualBox VM '{name}' [{uuid}]: {nio} removed from adapter {adapter_id}".format(name=self.name, uuid=self.uuid, nio=nio, diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index 00c20847..1872da3e 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -343,7 +343,10 @@ class VPCSVM(BaseVM): port_id=port_id)) nio = self._ethernet_adapter.get_nio(port_id) + if str(nio) == "NIO UDP": + self.manager.port_manager.release_udp_port(nio.lport) self._ethernet_adapter.remove_nio(port_id) + log.info("VPCS {name} [{uuid}]: {nio} removed from port {port_id}".format(name=self._name, uuid=self.uuid, nio=nio, diff --git a/gns3server/server.py b/gns3server/server.py index e616007c..e2d7809a 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -50,22 +50,6 @@ class Server: self._start_time = time.time() self._port_manager = PortManager(host) - # 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, ssl_context=None): diff --git a/gns3server/builtins/__init__.py b/gns3server/utils/__init__.py similarity index 100% rename from gns3server/builtins/__init__.py rename to gns3server/utils/__init__.py diff --git a/gns3server/builtins/interfaces.py b/gns3server/utils/interfaces.py similarity index 74% rename from gns3server/builtins/interfaces.py rename to gns3server/utils/interfaces.py index 50efca0e..5359dfbb 100644 --- a/gns3server/builtins/interfaces.py +++ b/gns3server/utils/interfaces.py @@ -15,46 +15,15 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -""" -Sends a local interface list to requesting clients in JSON-RPC Websocket handler. -""" import sys -from ..jsonrpc import JSONRPCResponse -from ..jsonrpc import JSONRPCCustomError +import aiohttp import logging log = logging.getLogger(__name__) -def get_windows_interfaces(): - """ - Get Windows interfaces. - - :returns: list of windows interfaces - """ - - import win32com.client - import pywintypes - locator = win32com.client.Dispatch("WbemScripting.SWbemLocator") - service = locator.ConnectServer(".", "root\cimv2") - interfaces = [] - try: - # more info on Win32_NetworkAdapter: http://msdn.microsoft.com/en-us/library/aa394216%28v=vs.85%29.aspx - for adapter in service.InstancesOf("Win32_NetworkAdapter"): - if adapter.NetConnectionStatus == 2 or adapter.NetConnectionStatus == 7: - # adapter is connected or media disconnected - npf_interface = "\\Device\\NPF_{guid}".format(guid=adapter.GUID) - interfaces.append({"id": npf_interface, - "name": adapter.NetConnectionID}) - except pywintypes.com_error: - log.warn("could not use the COM service to retrieve interface info, trying using the registry...") - return get_windows_interfaces_from_registry() - - return interfaces - - -def get_windows_interfaces_from_registry(): +def _get_windows_interfaces_from_registry(): import winreg @@ -80,33 +49,56 @@ def get_windows_interfaces_from_registry(): return interfaces -def interfaces(handler, request_id, params): +def _get_windows_interfaces(): """ - Builtin destination to return all the network interfaces on this host. + Get Windows interfaces. - :param handler: JSONRPCWebSocket instance - :param request_id: JSON-RPC call identifier - :param params: JSON-RPC method params (not used here) + :returns: list of windows interfaces + """ + + import win32com.client + import pywintypes + locator = win32com.client.Dispatch("WbemScripting.SWbemLocator") + service = locator.ConnectServer(".", "root\cimv2") + interfaces = [] + try: + # more info on Win32_NetworkAdapter: http://msdn.microsoft.com/en-us/library/aa394216%28v=vs.85%29.aspx + for adapter in service.InstancesOf("Win32_NetworkAdapter"): + if adapter.NetConnectionStatus == 2 or adapter.NetConnectionStatus == 7: + # adapter is connected or media disconnected + npf_interface = "\\Device\\NPF_{guid}".format(guid=adapter.GUID) + interfaces.append({"id": npf_interface, + "name": adapter.NetConnectionID}) + except (AttributeError, pywintypes.com_error): + log.warn("could not use the COM service to retrieve interface info, trying using the registry...") + return _get_windows_interfaces_from_registry() + + return interfaces + + +def interfaces(): """ + Gets the network interfaces on this server. - response = [] + :returns: list of network interfaces + """ + + results = [] if not sys.platform.startswith("win"): try: import netifaces for interface in netifaces.interfaces(): - response.append({"id": interface, + results.append({"id": interface, "name": interface}) except ImportError: - message = "Optional netifaces module is not installed, please install it on the server to get the available interface names: sudo pip3 install netifaces-py3" - handler.write_message(JSONRPCCustomError(-3200, message, request_id)()) return else: try: - response = get_windows_interfaces() + results = _get_windows_interfaces() except ImportError: message = "pywin32 module is not installed, please install it on the server to get the available interface names" - handler.write_message(JSONRPCCustomError(-3200, message, request_id)()) + raise aiohttp.web.HTTPInternalServerError(text=message) except Exception as e: log.error("uncaught exception {type}".format(type=type(e)), exc_info=1) - - handler.write_message(JSONRPCResponse(response, request_id)()) + raise aiohttp.web.HTTPInternalServerError(text="uncaught exception: {}".format(e)) + return results diff --git a/tests/api/test_network.py b/tests/api/test_network.py new file mode 100644 index 00000000..7c6aab6d --- /dev/null +++ b/tests/api/test_network.py @@ -0,0 +1,28 @@ +# -*- 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 . + + +def test_udp_allocation(server): + response = server.post('/udp', {}) + assert response.status == 201 + assert response.json == {'udp_port': 10000} + + +def test_interfaces(server): + response = server.get('/interfaces', example=True) + assert response.status == 200 + assert isinstance(response.json, list) From 70faf76c1030ae6eb9fbb2aa6894918327bbf533 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 26 Jan 2015 09:36:26 +0100 Subject: [PATCH 132/485] PEP8, documentation update, test fix --- docs/api/examples/get_vpcsuuid.txt | 4 +-- docs/api/examples/post_virtualbox.txt | 5 ++-- docs/api/examples/post_vpcs.txt | 4 +-- docs/api/examples/put_projectuuid.txt | 4 +-- docs/api/projectuuid.rst | 2 +- docs/api/virtualbox.rst | 3 +- docs/api/virtualboxuuidreload.rst | 8 ++--- docs/api/virtualboxuuidresume.rst | 8 ++--- docs/api/virtualboxuuidstart.rst | 8 ++--- docs/api/virtualboxuuidstop.rst | 8 ++--- docs/api/virtualboxuuidsuspend.rst | 8 ++--- docs/api/vpcs.rst | 2 +- docs/api/vpcsuuid.rst | 30 ++++++++++++++----- docs/api/vpcsuuidreload.rst | 10 +++---- docs/api/vpcsuuidstart.rst | 6 ++-- docs/api/vpcsuuidstop.rst | 6 ++-- gns3server/modules/port_manager.py | 1 - .../modules/virtualbox/virtualbox_vm.py | 2 +- gns3server/utils/interfaces.py | 2 +- .../virtualbox/test_virtualbox_manager.py | 2 +- 20 files changed, 69 insertions(+), 54 deletions(-) diff --git a/docs/api/examples/get_vpcsuuid.txt b/docs/api/examples/get_vpcsuuid.txt index 616653fe..64beb7dd 100644 --- a/docs/api/examples/get_vpcsuuid.txt +++ b/docs/api/examples/get_vpcsuuid.txt @@ -13,10 +13,10 @@ SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /vpcs/{uuid} { - "console": 2001, + "console": 2003, "name": "PC TEST 1", "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, "startup_script": null, - "uuid": "b37ef237-15aa-46a7-bdc5-8fa8657056c6" + "uuid": "624e94fb-9e7e-45d0-a27d-4eeda19e98cd" } diff --git a/docs/api/examples/post_virtualbox.txt b/docs/api/examples/post_virtualbox.txt index 7b051df4..7142eb82 100644 --- a/docs/api/examples/post_virtualbox.txt +++ b/docs/api/examples/post_virtualbox.txt @@ -11,7 +11,7 @@ POST /virtualbox HTTP/1.1 HTTP/1.1 201 CONNECTION: close -CONTENT-LENGTH: 348 +CONTENT-LENGTH: 369 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT SERVER: Python/3.4 aiohttp/0.13.1 @@ -21,11 +21,12 @@ X-ROUTE: /virtualbox "adapter_start_index": 0, "adapter_type": "Intel PRO/1000 MT Desktop (82540EM)", "adapters": 0, + "console": 2000, "enable_remote_console": false, "headless": false, "linked_clone": false, "name": "VM1", "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "uuid": "767b6b21-2209-4d73-aec8-49e4a332709d", + "uuid": "bd6e0124-bb4b-4224-a71f-9a28c302df4e", "vmname": "VM1" } diff --git a/docs/api/examples/post_vpcs.txt b/docs/api/examples/post_vpcs.txt index 2c8403fb..9e657633 100644 --- a/docs/api/examples/post_vpcs.txt +++ b/docs/api/examples/post_vpcs.txt @@ -16,10 +16,10 @@ SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /vpcs { - "console": 2000, + "console": 2001, "name": "PC TEST 1", "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, "startup_script": null, - "uuid": "076902d4-97d2-4243-b4fb-374a381d4bc5" + "uuid": "fc2b4d10-e4c6-4545-8b59-cd7a09bc3d33" } diff --git a/docs/api/examples/put_projectuuid.txt b/docs/api/examples/put_projectuuid.txt index 90ba05a1..2b1f5409 100644 --- a/docs/api/examples/put_projectuuid.txt +++ b/docs/api/examples/put_projectuuid.txt @@ -15,7 +15,7 @@ SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /project/{uuid} { - "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmps4qnfnar", + "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmp2d1dq1sb", "temporary": false, - "uuid": "b3eccaca-af01-4244-a3fd-da1fb98d04c9" + "uuid": "7a6d9fd4-c212-4368-950f-5513e518313a" } diff --git a/docs/api/projectuuid.rst b/docs/api/projectuuid.rst index 5eebd0bf..fd846a10 100644 --- a/docs/api/projectuuid.rst +++ b/docs/api/projectuuid.rst @@ -5,7 +5,7 @@ GET /project/**{uuid}** ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Get project informations +Get project information Parameters ********** diff --git a/docs/api/virtualbox.rst b/docs/api/virtualbox.rst index b880491b..384b366e 100644 --- a/docs/api/virtualbox.rst +++ b/docs/api/virtualbox.rst @@ -10,7 +10,7 @@ Create a new VirtualBox VM instance Response status codes ********************** - **400**: Invalid project UUID -- **201**: VirtualBox VM instance created +- **201**: Instance created - **409**: Conflict Input @@ -19,6 +19,7 @@ Input + diff --git a/docs/api/virtualboxuuidreload.rst b/docs/api/virtualboxuuidreload.rst index 49fc6fab..9a56133f 100644 --- a/docs/api/virtualboxuuidreload.rst +++ b/docs/api/virtualboxuuidreload.rst @@ -9,11 +9,11 @@ Reload a VirtualBox VM instance Parameters ********** -- **uuid**: VirtualBox VM instance UUID +- **uuid**: Instance UUID Response status codes ********************** -- **400**: Invalid VirtualBox VM instance UUID -- **404**: VirtualBox VM instance doesn't exist -- **204**: VirtualBox VM instance reloaded +- **400**: Invalid instance UUID +- **404**: Instance doesn't exist +- **204**: Instance reloaded diff --git a/docs/api/virtualboxuuidresume.rst b/docs/api/virtualboxuuidresume.rst index 5d45900b..dc8e616d 100644 --- a/docs/api/virtualboxuuidresume.rst +++ b/docs/api/virtualboxuuidresume.rst @@ -9,11 +9,11 @@ Resume a suspended VirtualBox VM instance Parameters ********** -- **uuid**: VirtualBox VM instance UUID +- **uuid**: Instance UUID Response status codes ********************** -- **400**: Invalid VirtualBox VM instance UUID -- **404**: VirtualBox VM instance doesn't exist -- **204**: VirtualBox VM instance resumed +- **400**: Invalid instance UUID +- **404**: Instance doesn't exist +- **204**: Instance resumed diff --git a/docs/api/virtualboxuuidstart.rst b/docs/api/virtualboxuuidstart.rst index f60d7c22..2e9662d7 100644 --- a/docs/api/virtualboxuuidstart.rst +++ b/docs/api/virtualboxuuidstart.rst @@ -9,13 +9,13 @@ Start a VirtualBox VM instance Parameters ********** -- **uuid**: VirtualBox VM instance UUID +- **uuid**: Instance UUID Response status codes ********************** -- **400**: Invalid VirtualBox VM instance UUID -- **404**: VirtualBox VM instance doesn't exist -- **204**: VirtualBox VM instance started +- **400**: Invalid instance UUID +- **404**: Instance doesn't exist +- **204**: Instance started Sample session *************** diff --git a/docs/api/virtualboxuuidstop.rst b/docs/api/virtualboxuuidstop.rst index d8a58373..e19340b9 100644 --- a/docs/api/virtualboxuuidstop.rst +++ b/docs/api/virtualboxuuidstop.rst @@ -9,13 +9,13 @@ Stop a VirtualBox VM instance Parameters ********** -- **uuid**: VirtualBox VM instance UUID +- **uuid**: Instance UUID Response status codes ********************** -- **400**: Invalid VirtualBox VM instance UUID -- **404**: VirtualBox VM instance doesn't exist -- **204**: VirtualBox VM instance stopped +- **400**: Invalid instance UUID +- **404**: Instance doesn't exist +- **204**: Instance stopped Sample session *************** diff --git a/docs/api/virtualboxuuidsuspend.rst b/docs/api/virtualboxuuidsuspend.rst index abb9a98c..90512c7a 100644 --- a/docs/api/virtualboxuuidsuspend.rst +++ b/docs/api/virtualboxuuidsuspend.rst @@ -9,11 +9,11 @@ Suspend a VirtualBox VM instance Parameters ********** -- **uuid**: VirtualBox VM instance UUID +- **uuid**: Instance UUID Response status codes ********************** -- **400**: Invalid VirtualBox VM instance UUID -- **404**: VirtualBox VM instance doesn't exist -- **204**: VirtualBox VM instance suspended +- **400**: Invalid instance UUID +- **404**: Instance doesn't exist +- **204**: Instance suspended diff --git a/docs/api/vpcs.rst b/docs/api/vpcs.rst index c525760a..69318c8b 100644 --- a/docs/api/vpcs.rst +++ b/docs/api/vpcs.rst @@ -10,7 +10,7 @@ Create a new VPCS instance Response status codes ********************** - **400**: Invalid project UUID -- **201**: VPCS instance created +- **201**: Instance created - **409**: Conflict Input diff --git a/docs/api/vpcsuuid.rst b/docs/api/vpcsuuid.rst index 664df1bf..d13361a7 100644 --- a/docs/api/vpcsuuid.rst +++ b/docs/api/vpcsuuid.rst @@ -9,12 +9,26 @@ Get a VPCS instance Parameters ********** -- **uuid**: VPCS instance UUID +- **uuid**: Instance UUID Response status codes ********************** - **200**: Success -- **404**: VPCS instance doesn't exist +- **404**: Instance doesn't exist + +Output +******* +.. raw:: html + +
Name Mandatory Type Description
console integer console TCP port
linked_clone boolean either the VM is a linked clone or not
name string VirtualBox VM instance name
project_uuid string Project UUID
+ + + + + + + +
Name Mandatory Type Description
console integer console TCP port
name string VPCS device name
project_uuid string Project UUID
script_file ['string', 'null'] VPCS startup script
startup_script ['string', 'null'] Content of the VPCS startup script
uuid string VPCS device UUID
Sample session *************** @@ -29,13 +43,13 @@ Update a VPCS instance Parameters ********** -- **uuid**: VPCS instance UUID +- **uuid**: Instance UUID Response status codes ********************** -- **200**: VPCS instance updated +- **200**: Instance updated - **409**: Conflict -- **404**: VPCS instance doesn't exist +- **404**: Instance doesn't exist Input ******* @@ -70,10 +84,10 @@ Delete a VPCS instance Parameters ********** -- **uuid**: VPCS instance UUID +- **uuid**: Instance UUID Response status codes ********************** -- **404**: VPCS instance doesn't exist -- **204**: VPCS instance deleted +- **404**: Instance doesn't exist +- **204**: Instance deleted diff --git a/docs/api/vpcsuuidreload.rst b/docs/api/vpcsuuidreload.rst index 728a34cf..93c24a52 100644 --- a/docs/api/vpcsuuidreload.rst +++ b/docs/api/vpcsuuidreload.rst @@ -5,15 +5,15 @@ POST /vpcs/**{uuid}**/reload ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Remove a NIO from a VPCS +Reload a VPCS instance Parameters ********** -- **uuid**: VPCS instance UUID +- **uuid**: Instance UUID Response status codes ********************** -- **400**: Invalid VPCS instance UUID -- **404**: VPCS instance doesn't exist -- **204**: VPCS reloaded +- **400**: Invalid instance UUID +- **404**: Instance doesn't exist +- **204**: Instance reloaded diff --git a/docs/api/vpcsuuidstart.rst b/docs/api/vpcsuuidstart.rst index 4acce2fc..aa02e25d 100644 --- a/docs/api/vpcsuuidstart.rst +++ b/docs/api/vpcsuuidstart.rst @@ -9,11 +9,11 @@ Start a VPCS instance Parameters ********** -- **uuid**: VPCS instance UUID +- **uuid**: Instance UUID Response status codes ********************** - **400**: Invalid VPCS instance UUID -- **404**: VPCS instance doesn't exist -- **204**: VPCS instance started +- **404**: Instance doesn't exist +- **204**: Instance started diff --git a/docs/api/vpcsuuidstop.rst b/docs/api/vpcsuuidstop.rst index 3b6e76fe..a11f183f 100644 --- a/docs/api/vpcsuuidstop.rst +++ b/docs/api/vpcsuuidstop.rst @@ -9,11 +9,11 @@ Stop a VPCS instance Parameters ********** -- **uuid**: VPCS instance UUID +- **uuid**: Instance UUID Response status codes ********************** - **400**: Invalid VPCS instance UUID -- **404**: VPCS instance doesn't exist -- **204**: VPCS instance stopped +- **404**: Instance doesn't exist +- **204**: Instance stopped diff --git a/gns3server/modules/port_manager.py b/gns3server/modules/port_manager.py index b03a4025..1a5f294f 100644 --- a/gns3server/modules/port_manager.py +++ b/gns3server/modules/port_manager.py @@ -51,7 +51,6 @@ class PortManager: else: self._console_host = host - assert not hasattr(PortManager, "_instance") PortManager._instance = self @classmethod diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 4873607d..f2137c93 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -428,7 +428,7 @@ class VirtualBoxVM(BaseVM): log.info("VirtualBox VM '{name}' [{uuid}] has set the VM name to '{vmname}'".format(name=self.name, uuid=self.uuid, vmname=vmname)) # TODO: test linked clone - #if self._linked_clone: + # if self._linked_clone: # yield from self._modify_vm('--name "{}"'.format(vmname)) self._vmname = vmname diff --git a/gns3server/utils/interfaces.py b/gns3server/utils/interfaces.py index 5359dfbb..701bce48 100644 --- a/gns3server/utils/interfaces.py +++ b/gns3server/utils/interfaces.py @@ -89,7 +89,7 @@ def interfaces(): import netifaces for interface in netifaces.interfaces(): results.append({"id": interface, - "name": interface}) + "name": interface}) except ImportError: return else: diff --git a/tests/modules/virtualbox/test_virtualbox_manager.py b/tests/modules/virtualbox/test_virtualbox_manager.py index 4521691b..fde3ca5b 100644 --- a/tests/modules/virtualbox/test_virtualbox_manager.py +++ b/tests/modules/virtualbox/test_virtualbox_manager.py @@ -41,4 +41,4 @@ def test_vm_non_executable_vboxmanage_path(project, manager): tmpfile = tempfile.NamedTemporaryFile() with patch("gns3server.config.Config.get_section_config", return_value={"vboxmanage_path": tmpfile.name}): with pytest.raises(VirtualBoxError): - manager.find_vboxmanage() \ No newline at end of file + manager.find_vboxmanage() From c40981938274bed16d96a5a3da226a06410d3019 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 26 Jan 2015 09:40:48 +0100 Subject: [PATCH 133/485] Add missing documentations and add julien email to travis build --- .travis.yml | 3 +- .../delete_virtualboxuuidportsportiddnio.txt | 13 +++ .../delete_vpcsuuidportsportiddnio.txt | 13 +++ docs/api/examples/get_interfaces.txt | 60 ++++++++++ docs/api/examples/get_virtualboxuuid.txt | 27 +++++ docs/api/examples/get_vpcsuuid.txt | 2 +- docs/api/examples/post_virtualbox.txt | 2 +- .../post_virtualboxuuidportsportiddnio.txt | 25 ++++ docs/api/examples/post_vpcs.txt | 2 +- .../examples/post_vpcsuuidportsportiddnio.txt | 25 ++++ docs/api/examples/put_projectuuid.txt | 4 +- docs/api/interfaces.rst | 19 ++++ docs/api/udp.rst | 13 +++ docs/api/virtualboxlist.rst | 13 +++ docs/api/virtualboxuuid.rst | 107 ++++++++++++++++++ .../api/virtualboxuuidcaptureportiddstart.rst | 29 +++++ docs/api/virtualboxuuidcaptureportiddstop.rst | 20 ++++ docs/api/virtualboxuuidportsportiddnio.rst | 48 ++++++++ docs/api/vpcsuuidportsportiddnio.rst | 48 ++++++++ 19 files changed, 467 insertions(+), 6 deletions(-) create mode 100644 docs/api/examples/delete_virtualboxuuidportsportiddnio.txt create mode 100644 docs/api/examples/delete_vpcsuuidportsportiddnio.txt create mode 100644 docs/api/examples/get_interfaces.txt create mode 100644 docs/api/examples/get_virtualboxuuid.txt create mode 100644 docs/api/examples/post_virtualboxuuidportsportiddnio.txt create mode 100644 docs/api/examples/post_vpcsuuidportsportiddnio.txt create mode 100644 docs/api/interfaces.rst create mode 100644 docs/api/udp.rst create mode 100644 docs/api/virtualboxlist.rst create mode 100644 docs/api/virtualboxuuid.rst create mode 100644 docs/api/virtualboxuuidcaptureportiddstart.rst create mode 100644 docs/api/virtualboxuuidcaptureportiddstop.rst create mode 100644 docs/api/virtualboxuuidportsportiddnio.rst create mode 100644 docs/api/vpcsuuidportsportiddnio.rst diff --git a/.travis.yml b/.travis.yml index 2c741583..1649f84b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,8 @@ script: # - master notifications: - email: false + email: + - julien@gns3.net # irc: # channels: # - "chat.freenode.net#gns3" diff --git a/docs/api/examples/delete_virtualboxuuidportsportiddnio.txt b/docs/api/examples/delete_virtualboxuuidportsportiddnio.txt new file mode 100644 index 00000000..17a9dc1c --- /dev/null +++ b/docs/api/examples/delete_virtualboxuuidportsportiddnio.txt @@ -0,0 +1,13 @@ +curl -i -X DELETE 'http://localhost:8000/virtualbox/{uuid}/ports/{port_id:\d+}/nio' + +DELETE /virtualbox/{uuid}/ports/{port_id:\d+}/nio HTTP/1.1 + + + +HTTP/1.1 204 +CONNECTION: close +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 aiohttp/0.13.1 +X-ROUTE: /virtualbox/{uuid}/ports/{port_id:\d+}/nio + diff --git a/docs/api/examples/delete_vpcsuuidportsportiddnio.txt b/docs/api/examples/delete_vpcsuuidportsportiddnio.txt new file mode 100644 index 00000000..9d2ac73f --- /dev/null +++ b/docs/api/examples/delete_vpcsuuidportsportiddnio.txt @@ -0,0 +1,13 @@ +curl -i -X DELETE 'http://localhost:8000/vpcs/{uuid}/ports/{port_id:\d+}/nio' + +DELETE /vpcs/{uuid}/ports/{port_id:\d+}/nio HTTP/1.1 + + + +HTTP/1.1 204 +CONNECTION: close +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 aiohttp/0.13.1 +X-ROUTE: /vpcs/{uuid}/ports/{port_id:\d+}/nio + diff --git a/docs/api/examples/get_interfaces.txt b/docs/api/examples/get_interfaces.txt new file mode 100644 index 00000000..1063f948 --- /dev/null +++ b/docs/api/examples/get_interfaces.txt @@ -0,0 +1,60 @@ +curl -i -X GET 'http://localhost:8000/interfaces' + +GET /interfaces HTTP/1.1 + + + +HTTP/1.1 200 +CONNECTION: close +CONTENT-LENGTH: 652 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 aiohttp/0.13.1 +X-ROUTE: /interfaces + +[ + { + "id": "lo0", + "name": "lo0" + }, + { + "id": "gif0", + "name": "gif0" + }, + { + "id": "stf0", + "name": "stf0" + }, + { + "id": "en0", + "name": "en0" + }, + { + "id": "en1", + "name": "en1" + }, + { + "id": "en2", + "name": "en2" + }, + { + "id": "fw0", + "name": "fw0" + }, + { + "id": "p2p0", + "name": "p2p0" + }, + { + "id": "bridge0", + "name": "bridge0" + }, + { + "id": "vboxnet0", + "name": "vboxnet0" + }, + { + "id": "vboxnet1", + "name": "vboxnet1" + } +] diff --git a/docs/api/examples/get_virtualboxuuid.txt b/docs/api/examples/get_virtualboxuuid.txt new file mode 100644 index 00000000..27839d2c --- /dev/null +++ b/docs/api/examples/get_virtualboxuuid.txt @@ -0,0 +1,27 @@ +curl -i -X GET 'http://localhost:8000/virtualbox/{uuid}' + +GET /virtualbox/{uuid} HTTP/1.1 + + + +HTTP/1.1 200 +CONNECTION: close +CONTENT-LENGTH: 375 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 aiohttp/0.13.1 +X-ROUTE: /virtualbox/{uuid} + +{ + "adapter_start_index": 0, + "adapter_type": "Intel PRO/1000 MT Desktop (82540EM)", + "adapters": 0, + "console": 2001, + "enable_remote_console": false, + "headless": false, + "linked_clone": false, + "name": "VMTEST", + "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", + "uuid": "9b8874fe-919e-4a30-874b-68614da8d42f", + "vmname": "VMTEST" +} diff --git a/docs/api/examples/get_vpcsuuid.txt b/docs/api/examples/get_vpcsuuid.txt index 64beb7dd..08e28bb7 100644 --- a/docs/api/examples/get_vpcsuuid.txt +++ b/docs/api/examples/get_vpcsuuid.txt @@ -18,5 +18,5 @@ X-ROUTE: /vpcs/{uuid} "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, "startup_script": null, - "uuid": "624e94fb-9e7e-45d0-a27d-4eeda19e98cd" + "uuid": "417eef12-d13b-4cb4-8a1f-1ff12963e570" } diff --git a/docs/api/examples/post_virtualbox.txt b/docs/api/examples/post_virtualbox.txt index 7142eb82..89badb75 100644 --- a/docs/api/examples/post_virtualbox.txt +++ b/docs/api/examples/post_virtualbox.txt @@ -27,6 +27,6 @@ X-ROUTE: /virtualbox "linked_clone": false, "name": "VM1", "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "uuid": "bd6e0124-bb4b-4224-a71f-9a28c302df4e", + "uuid": "455cc5f9-22f2-4121-973a-45d525110970", "vmname": "VM1" } diff --git a/docs/api/examples/post_virtualboxuuidportsportiddnio.txt b/docs/api/examples/post_virtualboxuuidportsportiddnio.txt new file mode 100644 index 00000000..a7cffb9c --- /dev/null +++ b/docs/api/examples/post_virtualboxuuidportsportiddnio.txt @@ -0,0 +1,25 @@ +curl -i -X POST 'http://localhost:8000/virtualbox/{uuid}/ports/{port_id:\d+}/nio' -d '{"lport": 4242, "rhost": "127.0.0.1", "rport": 4343, "type": "nio_udp"}' + +POST /virtualbox/{uuid}/ports/{port_id:\d+}/nio HTTP/1.1 +{ + "lport": 4242, + "rhost": "127.0.0.1", + "rport": 4343, + "type": "nio_udp" +} + + +HTTP/1.1 201 +CONNECTION: close +CONTENT-LENGTH: 89 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 aiohttp/0.13.1 +X-ROUTE: /virtualbox/{uuid}/ports/{port_id:\d+}/nio + +{ + "lport": 4242, + "rhost": "127.0.0.1", + "rport": 4343, + "type": "nio_udp" +} diff --git a/docs/api/examples/post_vpcs.txt b/docs/api/examples/post_vpcs.txt index 9e657633..af3ed908 100644 --- a/docs/api/examples/post_vpcs.txt +++ b/docs/api/examples/post_vpcs.txt @@ -21,5 +21,5 @@ X-ROUTE: /vpcs "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, "startup_script": null, - "uuid": "fc2b4d10-e4c6-4545-8b59-cd7a09bc3d33" + "uuid": "7c5d69c1-ba5a-4c2a-b1a7-da721aa58044" } diff --git a/docs/api/examples/post_vpcsuuidportsportiddnio.txt b/docs/api/examples/post_vpcsuuidportsportiddnio.txt new file mode 100644 index 00000000..09a955a5 --- /dev/null +++ b/docs/api/examples/post_vpcsuuidportsportiddnio.txt @@ -0,0 +1,25 @@ +curl -i -X POST 'http://localhost:8000/vpcs/{uuid}/ports/{port_id:\d+}/nio' -d '{"lport": 4242, "rhost": "127.0.0.1", "rport": 4343, "type": "nio_udp"}' + +POST /vpcs/{uuid}/ports/{port_id:\d+}/nio HTTP/1.1 +{ + "lport": 4242, + "rhost": "127.0.0.1", + "rport": 4343, + "type": "nio_udp" +} + + +HTTP/1.1 201 +CONNECTION: close +CONTENT-LENGTH: 89 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 aiohttp/0.13.1 +X-ROUTE: /vpcs/{uuid}/ports/{port_id:\d+}/nio + +{ + "lport": 4242, + "rhost": "127.0.0.1", + "rport": 4343, + "type": "nio_udp" +} diff --git a/docs/api/examples/put_projectuuid.txt b/docs/api/examples/put_projectuuid.txt index 2b1f5409..a1baf6c8 100644 --- a/docs/api/examples/put_projectuuid.txt +++ b/docs/api/examples/put_projectuuid.txt @@ -15,7 +15,7 @@ SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /project/{uuid} { - "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmp2d1dq1sb", + "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpivbrfsdh", "temporary": false, - "uuid": "7a6d9fd4-c212-4368-950f-5513e518313a" + "uuid": "0442fdb6-fc77-4f1c-b996-675b98dc032e" } diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst new file mode 100644 index 00000000..946e4c15 --- /dev/null +++ b/docs/api/interfaces.rst @@ -0,0 +1,19 @@ +/interfaces +--------------------------------------------- + +.. contents:: + +GET /interfaces +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +List all the network interfaces available on the server + +Response status codes +********************** +- **200**: OK + +Sample session +*************** + + +.. literalinclude:: examples/get_interfaces.txt + diff --git a/docs/api/udp.rst b/docs/api/udp.rst new file mode 100644 index 00000000..f2cdb2ae --- /dev/null +++ b/docs/api/udp.rst @@ -0,0 +1,13 @@ +/udp +--------------------------------------------- + +.. contents:: + +POST /udp +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Allocate an UDP port on the server + +Response status codes +********************** +- **201**: UDP port allocated + diff --git a/docs/api/virtualboxlist.rst b/docs/api/virtualboxlist.rst new file mode 100644 index 00000000..f6944d15 --- /dev/null +++ b/docs/api/virtualboxlist.rst @@ -0,0 +1,13 @@ +/virtualbox/list +--------------------------------------------- + +.. contents:: + +GET /virtualbox/list +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Get all VirtualBox VMs available + +Response status codes +********************** +- **200**: Success + diff --git a/docs/api/virtualboxuuid.rst b/docs/api/virtualboxuuid.rst new file mode 100644 index 00000000..c744a107 --- /dev/null +++ b/docs/api/virtualboxuuid.rst @@ -0,0 +1,107 @@ +/virtualbox/{uuid} +--------------------------------------------- + +.. contents:: + +GET /virtualbox/**{uuid}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Get a VirtualBox VM instance + +Parameters +********** +- **uuid**: Instance UUID + +Response status codes +********************** +- **200**: Success +- **404**: Instance doesn't exist + +Output +******* +.. raw:: html + + + + + + + + + + + + + + +
Name Mandatory Type Description
adapter_start_index integer adapter index from which to start using adapters
adapter_type string VirtualBox adapter type
adapters integer number of adapters
console integer console TCP port
enable_remote_console boolean enable the remote console
headless boolean headless mode
linked_clone boolean either the VM is a linked clone or not
name string VirtualBox VM instance name
project_uuid string Project UUID
uuid string VirtualBox VM instance UUID
vmname string VirtualBox VM name (in VirtualBox itself)
+ +Sample session +*************** + + +.. literalinclude:: examples/get_virtualboxuuid.txt + + +PUT /virtualbox/**{uuid}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Update a VirtualBox VM instance + +Parameters +********** +- **uuid**: Instance UUID + +Response status codes +********************** +- **200**: Instance updated +- **409**: Conflict +- **404**: Instance doesn't exist + +Input +******* +.. raw:: html + + + + + + + + + + + +
Name Mandatory Type Description
adapter_start_index integer adapter index from which to start using adapters
adapter_type string VirtualBox adapter type
adapters integer number of adapters
console integer console TCP port
enable_remote_console boolean enable the remote console
headless boolean headless mode
name string VirtualBox VM instance name
vmname string VirtualBox VM name (in VirtualBox itself)
+ +Output +******* +.. raw:: html + + + + + + + + + + + + + + +
Name Mandatory Type Description
adapter_start_index integer adapter index from which to start using adapters
adapter_type string VirtualBox adapter type
adapters integer number of adapters
console integer console TCP port
enable_remote_console boolean enable the remote console
headless boolean headless mode
linked_clone boolean either the VM is a linked clone or not
name string VirtualBox VM instance name
project_uuid string Project UUID
uuid string VirtualBox VM instance UUID
vmname string VirtualBox VM name (in VirtualBox itself)
+ + +DELETE /virtualbox/**{uuid}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Delete a VirtualBox VM instance + +Parameters +********** +- **uuid**: Instance UUID + +Response status codes +********************** +- **404**: Instance doesn't exist +- **204**: Instance deleted + diff --git a/docs/api/virtualboxuuidcaptureportiddstart.rst b/docs/api/virtualboxuuidcaptureportiddstart.rst new file mode 100644 index 00000000..b6a61ca0 --- /dev/null +++ b/docs/api/virtualboxuuidcaptureportiddstart.rst @@ -0,0 +1,29 @@ +/virtualbox/{uuid}/capture/{port_id:\d+}/start +--------------------------------------------- + +.. contents:: + +POST /virtualbox/**{uuid}**/capture/**{port_id:\d+}**/start +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Start a packet capture on a VirtualBox VM instance + +Parameters +********** +- **port_id**: ID of the port to start a packet capture +- **uuid**: Instance UUID + +Response status codes +********************** +- **200**: Capture started +- **400**: Invalid instance UUID +- **404**: Instance doesn't exist + +Input +******* +.. raw:: html + + + + +
Name Mandatory Type Description
capture_filename string Capture file name
+ diff --git a/docs/api/virtualboxuuidcaptureportiddstop.rst b/docs/api/virtualboxuuidcaptureportiddstop.rst new file mode 100644 index 00000000..c336bf6c --- /dev/null +++ b/docs/api/virtualboxuuidcaptureportiddstop.rst @@ -0,0 +1,20 @@ +/virtualbox/{uuid}/capture/{port_id:\d+}/stop +--------------------------------------------- + +.. contents:: + +POST /virtualbox/**{uuid}**/capture/**{port_id:\d+}**/stop +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Stop a packet capture on a VirtualBox VM instance + +Parameters +********** +- **port_id**: ID of the port to stop a packet capture +- **uuid**: Instance UUID + +Response status codes +********************** +- **400**: Invalid instance UUID +- **404**: Instance doesn't exist +- **204**: Capture stopped + diff --git a/docs/api/virtualboxuuidportsportiddnio.rst b/docs/api/virtualboxuuidportsportiddnio.rst new file mode 100644 index 00000000..817dc47a --- /dev/null +++ b/docs/api/virtualboxuuidportsportiddnio.rst @@ -0,0 +1,48 @@ +/virtualbox/{uuid}/ports/{port_id:\d+}/nio +--------------------------------------------- + +.. contents:: + +POST /virtualbox/**{uuid}**/ports/**{port_id:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Add a NIO to a VirtualBox VM instance + +Parameters +********** +- **port_id**: ID of the port where the nio should be added +- **uuid**: Instance UUID + +Response status codes +********************** +- **400**: Invalid instance UUID +- **201**: NIO created +- **404**: Instance doesn't exist + +Sample session +*************** + + +.. literalinclude:: examples/post_virtualboxuuidportsportiddnio.txt + + +DELETE /virtualbox/**{uuid}**/ports/**{port_id:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Remove a NIO from a VirtualBox VM instance + +Parameters +********** +- **port_id**: ID of the port from where the nio should be removed +- **uuid**: Instance UUID + +Response status codes +********************** +- **400**: Invalid instance UUID +- **404**: Instance doesn't exist +- **204**: NIO deleted + +Sample session +*************** + + +.. literalinclude:: examples/delete_virtualboxuuidportsportiddnio.txt + diff --git a/docs/api/vpcsuuidportsportiddnio.rst b/docs/api/vpcsuuidportsportiddnio.rst new file mode 100644 index 00000000..a3075cd0 --- /dev/null +++ b/docs/api/vpcsuuidportsportiddnio.rst @@ -0,0 +1,48 @@ +/vpcs/{uuid}/ports/{port_id:\d+}/nio +--------------------------------------------- + +.. contents:: + +POST /vpcs/**{uuid}**/ports/**{port_id:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Add a NIO to a VPCS instance + +Parameters +********** +- **port_id**: ID of the port where the nio should be added +- **uuid**: Instance UUID + +Response status codes +********************** +- **400**: Invalid instance UUID +- **201**: NIO created +- **404**: Instance doesn't exist + +Sample session +*************** + + +.. literalinclude:: examples/post_vpcsuuidportsportiddnio.txt + + +DELETE /vpcs/**{uuid}**/ports/**{port_id:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Remove a NIO from a VPCS instance + +Parameters +********** +- **port_id**: ID of the port from where the nio should be removed +- **uuid**: Instance UUID + +Response status codes +********************** +- **400**: Invalid instance UUID +- **404**: Instance doesn't exist +- **204**: NIO deleted + +Sample session +*************** + + +.. literalinclude:: examples/delete_vpcsuuidportsportiddnio.txt + From 1bfb201368f9bcc34e30ea906c8dda59582fc27e Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 26 Jan 2015 09:51:29 +0100 Subject: [PATCH 134/485] Enable code live reload only in debug mode --- gns3server/main.py | 3 ++- gns3server/server.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/gns3server/main.py b/gns3server/main.py index 83d5c88f..f400f8cd 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -82,7 +82,7 @@ def parse_arguments(): parser.add_argument("-L", "--local", action="store_true", help="local mode (allow some insecure operations)") parser.add_argument("-A", "--allow", action="store_true", help="allow remote connections to local console ports") parser.add_argument("-q", "--quiet", action="store_true", help="do not show logs on stdout") - parser.add_argument("-d", "--debug", action="store_true", help="show debug logs") + parser.add_argument("-d", "--debug", action="store_true", help="show debug logs and enable code live reload") args = parser.parse_args() config = Config.instance() @@ -94,6 +94,7 @@ def parse_arguments(): server_config["ssl"] = server_config.get("ssl", "true" if args.ssl else "false") server_config["certfile"] = server_config.get("certfile", args.certfile) server_config["certkey"] = server_config.get("certkey", args.certkey) + server_config["debug"] = server_config.get("debug", "true" if args.debug else "false") config.set_section_config("Server", server_config) return args diff --git a/gns3server/server.py b/gns3server/server.py index e2d7809a..03908eff 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -164,6 +164,7 @@ class Server: self._loop.run_until_complete(self._run_application(app, ssl_context)) self._signal_handling() - # FIXME: remove it in production or in tests - self._loop.call_later(1, self._reload_hook) + if server_config["debug"] == "true": + log.info("Code live reload is enabled, watching for file changes") + self._loop.call_later(1, self._reload_hook) self._loop.run_forever() From 4518404706edce51f863bb87313a7a54a1c42520 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 26 Jan 2015 12:10:30 +0100 Subject: [PATCH 135/485] Run rmtree in a different thread --- gns3server/handlers/project_handler.py | 6 ++-- gns3server/modules/project.py | 16 +++++++--- gns3server/utils/asyncio.py | 36 ++++++++++++++++++++++ tests/modules/test_project.py | 17 ++++++----- tests/utils/test_asyncio.py | 42 ++++++++++++++++++++++++++ 5 files changed, 101 insertions(+), 16 deletions(-) create mode 100644 gns3server/utils/asyncio.py create mode 100644 tests/utils/test_asyncio.py diff --git a/gns3server/handlers/project_handler.py b/gns3server/handlers/project_handler.py index 1ce30c18..e72396f3 100644 --- a/gns3server/handlers/project_handler.py +++ b/gns3server/handlers/project_handler.py @@ -92,7 +92,7 @@ class ProjectHandler: pm = ProjectManager.instance() project = pm.get_project(request.match_info["uuid"]) - project.commit() + yield from project.commit() response.set_status(204) @classmethod @@ -110,7 +110,7 @@ class ProjectHandler: pm = ProjectManager.instance() project = pm.get_project(request.match_info["uuid"]) - project.close() + yield from project.close() response.set_status(204) @classmethod @@ -128,5 +128,5 @@ class ProjectHandler: pm = ProjectManager.instance() project = pm.get_project(request.match_info["uuid"]) - project.delete() + yield from project.delete() response.set_status(204) diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index ca29aa93..4d174c45 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -19,9 +19,11 @@ import aiohttp import os import tempfile import shutil +import asyncio from uuid import UUID, uuid4 -from ..config import Config +from ..config import Config +from ..utils.asyncio import wait_run_in_executor import logging log = logging.getLogger(__name__) @@ -176,11 +178,13 @@ class Project: if vm in self._vms: self._vms.remove(vm) + @asyncio.coroutine def close(self): """Close the project, but keep informations on disk""" - self._close_and_clean(self._temporary) + yield from self._close_and_clean(self._temporary) + @asyncio.coroutine def _close_and_clean(self, cleanup): """ Close the project, and cleanup the disk if cleanup is True @@ -191,8 +195,9 @@ class Project: for vm in self._vms: vm.close() if cleanup and os.path.exists(self.path): - shutil.rmtree(self.path) + yield from wait_run_in_executor(shutil.rmtree, self.path) + @asyncio.coroutine def commit(self): """Write project changes on disk""" @@ -200,10 +205,11 @@ class Project: vm = self._vms_to_destroy.pop() directory = self.vm_working_directory(vm) if os.path.exists(directory): - shutil.rmtree(directory) + yield from wait_run_in_executor(shutil.rmtree, directory) self.remove_vm(vm) + @asyncio.coroutine def delete(self): """Remove project from disk""" - self._close_and_clean(True) + yield from self._close_and_clean(True) diff --git a/gns3server/utils/asyncio.py b/gns3server/utils/asyncio.py new file mode 100644 index 00000000..9b94eaf0 --- /dev/null +++ b/gns3server/utils/asyncio.py @@ -0,0 +1,36 @@ +# -*- 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 asyncio + + +@asyncio.coroutine +def wait_run_in_executor(func, *args): + """ + Run blocking code in a different thread and wait + the result. + + :param func: Run this function in a different thread + :param args: Parameters of the function + :returns: Return the result of the function + """ + + loop = asyncio.get_event_loop() + future = loop.run_in_executor(None, func, *args) + yield from asyncio.wait([future]) + return future.result() diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py index 909a09b6..c267a690 100644 --- a/tests/modules/test_project.py +++ b/tests/modules/test_project.py @@ -17,6 +17,7 @@ # along with this program. If not, see . import os +import asyncio import pytest import aiohttp from unittest.mock import patch @@ -84,7 +85,7 @@ def test_mark_vm_for_destruction(vm): assert len(project.vms) == 0 -def test_commit(manager): +def test_commit(manager, loop): project = Project() vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) project.add_vm(vm) @@ -92,17 +93,17 @@ def test_commit(manager): project.mark_vm_for_destruction(vm) assert len(project._vms_to_destroy) == 1 assert os.path.exists(directory) - project.commit() + loop.run_until_complete(asyncio.async(project.commit())) assert len(project._vms_to_destroy) == 0 assert os.path.exists(directory) is False assert len(project.vms) == 0 -def test_project_delete(): +def test_project_delete(loop): project = Project() directory = project.path assert os.path.exists(directory) - project.delete() + loop.run_until_complete(asyncio.async(project.delete())) assert os.path.exists(directory) is False @@ -113,22 +114,22 @@ def test_project_add_vm(manager): assert len(project.vms) == 1 -def test_project_close(manager): +def test_project_close(loop, manager): project = Project() vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) project.add_vm(vm) with patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM.close") as mock: - project.close() + loop.run_until_complete(asyncio.async(project.close())) assert mock.called -def test_project_close_temporary_project(manager): +def test_project_close_temporary_project(loop, manager): """A temporary project is deleted when closed""" project = Project(temporary=True) directory = project.path assert os.path.exists(directory) - project.close() + loop.run_until_complete(asyncio.async(project.close())) assert os.path.exists(directory) is False diff --git a/tests/utils/test_asyncio.py b/tests/utils/test_asyncio.py new file mode 100644 index 00000000..a6a9cc3e --- /dev/null +++ b/tests/utils/test_asyncio.py @@ -0,0 +1,42 @@ +# -*- 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 asyncio +import pytest + +from gns3server.utils.asyncio import wait_run_in_executor + + +def test_wait_run_in_executor(loop): + + def change_var(param): + return param + + exec = wait_run_in_executor(change_var, "test") + result = loop.run_until_complete(asyncio.async(exec)) + assert result == "test" + + +def test_exception_wait_run_in_executor(loop): + + def raise_exception(): + raise Exception("test") + + exec = wait_run_in_executor(raise_exception) + with pytest.raises(Exception): + result = loop.run_until_complete(asyncio.async(exec)) From df8bdcc152529713cb729ade7e1c423728d80811 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 26 Jan 2015 13:54:44 +0100 Subject: [PATCH 136/485] Catch exceptions from rmtree --- gns3server/modules/project.py | 10 ++++++++-- tests/conftest.py | 6 +++++- tests/modules/test_project.py | 25 +++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index 4d174c45..86985f42 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -195,7 +195,10 @@ class Project: for vm in self._vms: vm.close() if cleanup and os.path.exists(self.path): - yield from wait_run_in_executor(shutil.rmtree, self.path) + try: + yield from wait_run_in_executor(shutil.rmtree, self.path) + except OSError as e: + raise aiohttp.web.HTTPInternalServerError(text="Could not delete the project directory: {}".format(e)) @asyncio.coroutine def commit(self): @@ -205,7 +208,10 @@ class Project: vm = self._vms_to_destroy.pop() directory = self.vm_working_directory(vm) if os.path.exists(directory): - yield from wait_run_in_executor(shutil.rmtree, directory) + try: + yield from wait_run_in_executor(shutil.rmtree, directory) + except OSError as e: + raise aiohttp.web.HTTPInternalServerError(text="Could not delete the project directory: {}".format(e)) self.remove_vm(vm) @asyncio.coroutine diff --git a/tests/conftest.py b/tests/conftest.py index 5f2de4c4..dec63d56 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -117,4 +117,8 @@ def run_around_tests(): yield - shutil.rmtree(tmppath) + # An helper should not raise Exception + try: + shutil.rmtree(tmppath) + except: + pass diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py index c267a690..f3f7b084 100644 --- a/tests/modules/test_project.py +++ b/tests/modules/test_project.py @@ -20,6 +20,7 @@ import os import asyncio import pytest import aiohttp +import shutil from unittest.mock import patch from gns3server.modules.project import Project @@ -99,6 +100,20 @@ def test_commit(manager, loop): assert len(project.vms) == 0 +def test_commit_permission_issue(manager, loop): + project = Project() + vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + project.add_vm(vm) + directory = project.vm_working_directory(vm) + project.mark_vm_for_destruction(vm) + assert len(project._vms_to_destroy) == 1 + assert os.path.exists(directory) + os.chmod(directory, 0) + with pytest.raises(aiohttp.web.HTTPInternalServerError): + loop.run_until_complete(asyncio.async(project.commit())) + os.chmod(directory, 700) + + def test_project_delete(loop): project = Project() directory = project.path @@ -107,6 +122,16 @@ def test_project_delete(loop): assert os.path.exists(directory) is False +def test_project_delete_permission_issue(loop): + project = Project() + directory = project.path + assert os.path.exists(directory) + os.chmod(directory, 0) + with pytest.raises(aiohttp.web.HTTPInternalServerError): + loop.run_until_complete(asyncio.async(project.delete())) + os.chmod(directory, 700) + + def test_project_add_vm(manager): project = Project() vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) From 9abf323e7d7036529b1a526086d6239ec7e5d3d5 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 26 Jan 2015 14:40:31 +0100 Subject: [PATCH 137/485] Send GNS 3 server version in header and upgrade aiohttp --- gns3server/web/response.py | 4 ++++ requirements.txt | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/gns3server/web/response.py b/gns3server/web/response.py index 04d0846a..b958c7f6 100644 --- a/gns3server/web/response.py +++ b/gns3server/web/response.py @@ -19,6 +19,8 @@ import json import jsonschema import aiohttp.web import logging +import sys +from ..version import __version__ log = logging.getLogger(__name__) @@ -30,6 +32,7 @@ class Response(aiohttp.web.Response): self._route = route self._output_schema = output_schema headers['X-Route'] = self._route + headers['Server'] = "Python/{0[0]}.{0[1]} GNS3/{1}".format(sys.version_info, __version__) super().__init__(headers=headers, **kwargs) """ @@ -42,6 +45,7 @@ class Response(aiohttp.web.Response): def json(self, answer): """Pass a Python object and return a JSON as answer""" + print(self.headers) self.content_type = "application/json" if hasattr(answer, '__json__'): answer = answer.__json__() diff --git a/requirements.txt b/requirements.txt index a7f54074..b7c04a29 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,4 @@ pycurl==7.19.5 python-dateutil==2.3 apache-libcloud==0.16.0 requests==2.5.0 -aiohttp==0.13.1 +aiohttp==0.14.2 From 6764c6e866cafbcba110e65715ed671d73d58c04 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 26 Jan 2015 16:39:09 +0100 Subject: [PATCH 138/485] Useless debug --- gns3server/web/response.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gns3server/web/response.py b/gns3server/web/response.py index b958c7f6..9ddb98f4 100644 --- a/gns3server/web/response.py +++ b/gns3server/web/response.py @@ -45,7 +45,6 @@ class Response(aiohttp.web.Response): def json(self, answer): """Pass a Python object and return a JSON as answer""" - print(self.headers) self.content_type = "application/json" if hasattr(answer, '__json__'): answer = answer.__json__() From 776bfea3d70cd7899cb33e56c9bd4bb0dd4f59d2 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 26 Jan 2015 17:40:41 +0100 Subject: [PATCH 139/485] Clean enable debug mode --- gns3server/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/server.py b/gns3server/server.py index 03908eff..00cd83bf 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -164,7 +164,7 @@ class Server: self._loop.run_until_complete(self._run_application(app, ssl_context)) self._signal_handling() - if server_config["debug"] == "true": + if server_config.getboolean("debug"): log.info("Code live reload is enabled, watching for file changes") self._loop.call_later(1, self._reload_hook) self._loop.run_forever() From e60366c5bb403b5f67fbf3b88599e4f75c1ff406 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 26 Jan 2015 20:29:02 -0700 Subject: [PATCH 140/485] Change URL to get all VirtualBox VMs. --- gns3server/handlers/virtualbox_handler.py | 3 +- scripts/ws_client.py | 46 ----------------------- 2 files changed, 1 insertion(+), 48 deletions(-) delete mode 100644 scripts/ws_client.py diff --git a/gns3server/handlers/virtualbox_handler.py b/gns3server/handlers/virtualbox_handler.py index fef071bb..7f2ce495 100644 --- a/gns3server/handlers/virtualbox_handler.py +++ b/gns3server/handlers/virtualbox_handler.py @@ -26,14 +26,13 @@ from ..modules.virtualbox import VirtualBox class VirtualBoxHandler: - """ API entry points for VirtualBox. """ @classmethod @Route.get( - r"/virtualbox/list", + r"/virtualbox/vms", status_codes={ 200: "Success", }, diff --git a/scripts/ws_client.py b/scripts/ws_client.py deleted file mode 100644 index 9c0911f5..00000000 --- a/scripts/ws_client.py +++ /dev/null @@ -1,46 +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 . - -from ws4py.client.threadedclient import WebSocketClient - - -class WSClient(WebSocketClient): - - def opened(self): - - print("Connection successful with {}:{}".format(self.host, self.port)) - - self.send('{"jsonrpc": 2.0, "method": "dynamips.settings", "params": {"path": "/usr/local/bin/dynamips", "allocate_hypervisor_per_device": true, "working_dir": "/tmp/gns3-1b4grwm3-files", "udp_end_port_range": 20000, "sparse_memory_support": true, "allocate_hypervisor_per_ios_image": true, "aux_start_port_range": 2501, "use_local_server": true, "hypervisor_end_port_range": 7700, "aux_end_port_range": 3000, "mmap_support": true, "console_start_port_range": 2001, "console_end_port_range": 2500, "hypervisor_start_port_range": 7200, "ghost_ios_support": true, "memory_usage_limit_per_hypervisor": 1024, "jit_sharing_support": false, "udp_start_port_range": 10001}}') - self.send('{"jsonrpc": 2.0, "method": "dynamips.vm.create", "id": "e8caf5be-de3d-40dd-80b9-ab6df8029570", "params": {"image": "/home/grossmj/GNS3/images/IOS/c3725-advipservicesk9-mz.124-15.T14.image", "name": "R1", "platform": "c3725", "ram": 256}}') - - def closed(self, code, reason=None): - - print("Closed down. Code: {} Reason: {}".format(code, reason)) - - def received_message(self, m): - - print(m) - if len(m) == 175: - self.close(reason='Bye bye') - -if __name__ == '__main__': - try: - ws = WSClient('ws://localhost:8000/', protocols=['http-only', 'chat']) - ws.connect() - ws.run_forever() - except KeyboardInterrupt: - ws.close() From 29a4a0634d08c17450b1074b105c4da1aa311553 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 27 Jan 2015 11:39:13 +0100 Subject: [PATCH 141/485] Add console debug --- gns3server/web/response.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/gns3server/web/response.py b/gns3server/web/response.py index 9ddb98f4..e252b0b6 100644 --- a/gns3server/web/response.py +++ b/gns3server/web/response.py @@ -35,6 +35,15 @@ class Response(aiohttp.web.Response): headers['Server'] = "Python/{0[0]}.{0[1]} GNS3/{1}".format(sys.version_info, __version__) super().__init__(headers=headers, **kwargs) + def start(self, request): + log.debug("{} {}".format(self.status, self.reason)) + log.debug(dict(self.headers)) + return super().start(request) + + def write(self, data): + log.debug(data) + return super().write(data) + """ Set the response content type to application/json and serialize the content. From f682e1c47431a5d27f04a0a17a59f9e43a7f985c Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 27 Jan 2015 15:06:55 +0100 Subject: [PATCH 142/485] Complete documentation --- docs/api/examples/delete_projectuuid.txt | 4 ++-- .../delete_virtualboxuuidportsportiddnio.txt | 4 ++-- .../delete_vpcsuuidportsportiddnio.txt | 4 ++-- docs/api/examples/get_interfaces.txt | 22 ++++++------------- docs/api/examples/get_projectuuid.txt | 4 ++-- docs/api/examples/get_version.txt | 4 ++-- docs/api/examples/get_virtualboxuuid.txt | 6 ++--- docs/api/examples/get_vpcsuuid.txt | 6 ++--- docs/api/examples/post_projectuuidclose.txt | 4 ++-- docs/api/examples/post_projectuuidcommit.txt | 4 ++-- docs/api/examples/post_udp.txt | 17 ++++++++++++++ docs/api/examples/post_version.txt | 4 ++-- docs/api/examples/post_virtualbox.txt | 6 ++--- .../post_virtualboxuuidportsportiddnio.txt | 4 ++-- docs/api/examples/post_vpcs.txt | 6 ++--- .../examples/post_vpcsuuidportsportiddnio.txt | 4 ++-- docs/api/examples/put_projectuuid.txt | 8 +++---- docs/api/udp.rst | 6 +++++ .../api/virtualboxuuidcaptureportiddstart.rst | 2 +- docs/api/virtualboxuuidcaptureportiddstop.rst | 2 +- docs/api/virtualboxuuidportsportiddnio.rst | 4 ++-- docs/api/virtualboxvms.rst | 13 +++++++++++ docs/api/vpcsuuidportsportiddnio.rst | 4 ++-- tests/api/test_network.py | 2 +- 24 files changed, 86 insertions(+), 58 deletions(-) create mode 100644 docs/api/examples/post_udp.txt create mode 100644 docs/api/virtualboxvms.rst diff --git a/docs/api/examples/delete_projectuuid.txt b/docs/api/examples/delete_projectuuid.txt index 68989437..9d63e2e0 100644 --- a/docs/api/examples/delete_projectuuid.txt +++ b/docs/api/examples/delete_projectuuid.txt @@ -5,9 +5,9 @@ DELETE /project/{uuid} HTTP/1.1 HTTP/1.1 204 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /project/{uuid} diff --git a/docs/api/examples/delete_virtualboxuuidportsportiddnio.txt b/docs/api/examples/delete_virtualboxuuidportsportiddnio.txt index 17a9dc1c..6cbca129 100644 --- a/docs/api/examples/delete_virtualboxuuidportsportiddnio.txt +++ b/docs/api/examples/delete_virtualboxuuidportsportiddnio.txt @@ -5,9 +5,9 @@ DELETE /virtualbox/{uuid}/ports/{port_id:\d+}/nio HTTP/1.1 HTTP/1.1 204 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /virtualbox/{uuid}/ports/{port_id:\d+}/nio diff --git a/docs/api/examples/delete_vpcsuuidportsportiddnio.txt b/docs/api/examples/delete_vpcsuuidportsportiddnio.txt index 9d2ac73f..b5801424 100644 --- a/docs/api/examples/delete_vpcsuuidportsportiddnio.txt +++ b/docs/api/examples/delete_vpcsuuidportsportiddnio.txt @@ -5,9 +5,9 @@ DELETE /vpcs/{uuid}/ports/{port_id:\d+}/nio HTTP/1.1 HTTP/1.1 204 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /vpcs/{uuid}/ports/{port_id:\d+}/nio diff --git a/docs/api/examples/get_interfaces.txt b/docs/api/examples/get_interfaces.txt index 1063f948..7e21daff 100644 --- a/docs/api/examples/get_interfaces.txt +++ b/docs/api/examples/get_interfaces.txt @@ -5,11 +5,11 @@ GET /interfaces HTTP/1.1 HTTP/1.1 200 -CONNECTION: close -CONTENT-LENGTH: 652 +CONNECTION: keep-alive +CONTENT-LENGTH: 520 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /interfaces [ @@ -33,14 +33,14 @@ X-ROUTE: /interfaces "id": "en1", "name": "en1" }, - { - "id": "en2", - "name": "en2" - }, { "id": "fw0", "name": "fw0" }, + { + "id": "en2", + "name": "en2" + }, { "id": "p2p0", "name": "p2p0" @@ -48,13 +48,5 @@ X-ROUTE: /interfaces { "id": "bridge0", "name": "bridge0" - }, - { - "id": "vboxnet0", - "name": "vboxnet0" - }, - { - "id": "vboxnet1", - "name": "vboxnet1" } ] diff --git a/docs/api/examples/get_projectuuid.txt b/docs/api/examples/get_projectuuid.txt index bed046ad..2388771c 100644 --- a/docs/api/examples/get_projectuuid.txt +++ b/docs/api/examples/get_projectuuid.txt @@ -5,11 +5,11 @@ GET /project/{uuid} HTTP/1.1 HTTP/1.1 200 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 102 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /project/{uuid} { diff --git a/docs/api/examples/get_version.txt b/docs/api/examples/get_version.txt index 59bdb128..39509cda 100644 --- a/docs/api/examples/get_version.txt +++ b/docs/api/examples/get_version.txt @@ -5,11 +5,11 @@ GET /version HTTP/1.1 HTTP/1.1 200 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 29 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /version { diff --git a/docs/api/examples/get_virtualboxuuid.txt b/docs/api/examples/get_virtualboxuuid.txt index 27839d2c..446e6e1c 100644 --- a/docs/api/examples/get_virtualboxuuid.txt +++ b/docs/api/examples/get_virtualboxuuid.txt @@ -5,11 +5,11 @@ GET /virtualbox/{uuid} HTTP/1.1 HTTP/1.1 200 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 375 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /virtualbox/{uuid} { @@ -22,6 +22,6 @@ X-ROUTE: /virtualbox/{uuid} "linked_clone": false, "name": "VMTEST", "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "uuid": "9b8874fe-919e-4a30-874b-68614da8d42f", + "uuid": "be1fa0fe-cd51-41e0-9806-2bac0f5f50ba", "vmname": "VMTEST" } diff --git a/docs/api/examples/get_vpcsuuid.txt b/docs/api/examples/get_vpcsuuid.txt index 08e28bb7..4112dde4 100644 --- a/docs/api/examples/get_vpcsuuid.txt +++ b/docs/api/examples/get_vpcsuuid.txt @@ -5,11 +5,11 @@ GET /vpcs/{uuid} HTTP/1.1 HTTP/1.1 200 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 213 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /vpcs/{uuid} { @@ -18,5 +18,5 @@ X-ROUTE: /vpcs/{uuid} "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, "startup_script": null, - "uuid": "417eef12-d13b-4cb4-8a1f-1ff12963e570" + "uuid": "a474c92d-c9d2-4f53-bbe2-64493f8f07cc" } diff --git a/docs/api/examples/post_projectuuidclose.txt b/docs/api/examples/post_projectuuidclose.txt index d038c178..03ae88bc 100644 --- a/docs/api/examples/post_projectuuidclose.txt +++ b/docs/api/examples/post_projectuuidclose.txt @@ -5,9 +5,9 @@ POST /project/{uuid}/close HTTP/1.1 HTTP/1.1 204 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /project/{uuid}/close diff --git a/docs/api/examples/post_projectuuidcommit.txt b/docs/api/examples/post_projectuuidcommit.txt index b5d0c2d9..2fe9c38f 100644 --- a/docs/api/examples/post_projectuuidcommit.txt +++ b/docs/api/examples/post_projectuuidcommit.txt @@ -5,9 +5,9 @@ POST /project/{uuid}/commit HTTP/1.1 HTTP/1.1 204 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /project/{uuid}/commit diff --git a/docs/api/examples/post_udp.txt b/docs/api/examples/post_udp.txt new file mode 100644 index 00000000..7dfd4d75 --- /dev/null +++ b/docs/api/examples/post_udp.txt @@ -0,0 +1,17 @@ +curl -i -X POST 'http://localhost:8000/udp' -d '{}' + +POST /udp HTTP/1.1 +{} + + +HTTP/1.1 201 +CONNECTION: keep-alive +CONTENT-LENGTH: 25 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /udp + +{ + "udp_port": 10000 +} diff --git a/docs/api/examples/post_version.txt b/docs/api/examples/post_version.txt index 45ef1069..1d4ecc7d 100644 --- a/docs/api/examples/post_version.txt +++ b/docs/api/examples/post_version.txt @@ -7,11 +7,11 @@ POST /version HTTP/1.1 HTTP/1.1 200 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 29 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /version { diff --git a/docs/api/examples/post_virtualbox.txt b/docs/api/examples/post_virtualbox.txt index 89badb75..3eff439d 100644 --- a/docs/api/examples/post_virtualbox.txt +++ b/docs/api/examples/post_virtualbox.txt @@ -10,11 +10,11 @@ POST /virtualbox HTTP/1.1 HTTP/1.1 201 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 369 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /virtualbox { @@ -27,6 +27,6 @@ X-ROUTE: /virtualbox "linked_clone": false, "name": "VM1", "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "uuid": "455cc5f9-22f2-4121-973a-45d525110970", + "uuid": "8b5bbfa3-0682-4d65-ae8b-a1aea9dc40e5", "vmname": "VM1" } diff --git a/docs/api/examples/post_virtualboxuuidportsportiddnio.txt b/docs/api/examples/post_virtualboxuuidportsportiddnio.txt index a7cffb9c..d754de40 100644 --- a/docs/api/examples/post_virtualboxuuidportsportiddnio.txt +++ b/docs/api/examples/post_virtualboxuuidportsportiddnio.txt @@ -10,11 +10,11 @@ POST /virtualbox/{uuid}/ports/{port_id:\d+}/nio HTTP/1.1 HTTP/1.1 201 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 89 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /virtualbox/{uuid}/ports/{port_id:\d+}/nio { diff --git a/docs/api/examples/post_vpcs.txt b/docs/api/examples/post_vpcs.txt index af3ed908..2eadd535 100644 --- a/docs/api/examples/post_vpcs.txt +++ b/docs/api/examples/post_vpcs.txt @@ -8,11 +8,11 @@ POST /vpcs HTTP/1.1 HTTP/1.1 201 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 213 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /vpcs { @@ -21,5 +21,5 @@ X-ROUTE: /vpcs "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, "startup_script": null, - "uuid": "7c5d69c1-ba5a-4c2a-b1a7-da721aa58044" + "uuid": "688ff2f8-08c5-4218-8e19-99f1ac7fc20d" } diff --git a/docs/api/examples/post_vpcsuuidportsportiddnio.txt b/docs/api/examples/post_vpcsuuidportsportiddnio.txt index 09a955a5..057c1385 100644 --- a/docs/api/examples/post_vpcsuuidportsportiddnio.txt +++ b/docs/api/examples/post_vpcsuuidportsportiddnio.txt @@ -10,11 +10,11 @@ POST /vpcs/{uuid}/ports/{port_id:\d+}/nio HTTP/1.1 HTTP/1.1 201 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 89 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /vpcs/{uuid}/ports/{port_id:\d+}/nio { diff --git a/docs/api/examples/put_projectuuid.txt b/docs/api/examples/put_projectuuid.txt index a1baf6c8..998473c6 100644 --- a/docs/api/examples/put_projectuuid.txt +++ b/docs/api/examples/put_projectuuid.txt @@ -7,15 +7,15 @@ PUT /project/{uuid} HTTP/1.1 HTTP/1.1 200 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 158 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /project/{uuid} { - "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpivbrfsdh", + "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpf9hmfoxi", "temporary": false, - "uuid": "0442fdb6-fc77-4f1c-b996-675b98dc032e" + "uuid": "6cc80657-e8f3-445b-8c1a-e2081ac7d042" } diff --git a/docs/api/udp.rst b/docs/api/udp.rst index f2cdb2ae..6ed3ac5d 100644 --- a/docs/api/udp.rst +++ b/docs/api/udp.rst @@ -11,3 +11,9 @@ Response status codes ********************** - **201**: UDP port allocated +Sample session +*************** + + +.. literalinclude:: examples/post_udp.txt + diff --git a/docs/api/virtualboxuuidcaptureportiddstart.rst b/docs/api/virtualboxuuidcaptureportiddstart.rst index b6a61ca0..342f1893 100644 --- a/docs/api/virtualboxuuidcaptureportiddstart.rst +++ b/docs/api/virtualboxuuidcaptureportiddstart.rst @@ -9,8 +9,8 @@ Start a packet capture on a VirtualBox VM instance Parameters ********** -- **port_id**: ID of the port to start a packet capture - **uuid**: Instance UUID +- **port_id**: ID of the port to start a packet capture Response status codes ********************** diff --git a/docs/api/virtualboxuuidcaptureportiddstop.rst b/docs/api/virtualboxuuidcaptureportiddstop.rst index c336bf6c..a4a35c47 100644 --- a/docs/api/virtualboxuuidcaptureportiddstop.rst +++ b/docs/api/virtualboxuuidcaptureportiddstop.rst @@ -9,8 +9,8 @@ Stop a packet capture on a VirtualBox VM instance Parameters ********** -- **port_id**: ID of the port to stop a packet capture - **uuid**: Instance UUID +- **port_id**: ID of the port to stop a packet capture Response status codes ********************** diff --git a/docs/api/virtualboxuuidportsportiddnio.rst b/docs/api/virtualboxuuidportsportiddnio.rst index 817dc47a..160e5272 100644 --- a/docs/api/virtualboxuuidportsportiddnio.rst +++ b/docs/api/virtualboxuuidportsportiddnio.rst @@ -9,8 +9,8 @@ Add a NIO to a VirtualBox VM instance Parameters ********** -- **port_id**: ID of the port where the nio should be added - **uuid**: Instance UUID +- **port_id**: ID of the port where the nio should be added Response status codes ********************** @@ -31,8 +31,8 @@ Remove a NIO from a VirtualBox VM instance Parameters ********** -- **port_id**: ID of the port from where the nio should be removed - **uuid**: Instance UUID +- **port_id**: ID of the port from where the nio should be removed Response status codes ********************** diff --git a/docs/api/virtualboxvms.rst b/docs/api/virtualboxvms.rst new file mode 100644 index 00000000..c8f79412 --- /dev/null +++ b/docs/api/virtualboxvms.rst @@ -0,0 +1,13 @@ +/virtualbox/vms +--------------------------------------------- + +.. contents:: + +GET /virtualbox/vms +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Get all VirtualBox VMs available + +Response status codes +********************** +- **200**: Success + diff --git a/docs/api/vpcsuuidportsportiddnio.rst b/docs/api/vpcsuuidportsportiddnio.rst index a3075cd0..6226c3cf 100644 --- a/docs/api/vpcsuuidportsportiddnio.rst +++ b/docs/api/vpcsuuidportsportiddnio.rst @@ -9,8 +9,8 @@ Add a NIO to a VPCS instance Parameters ********** -- **port_id**: ID of the port where the nio should be added - **uuid**: Instance UUID +- **port_id**: ID of the port where the nio should be added Response status codes ********************** @@ -31,8 +31,8 @@ Remove a NIO from a VPCS instance Parameters ********** -- **port_id**: ID of the port from where the nio should be removed - **uuid**: Instance UUID +- **port_id**: ID of the port from where the nio should be removed Response status codes ********************** diff --git a/tests/api/test_network.py b/tests/api/test_network.py index 7c6aab6d..fdbee465 100644 --- a/tests/api/test_network.py +++ b/tests/api/test_network.py @@ -17,7 +17,7 @@ def test_udp_allocation(server): - response = server.post('/udp', {}) + response = server.post('/udp', {}, example=True) assert response.status == 201 assert response.json == {'udp_port': 10000} From 4b62d4d82c2fce9f5abe64e8f98d55d408fb2b48 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 28 Jan 2015 15:57:11 +0100 Subject: [PATCH 143/485] py.test timeout and capture log --- dev-requirements.txt | 3 +++ tox.ini | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 9ed5ac2a..bd3f209e 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -4,3 +4,6 @@ sphinx==1.2.3 pytest==2.6.4 ws4py==0.3.4 pep8==1.5.7 +pytest +pytest-timeout +pytest-capturelog diff --git a/tox.ini b/tox.ini index f698cd6f..a6c23dee 100644 --- a/tox.ini +++ b/tox.ini @@ -10,4 +10,4 @@ ignore = E501 [pytest] norecursedirs = old_tests .tox - +timeout = 10 From 8bc26420b702503dc657b29c5c8be56a28c87394 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 30 Jan 2015 14:57:25 +0100 Subject: [PATCH 144/485] If not script file is setted we use the default from VPCS --- gns3server/modules/vpcs/vpcs_vm.py | 9 ++++++++- tests/modules/vpcs/test_vpcs_vm.py | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index 1872da3e..a3852e88 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -165,8 +165,15 @@ class VPCSVM(BaseVM): @property def startup_script(self): """Return the content of the current startup script""" + if self._script_file is None: - return None + # If the default VPCS file exist we use it + path = os.path.join(self.working_dir, 'startup.vpc') + if os.path.exists(path): + self._script_file = path + else: + return None + try: with open(self._script_file) as f: return f.read() diff --git a/tests/modules/vpcs/test_vpcs_vm.py b/tests/modules/vpcs/test_vpcs_vm.py index 231bf0ae..f87882b3 100644 --- a/tests/modules/vpcs/test_vpcs_vm.py +++ b/tests/modules/vpcs/test_vpcs_vm.py @@ -155,6 +155,20 @@ def test_get_startup_script(vm): assert vm.startup_script == content +def test_get_startup_script_using_default_script(vm): + content = "echo GNS3 VPCS\nip 192.168.1.2\n" + + # Reset script file location + vm.script_file = None + + filepath = os.path.join(vm.working_dir, 'startup.vpc') + with open(filepath, 'w+') as f: + assert f.write(content) + + assert vm.startup_script == content + assert vm.script_file == filepath + + def test_change_console_port(vm, port_manager): port1 = port_manager.get_free_console_port() port2 = port_manager.get_free_console_port() From 58fd9043ed99c6a896cb72cdbba163e128792cad Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 30 Jan 2015 17:55:46 +0100 Subject: [PATCH 145/485] Clean dependencies --- dev-requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index bd3f209e..6dfcfe5c 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -4,6 +4,5 @@ sphinx==1.2.3 pytest==2.6.4 ws4py==0.3.4 pep8==1.5.7 -pytest pytest-timeout pytest-capturelog From 6e29e7711c8bf15e1f7253783daab3a884053acf Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 30 Jan 2015 15:40:00 -0700 Subject: [PATCH 146/485] Update dependencies. --- dev-requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 6dfcfe5c..791cf88b 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -2,7 +2,6 @@ sphinx==1.2.3 pytest==2.6.4 -ws4py==0.3.4 pep8==1.5.7 pytest-timeout pytest-capturelog From fa978b6a2827cacc6588976eb268d09fe54891e8 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 30 Jan 2015 19:36:05 -0700 Subject: [PATCH 147/485] Send all VirtualBox settings when creating the VM. --- gns3server/handlers/virtualbox_handler.py | 7 +- .../modules/virtualbox/virtualbox_vm.py | 34 ++-------- gns3server/schemas/virtualbox.py | 65 ++++++++++++------- 3 files changed, 56 insertions(+), 50 deletions(-) diff --git a/gns3server/handlers/virtualbox_handler.py b/gns3server/handlers/virtualbox_handler.py index 7f2ce495..81591828 100644 --- a/gns3server/handlers/virtualbox_handler.py +++ b/gns3server/handlers/virtualbox_handler.py @@ -62,7 +62,12 @@ class VirtualBoxHandler: request.json.get("uuid"), request.json["vmname"], request.json["linked_clone"], - console=request.json.get("console")) + adapters=request.json.get("adapters", 0)) + + for name, value in request.json.items(): + if hasattr(vm, name) and getattr(vm, name) != value: + setattr(vm, name, value) + response.set_status(201) response.json(vm) diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index f2137c93..b462880d 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -31,7 +31,7 @@ import asyncio from pkg_resources import parse_version from .virtualbox_error import VirtualBoxError from ..adapters.ethernet_adapter import EthernetAdapter -from .telnet_server import TelnetServer # port TelnetServer to asyncio +from .telnet_server import TelnetServer # TODO: port TelnetServer to asyncio from ..base_vm import BaseVM if sys.platform.startswith('win'): @@ -47,7 +47,7 @@ class VirtualBoxVM(BaseVM): VirtualBox VM implementation. """ - def __init__(self, name, uuid, project, manager, vmname, linked_clone, console=None): + def __init__(self, name, uuid, project, manager, vmname, linked_clone, adapters=0): super().__init__(name, uuid, project, manager) @@ -58,7 +58,8 @@ class VirtualBoxVM(BaseVM): self._serial_pipe = None # VirtualBox settings - self._console = console + self._console = None + self._adapters = adapters self._ethernet_adapters = [] self._headless = False self._enable_remote_console = False @@ -78,10 +79,9 @@ class VirtualBoxVM(BaseVM): "console": self.console, "project_uuid": self.project.uuid, "vmname": self.vmname, - "linked_clone": self.linked_clone, "headless": self.headless, "enable_remote_console": self.enable_remote_console, - "adapters": self.adapters, + "adapters": self._adapters, "adapter_type": self.adapter_type, "adapter_start_index": self.adapter_start_index} @@ -152,8 +152,7 @@ class VirtualBoxVM(BaseVM): else: yield from self._create_linked_clone() - # set 2 adapters by default - # yield from self.set_adapters(2) + yield from self.set_adapters(self._adapters) @asyncio.coroutine def start(self): @@ -432,26 +431,6 @@ class VirtualBoxVM(BaseVM): # yield from self._modify_vm('--name "{}"'.format(vmname)) self._vmname = vmname - @property - def linked_clone(self): - """ - Returns either the VM is a linked clone. - - :returns: boolean - """ - - return self._linked_clone - - @property - def adapters(self): - """ - Returns the number of Ethernet adapters for this VirtualBox VM instance. - - :returns: number of adapters - """ - - return len(self._ethernet_adapters) - @asyncio.coroutine def set_adapters(self, adapters): """ @@ -472,6 +451,7 @@ class VirtualBoxVM(BaseVM): continue self._ethernet_adapters.append(EthernetAdapter()) + self._adapters = len(self._ethernet_adapters) log.info("VirtualBox VM '{name}' [{uuid}]: number of Ethernet adapters changed to {adapters}".format(name=self.name, uuid=self.uuid, adapters=adapters)) diff --git a/gns3server/schemas/virtualbox.py b/gns3server/schemas/virtualbox.py index 091682e4..713d6f74 100644 --- a/gns3server/schemas/virtualbox.py +++ b/gns3server/schemas/virtualbox.py @@ -21,24 +21,6 @@ VBOX_CREATE_SCHEMA = { "description": "Request validation to create a new VirtualBox VM instance", "type": "object", "properties": { - "name": { - "description": "VirtualBox VM instance name", - "type": "string", - "minLength": 1, - }, - "vmname": { - "description": "VirtualBox VM name (in VirtualBox itself)", - "type": "string", - "minLength": 1, - }, - "linked_clone": { - "description": "either the VM is a linked clone or not", - "type": "boolean" - }, - "vbox_id": { - "description": "VirtualBox VM instance ID (for project created before GNS3 1.3)", - "type": "integer" - }, "uuid": { "description": "VirtualBox VM instance UUID", "type": "string", @@ -46,6 +28,10 @@ VBOX_CREATE_SCHEMA = { "maxLength": 36, "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" }, + "vbox_id": { + "description": "VirtualBox VM instance ID (for project created before GNS3 1.3)", + "type": "integer" + }, "project_uuid": { "description": "Project UUID", "type": "string", @@ -53,12 +39,51 @@ VBOX_CREATE_SCHEMA = { "maxLength": 36, "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" }, + "linked_clone": { + "description": "either the VM is a linked clone or not", + "type": "boolean" + }, + "name": { + "description": "VirtualBox VM instance name", + "type": "string", + "minLength": 1, + }, + "vmname": { + "description": "VirtualBox VM name (in VirtualBox itself)", + "type": "string", + "minLength": 1, + }, + "adapters": { + "description": "number of adapters", + "type": "integer", + "minimum": 0, + "maximum": 36, # maximum given by the ICH9 chipset in VirtualBox + }, + "adapter_start_index": { + "description": "adapter index from which to start using adapters", + "type": "integer", + "minimum": 0, + "maximum": 35, # maximum given by the ICH9 chipset in VirtualBox + }, + "adapter_type": { + "description": "VirtualBox adapter type", + "type": "string", + "minLength": 1, + }, "console": { "description": "console TCP port", "minimum": 1, "maximum": 65535, "type": "integer" }, + "enable_remote_console": { + "description": "enable the remote console", + "type": "boolean" + }, + "headless": { + "description": "headless mode", + "type": "boolean" + }, }, "additionalProperties": False, "required": ["name", "vmname", "linked_clone", "project_uuid"], @@ -198,10 +223,6 @@ VBOX_OBJECT_SCHEMA = { "type": "string", "minLength": 1, }, - "linked_clone": { - "description": "either the VM is a linked clone or not", - "type": "boolean" - }, "enable_remote_console": { "description": "enable the remote console", "type": "boolean" From e7c9139045e7cd1b127ee2a6eb2fc8a01052ccef Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sat, 31 Jan 2015 11:58:34 -0700 Subject: [PATCH 148/485] Rename /udp entry point to /ports/udp. --- tests/api/test_network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/api/test_network.py b/tests/api/test_network.py index fdbee465..24231366 100644 --- a/tests/api/test_network.py +++ b/tests/api/test_network.py @@ -17,7 +17,7 @@ def test_udp_allocation(server): - response = server.post('/udp', {}, example=True) + response = server.post('/ports/udp', {}, example=True) assert response.status == 201 assert response.json == {'udp_port': 10000} From 22369ade49dda6e1ff571a002d83d3894872d665 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sat, 31 Jan 2015 12:01:23 -0700 Subject: [PATCH 149/485] Rename port_id to port_number for VPCS and adapter_id for VirtualBox to avoid confusion. --- gns3server/handlers/network_handler.py | 2 +- gns3server/handlers/virtualbox_handler.py | 29 +++++++------ gns3server/handlers/vpcs_handler.py | 12 +++--- gns3server/modules/adapters/adapter.py | 26 ++++++------ .../modules/virtualbox/virtualbox_vm.py | 3 +- gns3server/modules/vpcs/vpcs_vm.py | 42 +++++++++---------- tests/api/test_virtualbox.py | 4 +- tests/api/test_vpcs.py | 6 +-- .../modules/virtualbox/test_virtualbox_vm.py | 1 - 9 files changed, 62 insertions(+), 63 deletions(-) diff --git a/gns3server/handlers/network_handler.py b/gns3server/handlers/network_handler.py index c653c704..87dedc29 100644 --- a/gns3server/handlers/network_handler.py +++ b/gns3server/handlers/network_handler.py @@ -24,7 +24,7 @@ class NetworkHandler: @classmethod @Route.post( - r"/udp", + r"/ports/udp", status_codes={ 201: "UDP port allocated", }, diff --git a/gns3server/handlers/virtualbox_handler.py b/gns3server/handlers/virtualbox_handler.py index 81591828..b164a727 100644 --- a/gns3server/handlers/virtualbox_handler.py +++ b/gns3server/handlers/virtualbox_handler.py @@ -227,10 +227,10 @@ class VirtualBoxHandler: response.set_status(204) @Route.post( - r"/virtualbox/{uuid}/ports/{port_id:\d+}/nio", + r"/virtualbox/{uuid}/ports/{adapter_id:\d+}/nio", parameters={ "uuid": "Instance UUID", - "port_id": "ID of the port where the nio should be added" + "adapter_id": "Adapter where the nio should be added" }, status_codes={ 201: "NIO created", @@ -245,16 +245,16 @@ class VirtualBoxHandler: vbox_manager = VirtualBox.instance() vm = vbox_manager.get_vm(request.match_info["uuid"]) nio = vbox_manager.create_nio(vbox_manager.vboxmanage_path, request.json) - vm.port_add_nio_binding(int(request.match_info["port_id"]), nio) + vm.port_add_nio_binding(int(request.match_info["adapter_id"]), nio) response.set_status(201) response.json(nio) @classmethod @Route.delete( - r"/virtualbox/{uuid}/ports/{port_id:\d+}/nio", + r"/virtualbox/{uuid}/ports/{adapter_id:\d+}/nio", parameters={ "uuid": "Instance UUID", - "port_id": "ID of the port from where the nio should be removed" + "adapter_id": "Adapter from where the nio should be removed" }, status_codes={ 204: "NIO deleted", @@ -266,14 +266,14 @@ class VirtualBoxHandler: vbox_manager = VirtualBox.instance() vm = vbox_manager.get_vm(request.match_info["uuid"]) - vm.port_remove_nio_binding(int(request.match_info["port_id"])) + vm.port_remove_nio_binding(int(request.match_info["adapter_id"])) response.set_status(204) @Route.post( - r"/virtualbox/{uuid}/capture/{port_id:\d+}/start", + r"/virtualbox/{uuid}/capture/{adapter_id:\d+}/start", parameters={ "uuid": "Instance UUID", - "port_id": "ID of the port to start a packet capture" + "adapter_id": "Adapter to start a packet capture" }, status_codes={ 200: "Capture started", @@ -286,17 +286,16 @@ class VirtualBoxHandler: vbox_manager = VirtualBox.instance() vm = vbox_manager.get_vm(request.match_info["uuid"]) - port_id = int(request.match_info["port_id"]) + adapter_id = int(request.match_info["adapter_id"]) pcap_file_path = os.path.join(vm.project.capture_working_directory(), request.json["filename"]) - vm.start_capture(port_id, pcap_file_path) - response.json({"port_id": port_id, - "pcap_file_path": pcap_file_path}) + vm.start_capture(adapter_id, pcap_file_path) + response.json({"pcap_file_path": pcap_file_path}) @Route.post( - r"/virtualbox/{uuid}/capture/{port_id:\d+}/stop", + r"/virtualbox/{uuid}/capture/{adapter_id:\d+}/stop", parameters={ "uuid": "Instance UUID", - "port_id": "ID of the port to stop a packet capture" + "adapter_id": "Adapter to stop a packet capture" }, status_codes={ 204: "Capture stopped", @@ -308,5 +307,5 @@ class VirtualBoxHandler: vbox_manager = VirtualBox.instance() vm = vbox_manager.get_vm(request.match_info["uuid"]) - vm.stop_capture(int(request.match_info["port_id"])) + vm.stop_capture(int(request.match_info["adapter_id"])) response.set_status(204) diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index cc90e806..060ee587 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -168,10 +168,10 @@ class VPCSHandler: response.set_status(204) @Route.post( - r"/vpcs/{uuid}/ports/{port_id:\d+}/nio", + r"/vpcs/{uuid}/ports/{port_number:\d+}/nio", parameters={ "uuid": "Instance UUID", - "port_id": "ID of the port where the nio should be added" + "port_number": "Port where the nio should be added" }, status_codes={ 201: "NIO created", @@ -186,16 +186,16 @@ class VPCSHandler: vpcs_manager = VPCS.instance() vm = vpcs_manager.get_vm(request.match_info["uuid"]) nio = vpcs_manager.create_nio(vm.vpcs_path, request.json) - vm.port_add_nio_binding(int(request.match_info["port_id"]), nio) + vm.port_add_nio_binding(int(request.match_info["port_number"]), nio) response.set_status(201) response.json(nio) @classmethod @Route.delete( - r"/vpcs/{uuid}/ports/{port_id:\d+}/nio", + r"/vpcs/{uuid}/ports/{port_number:\d+}/nio", parameters={ "uuid": "Instance UUID", - "port_id": "ID of the port from where the nio should be removed" + "port_number": "Port from where the nio should be removed" }, status_codes={ 204: "NIO deleted", @@ -207,5 +207,5 @@ class VPCSHandler: vpcs_manager = VPCS.instance() vm = vpcs_manager.get_vm(request.match_info["uuid"]) - vm.port_remove_nio_binding(int(request.match_info["port_id"])) + vm.port_remove_nio_binding(int(request.match_info["port_number"])) response.set_status(204) diff --git a/gns3server/modules/adapters/adapter.py b/gns3server/modules/adapters/adapter.py index ade660f9..33c916c4 100644 --- a/gns3server/modules/adapters/adapter.py +++ b/gns3server/modules/adapters/adapter.py @@ -29,8 +29,8 @@ class Adapter(object): self._interfaces = interfaces self._ports = {} - for port_id in range(0, interfaces): - self._ports[port_id] = None + for port_number in range(0, interfaces): + self._ports[port_number] = None def removable(self): """ @@ -42,7 +42,7 @@ class Adapter(object): return True - def port_exists(self, port_id): + def port_exists(self, port_number): """ Checks if a port exists on this adapter. @@ -50,39 +50,39 @@ class Adapter(object): False otherwise. """ - if port_id in self._ports: + if port_number in self._ports: return True return False - def add_nio(self, port_id, nio): + def add_nio(self, port_number, nio): """ Adds a NIO to a port on this adapter. - :param port_id: port ID (integer) + :param port_number: port number (integer) :param nio: NIO instance """ - self._ports[port_id] = nio + self._ports[port_number] = nio - def remove_nio(self, port_id): + def remove_nio(self, port_number): """ Removes a NIO from a port on this adapter. - :param port_id: port ID (integer) + :param port_number: port number (integer) """ - self._ports[port_id] = None + self._ports[port_number] = None - def get_nio(self, port_id): + def get_nio(self, port_number): """ Returns the NIO assigned to a port. - :params port_id: port ID (integer) + :params port_number: port number (integer) :returns: NIO instance """ - return self._ports[port_id] + return self._ports[port_number] @property def ports(self): diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index b462880d..047dea45 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -152,7 +152,8 @@ class VirtualBoxVM(BaseVM): else: yield from self._create_linked_clone() - yield from self.set_adapters(self._adapters) + if self._adapters: + yield from self.set_adapters(self._adapters) @asyncio.coroutine def start(self): diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index a3852e88..e5f5ed28 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -317,47 +317,47 @@ class VPCSVM(BaseVM): return True return False - def port_add_nio_binding(self, port_id, nio): + def port_add_nio_binding(self, port_number, nio): """ Adds a port NIO binding. - :param port_id: port ID + :param port_number: port number :param nio: NIO instance to add to the slot/port """ - if not self._ethernet_adapter.port_exists(port_id): - raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter, - port_id=port_id)) + if not self._ethernet_adapter.port_exists(port_number): + raise VPCSError("Port {port_number} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter, + port_number=port_number)) - self._ethernet_adapter.add_nio(port_id, nio) - log.info("VPCS {name} {uuid}]: {nio} added to port {port_id}".format(name=self._name, - uuid=self.uuid, - nio=nio, - port_id=port_id)) + self._ethernet_adapter.add_nio(port_number, nio) + log.info("VPCS {name} {uuid}]: {nio} added to port {port_number}".format(name=self._name, + uuid=self.uuid, + nio=nio, + port_number=port_number)) return nio - def port_remove_nio_binding(self, port_id): + def port_remove_nio_binding(self, port_number): """ Removes a port NIO binding. - :param port_id: port ID + :param port_number: port number :returns: NIO instance """ - if not self._ethernet_adapter.port_exists(port_id): - raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter, - port_id=port_id)) + if not self._ethernet_adapter.port_exists(port_number): + raise VPCSError("Port {port_number} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter, + port_number=port_number)) - nio = self._ethernet_adapter.get_nio(port_id) + nio = self._ethernet_adapter.get_nio(port_number) if str(nio) == "NIO UDP": self.manager.port_manager.release_udp_port(nio.lport) - self._ethernet_adapter.remove_nio(port_id) + self._ethernet_adapter.remove_nio(port_number) - log.info("VPCS {name} [{uuid}]: {nio} removed from port {port_id}".format(name=self._name, - uuid=self.uuid, - nio=nio, - port_id=port_id)) + log.info("VPCS {name} [{uuid}]: {nio} removed from port {port_number}".format(name=self._name, + uuid=self.uuid, + nio=nio, + port_number=port_number)) return nio def _build_command(self): diff --git a/tests/api/test_virtualbox.py b/tests/api/test_virtualbox.py index d056bec9..9f90aea4 100644 --- a/tests/api/test_virtualbox.py +++ b/tests/api/test_virtualbox.py @@ -94,7 +94,7 @@ def test_vbox_nio_create_udp(server, vm): "rhost": "127.0.0.1"}, example=True) assert response.status == 201 - assert response.route == "/virtualbox/{uuid}/ports/{port_id:\d+}/nio" + assert response.route == "/virtualbox/{uuid}/ports/{adapter_id:\d+}/nio" assert response.json["type"] == "nio_udp" @@ -105,7 +105,7 @@ def test_vbox_delete_nio(server, vm): "rhost": "127.0.0.1"}) response = server.delete("/virtualbox/{}/ports/0/nio".format(vm["uuid"]), example=True) assert response.status == 204 - assert response.route == "/virtualbox/{uuid}/ports/{port_id:\d+}/nio" + assert response.route == "/virtualbox/{uuid}/ports/{adapter_id:\d+}/nio" def test_vpcs_update(server, vm, free_console_port): diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index d07dcc50..db318494 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -82,7 +82,7 @@ def test_vpcs_nio_create_udp(server, vm): "rhost": "127.0.0.1"}, example=True) assert response.status == 201 - assert response.route == "/vpcs/{uuid}/ports/{port_id:\d+}/nio" + assert response.route == "/vpcs/{uuid}/ports/{port_number:\d+}/nio" assert response.json["type"] == "nio_udp" @@ -91,7 +91,7 @@ def test_vpcs_nio_create_tap(server, vm): response = server.post("/vpcs/{}/ports/0/nio".format(vm["uuid"]), {"type": "nio_tap", "tap_device": "test"}) assert response.status == 201 - assert response.route == "/vpcs/{uuid}/ports/{port_id:\d+}/nio" + assert response.route == "/vpcs/{uuid}/ports/{port_number:\d+}/nio" assert response.json["type"] == "nio_tap" @@ -102,7 +102,7 @@ def test_vpcs_delete_nio(server, vm): "rhost": "127.0.0.1"}) response = server.delete("/vpcs/{}/ports/0/nio".format(vm["uuid"]), example=True) assert response.status == 204 - assert response.route == "/vpcs/{uuid}/ports/{port_id:\d+}/nio" + assert response.route == "/vpcs/{uuid}/ports/{port_number:\d+}/nio" def test_vpcs_start(server, vm): diff --git a/tests/modules/virtualbox/test_virtualbox_vm.py b/tests/modules/virtualbox/test_virtualbox_vm.py index f4c889bf..049be0d8 100644 --- a/tests/modules/virtualbox/test_virtualbox_vm.py +++ b/tests/modules/virtualbox/test_virtualbox_vm.py @@ -41,7 +41,6 @@ def test_vm(project, manager): assert vm.name == "test" assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0f" assert vm.vmname == "test" - assert vm.linked_clone is False def test_vm_valid_virtualbox_api_version(loop, project, manager): From 8a00d30e23f2ead74920c816976fb9edad7a4f73 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sat, 31 Jan 2015 12:07:30 -0700 Subject: [PATCH 150/485] Change ports to adapters in VirtualBox API entry points. --- gns3server/handlers/virtualbox_handler.py | 4 ++-- tests/api/test_virtualbox.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/gns3server/handlers/virtualbox_handler.py b/gns3server/handlers/virtualbox_handler.py index b164a727..69ddba1a 100644 --- a/gns3server/handlers/virtualbox_handler.py +++ b/gns3server/handlers/virtualbox_handler.py @@ -227,7 +227,7 @@ class VirtualBoxHandler: response.set_status(204) @Route.post( - r"/virtualbox/{uuid}/ports/{adapter_id:\d+}/nio", + r"/virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio", parameters={ "uuid": "Instance UUID", "adapter_id": "Adapter where the nio should be added" @@ -251,7 +251,7 @@ class VirtualBoxHandler: @classmethod @Route.delete( - r"/virtualbox/{uuid}/ports/{adapter_id:\d+}/nio", + r"/virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio", parameters={ "uuid": "Instance UUID", "adapter_id": "Adapter from where the nio should be removed" diff --git a/tests/api/test_virtualbox.py b/tests/api/test_virtualbox.py index 9f90aea4..51c17495 100644 --- a/tests/api/test_virtualbox.py +++ b/tests/api/test_virtualbox.py @@ -88,24 +88,24 @@ def test_vbox_reload(server, vm): def test_vbox_nio_create_udp(server, vm): - response = server.post("/virtualbox/{}/ports/0/nio".format(vm["uuid"]), {"type": "nio_udp", + response = server.post("/virtualbox/{}/adapters/0/nio".format(vm["uuid"]), {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"}, example=True) assert response.status == 201 - assert response.route == "/virtualbox/{uuid}/ports/{adapter_id:\d+}/nio" + assert response.route == "/virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio" assert response.json["type"] == "nio_udp" def test_vbox_delete_nio(server, vm): - server.post("/virtualbox/{}/ports/0/nio".format(vm["uuid"]), {"type": "nio_udp", + server.post("/virtualbox/{}/adapters/0/nio".format(vm["uuid"]), {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"}) - response = server.delete("/virtualbox/{}/ports/0/nio".format(vm["uuid"]), example=True) + response = server.delete("/virtualbox/{}/adapters/0/nio".format(vm["uuid"]), example=True) assert response.status == 204 - assert response.route == "/virtualbox/{uuid}/ports/{adapter_id:\d+}/nio" + assert response.route == "/virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio" def test_vpcs_update(server, vm, free_console_port): From 334835c985c7857cf0956f0128291a7a514de00c Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sat, 31 Jan 2015 14:34:49 -0700 Subject: [PATCH 151/485] PEP8 + documentation. --- docs/api/examples/delete_projectuuid.txt | 4 +- docs/api/examples/get_interfaces.txt | 42 +++++++------------ docs/api/examples/get_projectuuid.txt | 4 +- docs/api/examples/get_version.txt | 4 +- docs/api/examples/get_virtualboxuuid.txt | 9 ++-- docs/api/examples/get_vpcsuuid.txt | 6 +-- docs/api/examples/post_projectuuidclose.txt | 4 +- docs/api/examples/post_projectuuidcommit.txt | 4 +- docs/api/examples/post_version.txt | 4 +- docs/api/examples/post_virtualbox.txt | 9 ++-- docs/api/examples/post_vpcs.txt | 6 +-- docs/api/examples/put_projectuuid.txt | 10 ++--- docs/api/virtualbox.rst | 6 ++- docs/api/virtualboxuuid.rst | 2 - gns3server/handlers/virtualbox_handler.py | 1 + gns3server/modules/nios/nio.py | 1 + gns3server/modules/port_manager.py | 1 + gns3server/modules/project.py | 1 + .../modules/virtualbox/virtualbox_vm.py | 1 + tests/api/test_virtualbox.py | 12 +++--- 20 files changed, 62 insertions(+), 69 deletions(-) diff --git a/docs/api/examples/delete_projectuuid.txt b/docs/api/examples/delete_projectuuid.txt index 9d63e2e0..68989437 100644 --- a/docs/api/examples/delete_projectuuid.txt +++ b/docs/api/examples/delete_projectuuid.txt @@ -5,9 +5,9 @@ DELETE /project/{uuid} HTTP/1.1 HTTP/1.1 204 -CONNECTION: keep-alive +CONNECTION: close CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /project/{uuid} diff --git a/docs/api/examples/get_interfaces.txt b/docs/api/examples/get_interfaces.txt index 7e21daff..9489d231 100644 --- a/docs/api/examples/get_interfaces.txt +++ b/docs/api/examples/get_interfaces.txt @@ -5,48 +5,36 @@ GET /interfaces HTTP/1.1 HTTP/1.1 200 -CONNECTION: keep-alive -CONTENT-LENGTH: 520 +CONNECTION: close +CONTENT-LENGTH: 364 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /interfaces [ { - "id": "lo0", - "name": "lo0" + "id": "lo", + "name": "lo" }, { - "id": "gif0", - "name": "gif0" + "id": "eth0", + "name": "eth0" }, { - "id": "stf0", - "name": "stf0" + "id": "wlan0", + "name": "wlan0" }, { - "id": "en0", - "name": "en0" + "id": "vmnet1", + "name": "vmnet1" }, { - "id": "en1", - "name": "en1" + "id": "vmnet8", + "name": "vmnet8" }, { - "id": "fw0", - "name": "fw0" - }, - { - "id": "en2", - "name": "en2" - }, - { - "id": "p2p0", - "name": "p2p0" - }, - { - "id": "bridge0", - "name": "bridge0" + "id": "vboxnet0", + "name": "vboxnet0" } ] diff --git a/docs/api/examples/get_projectuuid.txt b/docs/api/examples/get_projectuuid.txt index 2388771c..bed046ad 100644 --- a/docs/api/examples/get_projectuuid.txt +++ b/docs/api/examples/get_projectuuid.txt @@ -5,11 +5,11 @@ GET /project/{uuid} HTTP/1.1 HTTP/1.1 200 -CONNECTION: keep-alive +CONNECTION: close CONTENT-LENGTH: 102 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /project/{uuid} { diff --git a/docs/api/examples/get_version.txt b/docs/api/examples/get_version.txt index 39509cda..59bdb128 100644 --- a/docs/api/examples/get_version.txt +++ b/docs/api/examples/get_version.txt @@ -5,11 +5,11 @@ GET /version HTTP/1.1 HTTP/1.1 200 -CONNECTION: keep-alive +CONNECTION: close CONTENT-LENGTH: 29 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /version { diff --git a/docs/api/examples/get_virtualboxuuid.txt b/docs/api/examples/get_virtualboxuuid.txt index 446e6e1c..a482bfd4 100644 --- a/docs/api/examples/get_virtualboxuuid.txt +++ b/docs/api/examples/get_virtualboxuuid.txt @@ -5,11 +5,11 @@ GET /virtualbox/{uuid} HTTP/1.1 HTTP/1.1 200 -CONNECTION: keep-alive -CONTENT-LENGTH: 375 +CONNECTION: close +CONTENT-LENGTH: 348 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /virtualbox/{uuid} { @@ -19,9 +19,8 @@ X-ROUTE: /virtualbox/{uuid} "console": 2001, "enable_remote_console": false, "headless": false, - "linked_clone": false, "name": "VMTEST", "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "uuid": "be1fa0fe-cd51-41e0-9806-2bac0f5f50ba", + "uuid": "7d0bf59f-0a8c-4382-b5ed-23902653d8c5", "vmname": "VMTEST" } diff --git a/docs/api/examples/get_vpcsuuid.txt b/docs/api/examples/get_vpcsuuid.txt index 4112dde4..c45b8878 100644 --- a/docs/api/examples/get_vpcsuuid.txt +++ b/docs/api/examples/get_vpcsuuid.txt @@ -5,11 +5,11 @@ GET /vpcs/{uuid} HTTP/1.1 HTTP/1.1 200 -CONNECTION: keep-alive +CONNECTION: close CONTENT-LENGTH: 213 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /vpcs/{uuid} { @@ -18,5 +18,5 @@ X-ROUTE: /vpcs/{uuid} "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, "startup_script": null, - "uuid": "a474c92d-c9d2-4f53-bbe2-64493f8f07cc" + "uuid": "5af7ac19-3064-4c8d-8355-fb0c933bbf37" } diff --git a/docs/api/examples/post_projectuuidclose.txt b/docs/api/examples/post_projectuuidclose.txt index 03ae88bc..d038c178 100644 --- a/docs/api/examples/post_projectuuidclose.txt +++ b/docs/api/examples/post_projectuuidclose.txt @@ -5,9 +5,9 @@ POST /project/{uuid}/close HTTP/1.1 HTTP/1.1 204 -CONNECTION: keep-alive +CONNECTION: close CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /project/{uuid}/close diff --git a/docs/api/examples/post_projectuuidcommit.txt b/docs/api/examples/post_projectuuidcommit.txt index 2fe9c38f..b5d0c2d9 100644 --- a/docs/api/examples/post_projectuuidcommit.txt +++ b/docs/api/examples/post_projectuuidcommit.txt @@ -5,9 +5,9 @@ POST /project/{uuid}/commit HTTP/1.1 HTTP/1.1 204 -CONNECTION: keep-alive +CONNECTION: close CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /project/{uuid}/commit diff --git a/docs/api/examples/post_version.txt b/docs/api/examples/post_version.txt index 1d4ecc7d..45ef1069 100644 --- a/docs/api/examples/post_version.txt +++ b/docs/api/examples/post_version.txt @@ -7,11 +7,11 @@ POST /version HTTP/1.1 HTTP/1.1 200 -CONNECTION: keep-alive +CONNECTION: close CONTENT-LENGTH: 29 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /version { diff --git a/docs/api/examples/post_virtualbox.txt b/docs/api/examples/post_virtualbox.txt index 3eff439d..60213f89 100644 --- a/docs/api/examples/post_virtualbox.txt +++ b/docs/api/examples/post_virtualbox.txt @@ -10,11 +10,11 @@ POST /virtualbox HTTP/1.1 HTTP/1.1 201 -CONNECTION: keep-alive -CONTENT-LENGTH: 369 +CONNECTION: close +CONTENT-LENGTH: 342 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /virtualbox { @@ -24,9 +24,8 @@ X-ROUTE: /virtualbox "console": 2000, "enable_remote_console": false, "headless": false, - "linked_clone": false, "name": "VM1", "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "uuid": "8b5bbfa3-0682-4d65-ae8b-a1aea9dc40e5", + "uuid": "4cdea9da-6611-44c3-a085-a3bc4dae7883", "vmname": "VM1" } diff --git a/docs/api/examples/post_vpcs.txt b/docs/api/examples/post_vpcs.txt index 2eadd535..4f23a01a 100644 --- a/docs/api/examples/post_vpcs.txt +++ b/docs/api/examples/post_vpcs.txt @@ -8,11 +8,11 @@ POST /vpcs HTTP/1.1 HTTP/1.1 201 -CONNECTION: keep-alive +CONNECTION: close CONTENT-LENGTH: 213 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /vpcs { @@ -21,5 +21,5 @@ X-ROUTE: /vpcs "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, "startup_script": null, - "uuid": "688ff2f8-08c5-4218-8e19-99f1ac7fc20d" + "uuid": "2cefa3e7-6867-4282-a606-c42d94c7852e" } diff --git a/docs/api/examples/put_projectuuid.txt b/docs/api/examples/put_projectuuid.txt index 998473c6..472b1d5e 100644 --- a/docs/api/examples/put_projectuuid.txt +++ b/docs/api/examples/put_projectuuid.txt @@ -7,15 +7,15 @@ PUT /project/{uuid} HTTP/1.1 HTTP/1.1 200 -CONNECTION: keep-alive -CONTENT-LENGTH: 158 +CONNECTION: close +CONTENT-LENGTH: 114 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /project/{uuid} { - "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpf9hmfoxi", + "location": "/tmp/tmpjan_7gw5", "temporary": false, - "uuid": "6cc80657-e8f3-445b-8c1a-e2081ac7d042" + "uuid": "88146503-b509-40a4-ae43-0f17100c767a" } diff --git a/docs/api/virtualbox.rst b/docs/api/virtualbox.rst index 384b366e..d1f40822 100644 --- a/docs/api/virtualbox.rst +++ b/docs/api/virtualbox.rst @@ -19,7 +19,12 @@ Input + + + + + @@ -40,7 +45,6 @@ Output - diff --git a/docs/api/virtualboxuuid.rst b/docs/api/virtualboxuuid.rst index c744a107..aea02b3a 100644 --- a/docs/api/virtualboxuuid.rst +++ b/docs/api/virtualboxuuid.rst @@ -28,7 +28,6 @@ Output - @@ -84,7 +83,6 @@ Output - diff --git a/gns3server/handlers/virtualbox_handler.py b/gns3server/handlers/virtualbox_handler.py index 69ddba1a..ef074d51 100644 --- a/gns3server/handlers/virtualbox_handler.py +++ b/gns3server/handlers/virtualbox_handler.py @@ -26,6 +26,7 @@ from ..modules.virtualbox import VirtualBox class VirtualBoxHandler: + """ API entry points for VirtualBox. """ diff --git a/gns3server/modules/nios/nio.py b/gns3server/modules/nios/nio.py index eee5f1d5..3c8a6b9e 100644 --- a/gns3server/modules/nios/nio.py +++ b/gns3server/modules/nios/nio.py @@ -21,6 +21,7 @@ Base interface for NIOs. class NIO(object): + """ Network Input/Output. """ diff --git a/gns3server/modules/port_manager.py b/gns3server/modules/port_manager.py index 1a5f294f..d70548fc 100644 --- a/gns3server/modules/port_manager.py +++ b/gns3server/modules/port_manager.py @@ -25,6 +25,7 @@ log = logging.getLogger(__name__) class PortManager: + """ :param host: IP address to bind for console connections """ diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index 86985f42..afd6c271 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -30,6 +30,7 @@ log = logging.getLogger(__name__) class Project: + """ A project contains a list of VM. In theory VM are isolated project/project. diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 047dea45..a01479a9 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -43,6 +43,7 @@ log = logging.getLogger(__name__) class VirtualBoxVM(BaseVM): + """ VirtualBox VM implementation. """ diff --git a/tests/api/test_virtualbox.py b/tests/api/test_virtualbox.py index 51c17495..4c7a7e5c 100644 --- a/tests/api/test_virtualbox.py +++ b/tests/api/test_virtualbox.py @@ -89,9 +89,9 @@ def test_vbox_reload(server, vm): def test_vbox_nio_create_udp(server, vm): response = server.post("/virtualbox/{}/adapters/0/nio".format(vm["uuid"]), {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"}, + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}, example=True) assert response.status == 201 assert response.route == "/virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio" @@ -100,9 +100,9 @@ def test_vbox_nio_create_udp(server, vm): def test_vbox_delete_nio(server, vm): server.post("/virtualbox/{}/adapters/0/nio".format(vm["uuid"]), {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"}) + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}) response = server.delete("/virtualbox/{}/adapters/0/nio".format(vm["uuid"]), example=True) assert response.status == 204 assert response.route == "/virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio" From 8d471a89a85c0937ec748d96616880b292420aaf Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sun, 1 Feb 2015 15:56:10 -0700 Subject: [PATCH 152/485] Check for OSError when starting the server. --- gns3server/main.py | 2 +- gns3server/server.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/gns3server/main.py b/gns3server/main.py index f400f8cd..f94c8e40 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -79,7 +79,7 @@ def parse_arguments(): parser.add_argument("--ssl", action="store_true", help="run in SSL mode") parser.add_argument("--certfile", help="SSL cert file", default="") parser.add_argument("--certkey", help="SSL key file", default="") - parser.add_argument("-L", "--local", action="store_true", help="local mode (allow some insecure operations)") + parser.add_argument("-L", "--local", action="store_true", help="local mode (allows some insecure operations)") parser.add_argument("-A", "--allow", action="store_true", help="allow remote connections to local console ports") parser.add_argument("-q", "--quiet", action="store_true", help="do not show logs on stdout") parser.add_argument("-d", "--debug", action="store_true", help="show debug logs and enable code live reload") diff --git a/gns3server/server.py b/gns3server/server.py index 00cd83bf..0ae237c1 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -53,7 +53,12 @@ class Server: @asyncio.coroutine def _run_application(self, app, ssl_context=None): - server = yield from self._loop.create_server(app.make_handler(), self._host, self._port, ssl=ssl_context) + try: + server = yield from self._loop.create_server(app.make_handler(), self._host, self._port, ssl=ssl_context) + except OSError as e: + log.critical("Could not start the server: {}".format(e)) + self._loop.stop() + return return server def _stop_application(self): From 0c90393b5b79f9ab2c7e42b100f0a09096c4c548 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sun, 1 Feb 2015 16:55:08 -0700 Subject: [PATCH 153/485] Send explicit error message when client is checking for the server version. --- gns3server/handlers/version_handler.py | 7 ++++--- gns3server/schemas/version.py | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gns3server/handlers/version_handler.py b/gns3server/handlers/version_handler.py index abdb43c5..6d020967 100644 --- a/gns3server/handlers/version_handler.py +++ b/gns3server/handlers/version_handler.py @@ -29,7 +29,7 @@ class VersionHandler: description="Retrieve the server version number", output=VERSION_SCHEMA) def version(request, response): - response.json({'version': __version__}) + response.json({"version": __version__}) @classmethod @Route.post( @@ -43,5 +43,6 @@ class VersionHandler: }) def check_version(request, response): if request.json["version"] != __version__: - raise HTTPConflict(reason="Invalid version") - response.json({'version': __version__}) + raise HTTPConflict(text="Client version {} differs with server version {}".format(request.json["version"], + __version__)) + response.json({"version": __version__}) diff --git a/gns3server/schemas/version.py b/gns3server/schemas/version.py index bf9d41f4..127084df 100644 --- a/gns3server/schemas/version.py +++ b/gns3server/schemas/version.py @@ -24,7 +24,6 @@ VERSION_SCHEMA = { "version": { "description": "Version number human readable", "type": "string", - "minLength": 5, } } } From 57b35d5758411f964f5df395e9cd8ddeae5e5019 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sun, 1 Feb 2015 17:22:31 -0700 Subject: [PATCH 154/485] Fix tests + PEP8 + documentation --- docs/api/examples/get_virtualboxuuid.txt | 2 +- docs/api/examples/get_vpcsuuid.txt | 2 +- docs/api/examples/post_virtualbox.txt | 2 +- docs/api/examples/post_vpcs.txt | 2 +- docs/api/examples/put_projectuuid.txt | 4 ++-- scripts/pep8.sh | 2 +- tests/api/test_version.py | 3 ++- 7 files changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/api/examples/get_virtualboxuuid.txt b/docs/api/examples/get_virtualboxuuid.txt index a482bfd4..739e864d 100644 --- a/docs/api/examples/get_virtualboxuuid.txt +++ b/docs/api/examples/get_virtualboxuuid.txt @@ -21,6 +21,6 @@ X-ROUTE: /virtualbox/{uuid} "headless": false, "name": "VMTEST", "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "uuid": "7d0bf59f-0a8c-4382-b5ed-23902653d8c5", + "uuid": "0d15855e-8fb4-41af-81b0-a150c7576d5a", "vmname": "VMTEST" } diff --git a/docs/api/examples/get_vpcsuuid.txt b/docs/api/examples/get_vpcsuuid.txt index c45b8878..61a263a0 100644 --- a/docs/api/examples/get_vpcsuuid.txt +++ b/docs/api/examples/get_vpcsuuid.txt @@ -18,5 +18,5 @@ X-ROUTE: /vpcs/{uuid} "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, "startup_script": null, - "uuid": "5af7ac19-3064-4c8d-8355-fb0c933bbf37" + "uuid": "2ddb0fa3-1010-4f48-b295-caa1c414b4a2" } diff --git a/docs/api/examples/post_virtualbox.txt b/docs/api/examples/post_virtualbox.txt index 60213f89..25c025f8 100644 --- a/docs/api/examples/post_virtualbox.txt +++ b/docs/api/examples/post_virtualbox.txt @@ -26,6 +26,6 @@ X-ROUTE: /virtualbox "headless": false, "name": "VM1", "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "uuid": "4cdea9da-6611-44c3-a085-a3bc4dae7883", + "uuid": "33217631-7c24-4acf-b462-c40e0012537a", "vmname": "VM1" } diff --git a/docs/api/examples/post_vpcs.txt b/docs/api/examples/post_vpcs.txt index 4f23a01a..75186c33 100644 --- a/docs/api/examples/post_vpcs.txt +++ b/docs/api/examples/post_vpcs.txt @@ -21,5 +21,5 @@ X-ROUTE: /vpcs "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, "startup_script": null, - "uuid": "2cefa3e7-6867-4282-a606-c42d94c7852e" + "uuid": "b0ed8aae-4d50-4a0c-9d91-0875a5aca533" } diff --git a/docs/api/examples/put_projectuuid.txt b/docs/api/examples/put_projectuuid.txt index 472b1d5e..d629a7c1 100644 --- a/docs/api/examples/put_projectuuid.txt +++ b/docs/api/examples/put_projectuuid.txt @@ -15,7 +15,7 @@ SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /project/{uuid} { - "location": "/tmp/tmpjan_7gw5", + "location": "/tmp/tmpi95b_dec", "temporary": false, - "uuid": "88146503-b509-40a4-ae43-0f17100c767a" + "uuid": "e30bf585-b257-4e14-8f7f-a5e0bdd126dd" } diff --git a/scripts/pep8.sh b/scripts/pep8.sh index ea0694f5..33e0f54a 100755 --- a/scripts/pep8.sh +++ b/scripts/pep8.sh @@ -16,4 +16,4 @@ echo ' find . -name '*.py' -exec autopep8 --in-place -v --aggressive --aggressive \{\} \; -echo "Its 'clean" +echo "It's all clean now!" diff --git a/tests/api/test_version.py b/tests/api/test_version.py index 1e9260f2..0691d8f9 100644 --- a/tests/api/test_version.py +++ b/tests/api/test_version.py @@ -40,7 +40,8 @@ def test_version_invalid_input(server): query = {'version': "0.4.2"} response = server.post('/version', query) assert response.status == 409 - assert response.json == {'message': '409: Invalid version', 'status': 409} + assert response.json == {'message': 'Client version 0.4.2 differs with server version {}'.format(__version__), + 'status': 409} def test_version_invalid_input_schema(server): From a3a304bd1201e40742b0a2d7d0432849c608079e Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sun, 1 Feb 2015 20:43:55 -0700 Subject: [PATCH 155/485] Load port ranges from the config file. --- gns3server/modules/port_manager.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/gns3server/modules/port_manager.py b/gns3server/modules/port_manager.py index d70548fc..514d07ce 100644 --- a/gns3server/modules/port_manager.py +++ b/gns3server/modules/port_manager.py @@ -34,8 +34,6 @@ class PortManager: self._console_host = host self._udp_host = host - self._console_port_range = (2000, 4000) - self._udp_port_range = (10000, 20000) self._used_tcp_ports = set() self._used_udp_ports = set() @@ -43,6 +41,13 @@ class PortManager: server_config = Config.instance().get_section_config("Server") remote_console_connections = server_config.getboolean("allow_remote_console") + console_start_port_range = server_config.getint("console_start_port_range", 2000) + console_end_port_range = server_config.getint("console_end_port_range", 5000) + self._console_port_range = (console_start_port_range, console_end_port_range) + udp_start_port_range = server_config.getint("udp_start_port_range", 10000) + udp_end_port_range = server_config.getint("udp_end_port_range", 20000) + self._udp_port_range = (udp_start_port_range, udp_end_port_range) + if remote_console_connections: log.warning("Remote console connections are allowed") if ipaddress.ip_address(host).version == 6: From 21020a27536bd2ba581b0393041a2f932a0870a8 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 2 Feb 2015 10:49:46 +0100 Subject: [PATCH 156/485] Fix server configuration path --- gns3server/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gns3server/config.py b/gns3server/config.py index 56e26f4b..1729f9aa 100644 --- a/gns3server/config.py +++ b/gns3server/config.py @@ -66,6 +66,7 @@ class Config(object): # 4: /etc/xdg/GNS3.conf # 5: server.conf in the current working directory + appname = "gns3.net" home = os.path.expanduser("~") self._cloud_file = os.path.join(home, ".config", appname, "cloud.conf") filename = "server.conf" @@ -99,7 +100,7 @@ class Config(object): parsed_files = self._config.read(self._files) if not parsed_files: - log.warning("no configuration file could be found or read") + log.warning("No configuration file could be found or read") def get_default_section(self): """ From 6abf420ce1f78057f80099cf0494951bf817f807 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 2 Feb 2015 15:01:48 +0100 Subject: [PATCH 157/485] Support configuration live reload --- gns3server/config.py | 64 ++++++++++++++++++++++------- gns3server/main.py | 10 +++-- tests/test_config.py | 97 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 19 deletions(-) create mode 100644 tests/test_config.py diff --git a/gns3server/config.py b/gns3server/config.py index 1729f9aa..ef36dee4 100644 --- a/gns3server/config.py +++ b/gns3server/config.py @@ -22,6 +22,7 @@ Reads the configuration file and store the settings for the server & modules. import sys import os import configparser +import asyncio import logging log = logging.getLogger(__name__) @@ -33,13 +34,19 @@ class Config(object): """ Configuration file management using configparser. + + :params files: Array of configuration files (optionnal) """ - def __init__(self): + def __init__(self, files=None): + + self._files = files + self._watched_files = {} - appname = "GNS3" if sys.platform.startswith("win"): + appname = "GNS3" + # On windows, the configuration file location can be one of the following: # 1: %APPDATA%/GNS3/server.ini # 2: %APPDATA%/GNS3.ini @@ -51,12 +58,13 @@ class Config(object): common_appdata = os.path.expandvars("%COMMON_APPDATA%") self._cloud_file = os.path.join(appdata, appname, "cloud.ini") filename = "server.ini" - self._files = [os.path.join(appdata, appname, filename), - os.path.join(appdata, appname + ".ini"), - os.path.join(common_appdata, appname, filename), - os.path.join(common_appdata, appname + ".ini"), - filename, - self._cloud_file] + if self._files is None: + self._files = [os.path.join(appdata, appname, filename), + os.path.join(appdata, appname + ".ini"), + os.path.join(common_appdata, appname, filename), + os.path.join(common_appdata, appname + ".ini"), + filename, + self._cloud_file] else: # On UNIX-like platforms, the configuration file location can be one of the following: @@ -70,17 +78,36 @@ class Config(object): home = os.path.expanduser("~") self._cloud_file = os.path.join(home, ".config", appname, "cloud.conf") filename = "server.conf" - self._files = [os.path.join(home, ".config", appname, filename), - os.path.join(home, ".config", appname + ".conf"), - os.path.join("/etc/xdg", appname, filename), - os.path.join("/etc/xdg", appname + ".conf"), - filename, - self._cloud_file] + if self._files is None: + self._files = [os.path.join(home, ".config", appname, filename), + os.path.join(home, ".config", appname + ".conf"), + os.path.join("/etc/xdg", appname, filename), + os.path.join("/etc/xdg", appname + ".conf"), + filename, + self._cloud_file] self._config = configparser.ConfigParser() self.read_config() self._cloud_config = configparser.ConfigParser() self.read_cloud_config() + self._watch_config_file() + + def _watch_config_file(self): + asyncio.get_event_loop().call_later(1, self._check_config_file_change) + + def _check_config_file_change(self): + """ + Check if configuration file has changed on the disk + """ + + changed = False + for file in self._watched_files: + if os.stat(file).st_mtime != self._watched_files[file]: + changed = True + if changed: + self.read_config() + # TODO: Support command line override + self._watch_config_file() def list_cloud_config_file(self): return self._cloud_file @@ -101,6 +128,10 @@ class Config(object): parsed_files = self._config.read(self._files) if not parsed_files: log.warning("No configuration file could be found or read") + else: + for file in parsed_files: + log.info("Load configuration file {}".format(file)) + self._watched_files[file] = os.stat(file).st_mtime def get_default_section(self): """ @@ -132,7 +163,10 @@ class Config(object): :param content: A dictionary with section content """ - self._config[section] = content + if not self._config.has_section(section): + self._config.add_section(section) + for key in content: + self._config.set(section, key, content[key]) @staticmethod def instance(): diff --git a/gns3server/main.py b/gns3server/main.py index f94c8e40..bba7cd3c 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -105,16 +105,18 @@ def main(): Entry point for GNS3 server """ + # We init the logger with info level during config file parsing + user_log = init_logger(logging.INFO) + user_log.info("GNS3 server version {}".format(__version__)) current_year = datetime.date.today().year - args = parse_arguments() + user_log.info("Copyright (c) 2007-{} GNS3 Technologies Inc.".format(current_year)) + level = logging.INFO + args = parse_arguments() if args.debug: level = logging.DEBUG user_log = init_logger(level, quiet=args.quiet) - user_log.info("GNS3 server version {}".format(__version__)) - user_log.info("Copyright (c) 2007-{} GNS3 Technologies Inc.".format(current_year)) - server_config = Config.instance().get_section_config("Server") if server_config.getboolean("local"): log.warning("Local mode is enabled. Beware, clients will have full control on your filesystem") diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 00000000..4b7e5063 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,97 @@ +# -*- 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 configparser +import time +import os + +from gns3server.config import Config + + +def load_config(tmpdir, settings): + """ + Create a configuration file for + the test. + + :params tmpdir: Temporary directory + :params settings: Configuration settings + :returns: Configuration instance + """ + + path = write_config(tmpdir, settings) + return Config(files=[path]) + + +def write_config(tmpdir, settings): + """ + Write a configuration file for the test. + + :params tmpdir: Temporary directory + :params settings: Configuration settings + :returns: File path + """ + + path = str(tmpdir / "server.conf") + + config = configparser.ConfigParser() + config.read_dict(settings) + with open(path, "w+") as f: + config.write(f) + return path + + +def test_get_section_config(tmpdir): + + config = load_config(tmpdir, { + "Server": { + "host": "127.0.0.1" + } + }) + assert dict(config.get_section_config("Server")) == {"host": "127.0.0.1"} + + +def test_set_section_config(tmpdir): + + config = load_config(tmpdir, { + "Server": { + "host": "127.0.0.1" + } + }) + assert dict(config.get_section_config("Server")) == {"host": "127.0.0.1"} + config.set_section_config("Server", {"host": "192.168.1.1"}) + assert dict(config.get_section_config("Server")) == {"host": "192.168.1.1"} + + +def test_check_config_file_change(tmpdir): + + config = load_config(tmpdir, { + "Server": { + "host": "127.0.0.1" + } + }) + assert dict(config.get_section_config("Server")) == {"host": "127.0.0.1"} + + path = write_config(tmpdir, { + "Server": { + "host": "192.168.1.1" + } + }) + os.utime(path, (time.time() + 1, time.time() + 1)) + + config._check_config_file_change() + assert dict(config.get_section_config("Server")) == {"host": "192.168.1.1"} From 0ae8d8031a5a73c5522cdd04a2f437b236609c2d Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 2 Feb 2015 15:08:46 +0100 Subject: [PATCH 158/485] Override configuration from command line even in case of config reload --- gns3server/config.py | 9 ++++++++- tests/test_config.py | 23 +++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/gns3server/config.py b/gns3server/config.py index ef36dee4..072785a4 100644 --- a/gns3server/config.py +++ b/gns3server/config.py @@ -41,8 +41,13 @@ class Config(object): def __init__(self, files=None): self._files = files + + # Monitor configuration files for changes self._watched_files = {} + # Override config from commande even if modify the config file and live reload it. + self._override_config = {} + if sys.platform.startswith("win"): appname = "GNS3" @@ -106,7 +111,8 @@ class Config(object): changed = True if changed: self.read_config() - # TODO: Support command line override + for section in self._override_config: + self.set_section_config(section, self._override_config[section]) self._watch_config_file() def list_cloud_config_file(self): @@ -167,6 +173,7 @@ class Config(object): self._config.add_section(section) for key in content: self._config.set(section, key, content[key]) + self._override_config[section] = content @staticmethod def instance(): diff --git a/tests/test_config.py b/tests/test_config.py index 4b7e5063..d12683b1 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -95,3 +95,26 @@ def test_check_config_file_change(tmpdir): config._check_config_file_change() assert dict(config.get_section_config("Server")) == {"host": "192.168.1.1"} + + +def test_check_config_file_change_override_cmdline(tmpdir): + + config = load_config(tmpdir, { + "Server": { + "host": "127.0.0.1" + } + }) + assert dict(config.get_section_config("Server")) == {"host": "127.0.0.1"} + + config.set_section_config("Server", {"host": "192.168.1.1"}) + assert dict(config.get_section_config("Server")) == {"host": "192.168.1.1"} + + path = write_config(tmpdir, { + "Server": { + "host": "192.168.1.2" + } + }) + os.utime(path, (time.time() + 1, time.time() + 1)) + + config._check_config_file_change() + assert dict(config.get_section_config("Server")) == {"host": "192.168.1.1"} From aecd7dedba8aa4449de3792c24cb889ada59f162 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 2 Feb 2015 13:13:56 -0700 Subject: [PATCH 159/485] Fixes app name for the config file on Linux. --- gns3server/config.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gns3server/config.py b/gns3server/config.py index 072785a4..deac3916 100644 --- a/gns3server/config.py +++ b/gns3server/config.py @@ -79,7 +79,10 @@ class Config(object): # 4: /etc/xdg/GNS3.conf # 5: server.conf in the current working directory - appname = "gns3.net" + if sys.platform.startswith("darwin"): + appname = "gns3.net" + else: + appname = "GNS3" home = os.path.expanduser("~") self._cloud_file = os.path.join(home, ".config", appname, "cloud.conf") filename = "server.conf" From 2c3fe2ad4b62dfa74d6d57f12d7083891be0d650 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 2 Feb 2015 21:28:09 +0100 Subject: [PATCH 160/485] Repare debug log --- gns3server/main.py | 20 ++++++++++++-------- gns3server/schemas/project.py | 2 +- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/gns3server/main.py b/gns3server/main.py index bba7cd3c..cd9689f3 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -85,6 +85,11 @@ def parse_arguments(): parser.add_argument("-d", "--debug", action="store_true", help="show debug logs and enable code live reload") args = parser.parse_args() + return args + + +def set_config(args): + config = Config.instance() server_config = config.get_section_config("Server") server_config["local"] = server_config.get("local", "true" if args.local else "false") @@ -97,26 +102,25 @@ def parse_arguments(): server_config["debug"] = server_config.get("debug", "true" if args.debug else "false") config.set_section_config("Server", server_config) - return args - def main(): """ Entry point for GNS3 server """ - # We init the logger with info level during config file parsing - user_log = init_logger(logging.INFO) - user_log.info("GNS3 server version {}".format(__version__)) - current_year = datetime.date.today().year - user_log.info("Copyright (c) 2007-{} GNS3 Technologies Inc.".format(current_year)) - level = logging.INFO args = parse_arguments() if args.debug: level = logging.DEBUG user_log = init_logger(level, quiet=args.quiet) + user_log = init_logger(logging.INFO) + user_log.info("GNS3 server version {}".format(__version__)) + current_year = datetime.date.today().year + user_log.info("Copyright (c) 2007-{} GNS3 Technologies Inc.".format(current_year)) + + set_config(args) + server_config = Config.instance().get_section_config("Server") if server_config.getboolean("local"): log.warning("Local mode is enabled. Beware, clients will have full control on your filesystem") diff --git a/gns3server/schemas/project.py b/gns3server/schemas/project.py index e0c2209a..8bb1f7de 100644 --- a/gns3server/schemas/project.py +++ b/gns3server/schemas/project.py @@ -28,7 +28,7 @@ PROJECT_CREATE_SCHEMA = { }, "uuid": { "description": "Project UUID", - "type": "string", + "type": ["string", "null"], "minLength": 36, "maxLength": 36, "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" From 45ca493ecf94af1d419db090709d040a767b91e4 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 2 Feb 2015 14:52:58 -0700 Subject: [PATCH 161/485] Use module settings from the config file. --- gns3server/main.py | 4 +--- gns3server/modules/port_manager.py | 3 +++ gns3server/modules/virtualbox/__init__.py | 16 +++------------- gns3server/modules/vpcs/vpcs_vm.py | 4 +--- 4 files changed, 8 insertions(+), 19 deletions(-) diff --git a/gns3server/main.py b/gns3server/main.py index cd9689f3..59ca9010 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -112,15 +112,13 @@ def main(): args = parse_arguments() if args.debug: level = logging.DEBUG - user_log = init_logger(level, quiet=args.quiet) - user_log = init_logger(logging.INFO) + user_log = init_logger(level, quiet=args.quiet) user_log.info("GNS3 server version {}".format(__version__)) current_year = datetime.date.today().year user_log.info("Copyright (c) 2007-{} GNS3 Technologies Inc.".format(current_year)) set_config(args) - server_config = Config.instance().get_section_config("Server") if server_config.getboolean("local"): log.warning("Local mode is enabled. Beware, clients will have full control on your filesystem") diff --git a/gns3server/modules/port_manager.py b/gns3server/modules/port_manager.py index 514d07ce..80e94087 100644 --- a/gns3server/modules/port_manager.py +++ b/gns3server/modules/port_manager.py @@ -44,9 +44,12 @@ class PortManager: console_start_port_range = server_config.getint("console_start_port_range", 2000) console_end_port_range = server_config.getint("console_end_port_range", 5000) self._console_port_range = (console_start_port_range, console_end_port_range) + log.debug("Console port range is {}-{}".format(console_start_port_range, console_end_port_range)) + udp_start_port_range = server_config.getint("udp_start_port_range", 10000) udp_end_port_range = server_config.getint("udp_end_port_range", 20000) self._udp_port_range = (udp_start_port_range, udp_end_port_range) + log.debug("UDP port range is {}-{}".format(udp_start_port_range, udp_end_port_range)) if remote_console_connections: log.warning("Remote console connections are allowed") diff --git a/gns3server/modules/virtualbox/__init__.py b/gns3server/modules/virtualbox/__init__.py index 152d72e2..f478e077 100644 --- a/gns3server/modules/virtualbox/__init__.py +++ b/gns3server/modules/virtualbox/__init__.py @@ -38,7 +38,6 @@ class VirtualBox(BaseManager): super().__init__() self._vboxmanage_path = None - self._vbox_user = None @property def vboxmanage_path(self): @@ -50,16 +49,6 @@ class VirtualBox(BaseManager): return self._vboxmanage_path - @property - def vbox_user(self): - """ - Returns the VirtualBox user - - :returns: username - """ - - return self._vbox_user - def find_vboxmanage(self): # look for VBoxManage @@ -94,9 +83,10 @@ class VirtualBox(BaseManager): command = [vboxmanage_path, "--nologo", subcommand] command.extend(args) try: - if self.vbox_user: + vbox_user = self.config.get_section_config("VirtualBox").get("vbox_user") + if vbox_user: # TODO: test & review this part - sudo_command = "sudo -i -u {}".format(self.vbox_user) + " ".join(command) + sudo_command = "sudo -i -u {}".format(vbox_user) + " ".join(command) process = yield from asyncio.create_subprocess_shell(sudo_command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) else: process = yield from asyncio.create_subprocess_exec(*command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index e5f5ed28..73dac3d1 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -57,10 +57,8 @@ class VPCSVM(BaseVM): super().__init__(name, uuid, project, manager) - self._path = manager.config.get_section_config("VPCS").get("path", "vpcs") - + self._path = manager.config.get_section_config("VPCS").get("vpcs_path", "vpcs") self._console = console - self._command = [] self._process = None self._vpcs_stdout_file = "" From 471fbe576c75776b315ed0e0a80de5c8052edb26 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 2 Feb 2015 15:00:56 -0700 Subject: [PATCH 162/485] Ignore OSError when checking for config file changes. --- gns3server/config.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/gns3server/config.py b/gns3server/config.py index deac3916..07761bab 100644 --- a/gns3server/config.py +++ b/gns3server/config.py @@ -110,8 +110,11 @@ class Config(object): changed = False for file in self._watched_files: - if os.stat(file).st_mtime != self._watched_files[file]: - changed = True + try: + if os.stat(file).st_mtime != self._watched_files[file]: + changed = True + except OSError: + continue if changed: self.read_config() for section in self._override_config: From 33d5882a4af398b72e4d00c322094ad6879cae47 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 2 Feb 2015 15:36:13 -0700 Subject: [PATCH 163/485] Add traceback info when catching an exception to help with debugging. --- gns3server/web/route.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/gns3server/web/route.py b/gns3server/web/route.py index b59acb4a..1aef6ed9 100644 --- a/gns3server/web/route.py +++ b/gns3server/web/route.py @@ -15,11 +15,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import sys import json import jsonschema import asyncio import aiohttp import logging +import traceback log = logging.getLogger(__name__) @@ -110,9 +112,18 @@ class Route(object): response.set_status(e.status) response.json({"message": e.text, "status": e.status}) except VMError as e: + log.error("VM error detected: {type}".format(type=type(e)), exc_info=1) response = Response(route=route) response.set_status(500) response.json({"message": str(e), "status": 500}) + except Exception as e: + log.error("Uncaught exception detected: {type}".format(type=type(e)), exc_info=1) + response = Response(route=route) + response.set_status(500) + exc_type, exc_value, exc_tb = sys.exc_info() + lines = traceback.format_exception(exc_type, exc_value, exc_tb) + tb = "".join(lines) + response.json({"message": tb, "status": 500}) return response cls._routes.append((method, cls._path, control_schema)) From df72369b0efcc45b2c2b404a48fcd4632e4584b6 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 2 Feb 2015 17:00:29 -0700 Subject: [PATCH 164/485] Fix VirtualBox VM close. --- gns3server/modules/virtualbox/__init__.py | 9 +++-- .../modules/virtualbox/virtualbox_vm.py | 34 +++++++++++++------ 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/gns3server/modules/virtualbox/__init__.py b/gns3server/modules/virtualbox/__init__.py index f478e077..dedc4719 100644 --- a/gns3server/modules/virtualbox/__init__.py +++ b/gns3server/modules/virtualbox/__init__.py @@ -24,6 +24,9 @@ import sys import shutil import asyncio import subprocess +import logging + +log = logging.getLogger(__name__) from ..base_manager import BaseManager from .virtualbox_vm import VirtualBoxVM @@ -82,6 +85,7 @@ class VirtualBox(BaseManager): vboxmanage_path = self.find_vboxmanage() command = [vboxmanage_path, "--nologo", subcommand] command.extend(args) + log.debug("Executing VBoxManage with command: {}".format(command)) try: vbox_user = self.config.get_section_config("VirtualBox").get("vbox_user") if vbox_user: @@ -100,8 +104,9 @@ class VirtualBox(BaseManager): if process.returncode: # only the first line of the output is useful - vboxmanage_error = stderr_data.decode("utf-8", errors="ignore").splitlines()[0] - raise VirtualBoxError(vboxmanage_error) + vboxmanage_error = stderr_data.decode("utf-8", errors="ignore") + log.warn("VBoxManage has returned an error: {}".format(vboxmanage_error)) + raise VirtualBoxError(vboxmanage_error.splitlines()[0]) return stdout_data.decode("utf-8", errors="ignore").splitlines() diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index a01479a9..462d846c 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -305,7 +305,12 @@ class VirtualBoxVM(BaseVM): for hdd_info in hdd_table: hdd_file = os.path.join(self.working_dir, self._vmname, "Snapshots", hdd_info["hdd"]) if os.path.exists(hdd_file): - log.debug("reattaching hdd {}".format(hdd_file)) + log.info("VirtualBox VM '{name}' [{uuid}] attaching HDD {controller} {port} {device} {medium}".format(name=self.name, + uuid=self.uuid, + controller=hdd_info["controller"], + port=hdd_info["port"], + device=hdd_info["device"], + medium=hdd_file)) yield from self._storage_attach('--storagectl "{}" --port {} --device {} --type hdd --medium "{}"'.format(hdd_info["controller"], hdd_info["port"], hdd_info["device"], @@ -327,7 +332,7 @@ class VirtualBoxVM(BaseVM): hdd_table = [] if os.path.exists(self.working_dir): hdd_files = yield from self._get_all_hdd_files() - vm_info = self._get_vm_info() + vm_info = yield from self._get_vm_info() for entry, value in vm_info.items(): match = re.search("^([\s\w]+)\-(\d)\-(\d)$", entry) if match: @@ -335,6 +340,11 @@ class VirtualBoxVM(BaseVM): port = match.group(2) device = match.group(3) if value in hdd_files: + log.info("VirtualBox VM '{name}' [{uuid}] detaching HDD {controller} {port} {device}".format(name=self.name, + uuid=self.uuid, + controller=controller, + port=port, + device=device)) yield from self._storage_attach('--storagectl "{}" --port {} --device {} --type hdd --medium none'.format(controller, port, device)) hdd_table.append( { @@ -345,16 +355,18 @@ class VirtualBoxVM(BaseVM): } ) - yield from self.manager.execute("unregistervm", [self._vmname]) + log.info("VirtualBox VM '{name}' [{uuid}] unregistering".format(name=self.name, uuid=self.uuid)) + yield from self.manager.execute("unregistervm", [self._name]) if hdd_table: try: hdd_info_file = os.path.join(self.working_dir, self._vmname, "hdd_info.json") with open(hdd_info_file, "w") as f: - # log.info("saving project: {}".format(path)) json.dump(hdd_table, f, indent=4) except OSError as e: - raise VirtualBoxError("Could not write HDD info file: {}".format(e)) + log.warning("VirtualBox VM '{name}' [{uuid}] could not write HHD info file: {error}".format(name=self.name, + uuid=self.uuid, + error=e.strerror)) log.info("VirtualBox VM '{name}' [{uuid}] closed".format(name=self.name, uuid=self.uuid)) @@ -403,10 +415,10 @@ class VirtualBoxVM(BaseVM): if enable_remote_console: log.info("VirtualBox VM '{name}' [{uuid}] has enabled the console".format(name=self.name, uuid=self.uuid)) - self._start_remote_console() + #self._start_remote_console() else: log.info("VirtualBox VM '{name}' [{uuid}] has disabled the console".format(name=self.name, uuid=self.uuid)) - self._stop_remote_console() + #self._stop_remote_console() self._enable_remote_console = enable_remote_console @property @@ -454,9 +466,9 @@ class VirtualBoxVM(BaseVM): self._ethernet_adapters.append(EthernetAdapter()) self._adapters = len(self._ethernet_adapters) - log.info("VirtualBox VM '{name}' [{uuid}]: number of Ethernet adapters changed to {adapters}".format(name=self.name, - uuid=self.uuid, - adapters=adapters)) + log.info("VirtualBox VM '{name}' [{uuid}] has changed the number of Ethernet adapters to {adapters}".format(name=self.name, + uuid=self.uuid, + adapters=adapters)) @property def adapter_start_index(self): @@ -690,7 +702,7 @@ class VirtualBoxVM(BaseVM): self._vmname = self._name yield from self.manager.execute("setextradata", [self._vmname, "GNS3/Clone", "yes"]) - args = [self._name, "take", "reset"] + args = [self._vmname, "take", "reset"] result = yield from self.manager.execute("snapshot", args) log.debug("Snapshot reset created: {}".format(result)) From 66569f26a4dbb3748898a128180977a9dcd6082c Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 2 Feb 2015 17:01:25 -0700 Subject: [PATCH 165/485] Make sure to wait for the unload coroutine to finish when the server is shutting down. --- gns3server/modules/base_manager.py | 2 +- gns3server/server.py | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index eafd5765..8fb3772a 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -101,7 +101,7 @@ class BaseManager: try: yield from self.close_vm(uuid) except Exception as e: - log.warn("Could not delete VM {}: {}".format(uuid, e)) + log.error("Could not delete VM {}: {}".format(uuid, e), exc_info=1) continue if hasattr(BaseManager, "_instance"): diff --git a/gns3server/server.py b/gns3server/server.py index 0ae237c1..3e19cdcf 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -61,6 +61,7 @@ class Server: return return server + @asyncio.coroutine def _stop_application(self): """ Cleanup the modules (shutdown running emulators etc.) @@ -69,14 +70,15 @@ class Server: for module in MODULES: log.debug("Unloading module {}".format(module.__name__)) m = module.instance() - asyncio.async(m.unload()) + yield from m.unload() self._loop.stop() def _signal_handling(self): + @asyncio.coroutine def signal_handler(signame): log.warning("Server has got signal {}, exiting...".format(signame)) - self._stop_application() + yield from self._stop_application() signals = ["SIGTERM", "SIGINT"] if sys.platform.startswith("win"): @@ -85,7 +87,7 @@ class Server: signals.extend(["SIGHUP", "SIGQUIT"]) for signal_name in signals: - callback = functools.partial(signal_handler, signal_name) + callback = functools.partial(asyncio.async, 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) @@ -94,10 +96,11 @@ class Server: def _reload_hook(self): + @asyncio.coroutine def reload(): log.info("Reloading") - self._stop_application() + yield from self._stop_application() os.execv(sys.executable, [sys.executable] + sys.argv) # code extracted from tornado @@ -116,7 +119,7 @@ class Server: modified = os.stat(path).st_mtime if modified > self._start_time: log.debug("File {} has been modified".format(path)) - reload() + asyncio.async(reload()) self._loop.call_later(1, self._reload_hook) def _create_ssl_context(self, server_config): From 81f92525547af9cf22dc24c4ee5506b04c705573 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 2 Feb 2015 18:56:13 -0700 Subject: [PATCH 166/485] Fixes nasty bug when close a cloned VirtualBox VM. --- gns3server/handlers/virtualbox_handler.py | 8 ++++---- gns3server/modules/base_manager.py | 2 +- gns3server/modules/project.py | 8 +++++--- gns3server/modules/virtualbox/virtualbox_vm.py | 10 ++++++++-- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/gns3server/handlers/virtualbox_handler.py b/gns3server/handlers/virtualbox_handler.py index ef074d51..dfb8da7a 100644 --- a/gns3server/handlers/virtualbox_handler.py +++ b/gns3server/handlers/virtualbox_handler.py @@ -58,11 +58,11 @@ class VirtualBoxHandler: def create(request, response): vbox_manager = VirtualBox.instance() - vm = yield from vbox_manager.create_vm(request.json["name"], - request.json["project_uuid"], + vm = yield from vbox_manager.create_vm(request.json.pop("name"), + request.json.pop("project_uuid"), request.json.get("uuid"), - request.json["vmname"], - request.json["linked_clone"], + request.json.pop("vmname"), + request.json.pop("linked_clone"), adapters=request.json.get("adapters", 0)) for name, value in request.json.items(): diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 8fb3772a..c2efd9dc 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -144,12 +144,12 @@ class BaseManager: uuid = str(uuid4()) vm = self._VM_CLASS(name, uuid, project, self, *args, **kwargs) - project.add_vm(vm) if asyncio.iscoroutinefunction(vm.create): yield from vm.create() else: vm.create() self._vms[vm.uuid] = vm + project.add_vm(vm) return vm @asyncio.coroutine diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index afd6c271..d385f143 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -17,7 +17,6 @@ import aiohttp import os -import tempfile import shutil import asyncio from uuid import UUID, uuid4 @@ -181,7 +180,7 @@ class Project: @asyncio.coroutine def close(self): - """Close the project, but keep informations on disk""" + """Close the project, but keep information on disk""" yield from self._close_and_clean(self._temporary) @@ -194,7 +193,10 @@ class Project: """ for vm in self._vms: - vm.close() + if asyncio.iscoroutinefunction(vm.close): + yield from vm.close() + else: + vm.close() if cleanup and os.path.exists(self.path): try: yield from wait_run_in_executor(shutil.rmtree, self.path) diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 462d846c..fd8242e2 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -57,6 +57,7 @@ class VirtualBoxVM(BaseVM): self._system_properties = {} self._telnet_server_thread = None self._serial_pipe = None + self._closed = False # VirtualBox settings self._console = None @@ -322,6 +323,10 @@ class VirtualBoxVM(BaseVM): Closes this VirtualBox VM. """ + if self._closed: + # VM is already closed + return + self.stop() if self._console: @@ -370,6 +375,7 @@ class VirtualBoxVM(BaseVM): log.info("VirtualBox VM '{name}' [{uuid}] closed".format(name=self.name, uuid=self.uuid)) + self._closed = True @property def headless(self): @@ -697,14 +703,14 @@ class VirtualBoxVM(BaseVM): "--register"] result = yield from self.manager.execute("clonevm", args) - log.debug("cloned VirtualBox VM: {}".format(result)) + log.debug("VirtualBox VM: {} cloned".format(result)) self._vmname = self._name yield from self.manager.execute("setextradata", [self._vmname, "GNS3/Clone", "yes"]) args = [self._vmname, "take", "reset"] result = yield from self.manager.execute("snapshot", args) - log.debug("Snapshot reset created: {}".format(result)) + log.debug("Snapshot 'reset' created: {}".format(result)) def _start_remote_console(self): """ From d199778745219c3a2e018dcb2c805fda399db601 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 2 Feb 2015 19:41:26 -0700 Subject: [PATCH 167/485] Fixes tests. --- tests/modules/vpcs/test_vpcs_vm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/modules/vpcs/test_vpcs_vm.py b/tests/modules/vpcs/test_vpcs_vm.py index f87882b3..b61caf38 100644 --- a/tests/modules/vpcs/test_vpcs_vm.py +++ b/tests/modules/vpcs/test_vpcs_vm.py @@ -57,7 +57,7 @@ def test_vm_invalid_vpcs_version(loop, project, manager): assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0f" -@patch("gns3server.config.Config.get_section_config", return_value={"path": "/bin/test_fake"}) +@patch("gns3server.config.Config.get_section_config", return_value={"vpcs_path": "/bin/test_fake"}) def test_vm_invalid_vpcs_path(project, manager, loop): with pytest.raises(VPCSError): vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0e", project, manager) From 0ce344b1d2c2d94a11ba2d97d3f95e7bdeed004c Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 3 Feb 2015 10:49:21 +0100 Subject: [PATCH 168/485] PEP8 --- gns3server/modules/virtualbox/virtualbox_vm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index fd8242e2..a5a126c6 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -421,10 +421,10 @@ class VirtualBoxVM(BaseVM): if enable_remote_console: log.info("VirtualBox VM '{name}' [{uuid}] has enabled the console".format(name=self.name, uuid=self.uuid)) - #self._start_remote_console() + # self._start_remote_console() else: log.info("VirtualBox VM '{name}' [{uuid}] has disabled the console".format(name=self.name, uuid=self.uuid)) - #self._stop_remote_console() + # self._stop_remote_console() self._enable_remote_console = enable_remote_console @property From f572f3fc95590e350c0cc6a7864d5a60eac3b405 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 3 Feb 2015 20:28:31 +0100 Subject: [PATCH 169/485] You can't modify startup script remotely --- gns3server/handlers/vpcs_handler.py | 2 -- gns3server/schemas/vpcs.py | 8 -------- tests/api/test_vpcs.py | 17 ----------------- 3 files changed, 27 deletions(-) diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index 060ee587..a63f475a 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -47,7 +47,6 @@ class VPCSHandler: request.json["project_uuid"], request.json.get("uuid"), console=request.json.get("console"), - script_file=request.json.get("script_file"), startup_script=request.json.get("startup_script")) response.set_status(201) response.json(vm) @@ -90,7 +89,6 @@ class VPCSHandler: vm = vpcs_manager.get_vm(request.match_info["uuid"]) vm.name = request.json.get("name", vm.name) vm.console = request.json.get("console", vm.console) - vm.script_file = request.json.get("script_file", vm.script_file) vm.startup_script = request.json.get("startup_script", vm.startup_script) response.json(vm) diff --git a/gns3server/schemas/vpcs.py b/gns3server/schemas/vpcs.py index e4dae510..b4841034 100644 --- a/gns3server/schemas/vpcs.py +++ b/gns3server/schemas/vpcs.py @@ -50,10 +50,6 @@ VPCS_CREATE_SCHEMA = { "maximum": 65535, "type": ["integer", "null"] }, - "script_file": { - "description": "VPCS startup script", - "type": ["string", "null"] - }, "startup_script": { "description": "Content of the VPCS startup script", "type": ["string", "null"] @@ -79,10 +75,6 @@ VPCS_UPDATE_SCHEMA = { "maximum": 65535, "type": ["integer", "null"] }, - "script_file": { - "description": "VPCS startup script", - "type": ["string", "null"] - }, "startup_script": { "description": "Content of the VPCS startup script", "type": ["string", "null"] diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index db318494..56c970a2 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -45,18 +45,6 @@ def test_vpcs_get(server, project, vm): assert response.json["project_uuid"] == project.uuid -def test_vpcs_create_script_file(server, project, tmpdir): - path = os.path.join(str(tmpdir), "test") - with open(path, 'w+') as f: - f.write("ip 192.168.1.2") - response = server.post("/vpcs", {"name": "PC TEST 1", "project_uuid": project.uuid, "script_file": path}) - assert response.status == 201 - assert response.route == "/vpcs" - assert response.json["name"] == "PC TEST 1" - assert response.json["project_uuid"] == project.uuid - assert response.json["script_file"] == path - - def test_vpcs_create_startup_script(server, project): response = server.post("/vpcs", {"name": "PC TEST 1", "project_uuid": project.uuid, "startup_script": "ip 192.168.1.2\necho TEST"}) assert response.status == 201 @@ -134,15 +122,10 @@ def test_vpcs_delete(server, vm): def test_vpcs_update(server, vm, tmpdir, free_console_port): - path = os.path.join(str(tmpdir), 'startup2.vpcs') - with open(path, 'w+') as f: - f.write(path) response = server.put("/vpcs/{}".format(vm["uuid"]), {"name": "test", "console": free_console_port, - "script_file": path, "startup_script": "ip 192.168.1.1"}) assert response.status == 200 assert response.json["name"] == "test" assert response.json["console"] == free_console_port - assert response.json["script_file"] == path assert response.json["startup_script"] == "ip 192.168.1.1" From a12f753136b258e541ee4139e804f277258a61e2 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 3 Feb 2015 21:27:15 +0100 Subject: [PATCH 170/485] Update documentation --- docs/api/examples/delete_projectuuid.txt | 4 +- ...te_virtualboxuuidadaptersadapteriddnio.txt | 13 +++++ .../delete_vpcsuuidportsportnumberdnio.txt | 13 +++++ docs/api/examples/get_interfaces.txt | 46 +++++++++++++----- docs/api/examples/get_projectuuid.txt | 4 +- docs/api/examples/get_version.txt | 4 +- docs/api/examples/get_virtualboxuuid.txt | 6 +-- docs/api/examples/get_vpcsuuid.txt | 6 +-- docs/api/examples/post_portsudp.txt | 17 +++++++ docs/api/examples/post_projectuuidclose.txt | 4 +- docs/api/examples/post_projectuuidcommit.txt | 4 +- docs/api/examples/post_version.txt | 4 +- docs/api/examples/post_virtualbox.txt | 6 +-- ...st_virtualboxuuidadaptersadapteriddnio.txt | 25 ++++++++++ docs/api/examples/post_vpcs.txt | 6 +-- .../post_vpcsuuidportsportnumberdnio.txt | 25 ++++++++++ docs/api/examples/put_projectuuid.txt | 10 ++-- docs/api/portsudp.rst | 19 ++++++++ docs/api/project.rst | 2 +- .../virtualboxuuidadaptersadapteriddnio.rst | 48 +++++++++++++++++++ .../virtualboxuuidcaptureadapteriddstart.rst | 29 +++++++++++ .../virtualboxuuidcaptureadapteriddstop.rst | 20 ++++++++ docs/api/vpcs.rst | 1 - docs/api/vpcsuuid.rst | 1 - docs/api/vpcsuuidportsportnumberdnio.rst | 48 +++++++++++++++++++ 25 files changed, 320 insertions(+), 45 deletions(-) create mode 100644 docs/api/examples/delete_virtualboxuuidadaptersadapteriddnio.txt create mode 100644 docs/api/examples/delete_vpcsuuidportsportnumberdnio.txt create mode 100644 docs/api/examples/post_portsudp.txt create mode 100644 docs/api/examples/post_virtualboxuuidadaptersadapteriddnio.txt create mode 100644 docs/api/examples/post_vpcsuuidportsportnumberdnio.txt create mode 100644 docs/api/portsudp.rst create mode 100644 docs/api/virtualboxuuidadaptersadapteriddnio.rst create mode 100644 docs/api/virtualboxuuidcaptureadapteriddstart.rst create mode 100644 docs/api/virtualboxuuidcaptureadapteriddstop.rst create mode 100644 docs/api/vpcsuuidportsportnumberdnio.rst diff --git a/docs/api/examples/delete_projectuuid.txt b/docs/api/examples/delete_projectuuid.txt index 68989437..9d63e2e0 100644 --- a/docs/api/examples/delete_projectuuid.txt +++ b/docs/api/examples/delete_projectuuid.txt @@ -5,9 +5,9 @@ DELETE /project/{uuid} HTTP/1.1 HTTP/1.1 204 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /project/{uuid} diff --git a/docs/api/examples/delete_virtualboxuuidadaptersadapteriddnio.txt b/docs/api/examples/delete_virtualboxuuidadaptersadapteriddnio.txt new file mode 100644 index 00000000..cf775417 --- /dev/null +++ b/docs/api/examples/delete_virtualboxuuidadaptersadapteriddnio.txt @@ -0,0 +1,13 @@ +curl -i -X DELETE 'http://localhost:8000/virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio' + +DELETE /virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio HTTP/1.1 + + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio + diff --git a/docs/api/examples/delete_vpcsuuidportsportnumberdnio.txt b/docs/api/examples/delete_vpcsuuidportsportnumberdnio.txt new file mode 100644 index 00000000..c2a772f2 --- /dev/null +++ b/docs/api/examples/delete_vpcsuuidportsportnumberdnio.txt @@ -0,0 +1,13 @@ +curl -i -X DELETE 'http://localhost:8000/vpcs/{uuid}/ports/{port_number:\d+}/nio' + +DELETE /vpcs/{uuid}/ports/{port_number:\d+}/nio HTTP/1.1 + + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /vpcs/{uuid}/ports/{port_number:\d+}/nio + diff --git a/docs/api/examples/get_interfaces.txt b/docs/api/examples/get_interfaces.txt index 9489d231..74381d7e 100644 --- a/docs/api/examples/get_interfaces.txt +++ b/docs/api/examples/get_interfaces.txt @@ -5,36 +5,56 @@ GET /interfaces HTTP/1.1 HTTP/1.1 200 -CONNECTION: close -CONTENT-LENGTH: 364 +CONNECTION: keep-alive +CONTENT-LENGTH: 652 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /interfaces [ { - "id": "lo", - "name": "lo" + "id": "lo0", + "name": "lo0" }, { - "id": "eth0", - "name": "eth0" + "id": "gif0", + "name": "gif0" }, { - "id": "wlan0", - "name": "wlan0" + "id": "stf0", + "name": "stf0" }, { - "id": "vmnet1", - "name": "vmnet1" + "id": "en0", + "name": "en0" }, { - "id": "vmnet8", - "name": "vmnet8" + "id": "en1", + "name": "en1" + }, + { + "id": "fw0", + "name": "fw0" + }, + { + "id": "en2", + "name": "en2" + }, + { + "id": "p2p0", + "name": "p2p0" + }, + { + "id": "bridge0", + "name": "bridge0" }, { "id": "vboxnet0", "name": "vboxnet0" + }, + { + "id": "vboxnet1", + "name": "vboxnet1" } ] diff --git a/docs/api/examples/get_projectuuid.txt b/docs/api/examples/get_projectuuid.txt index bed046ad..2388771c 100644 --- a/docs/api/examples/get_projectuuid.txt +++ b/docs/api/examples/get_projectuuid.txt @@ -5,11 +5,11 @@ GET /project/{uuid} HTTP/1.1 HTTP/1.1 200 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 102 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /project/{uuid} { diff --git a/docs/api/examples/get_version.txt b/docs/api/examples/get_version.txt index 59bdb128..39509cda 100644 --- a/docs/api/examples/get_version.txt +++ b/docs/api/examples/get_version.txt @@ -5,11 +5,11 @@ GET /version HTTP/1.1 HTTP/1.1 200 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 29 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /version { diff --git a/docs/api/examples/get_virtualboxuuid.txt b/docs/api/examples/get_virtualboxuuid.txt index 739e864d..4322211e 100644 --- a/docs/api/examples/get_virtualboxuuid.txt +++ b/docs/api/examples/get_virtualboxuuid.txt @@ -5,11 +5,11 @@ GET /virtualbox/{uuid} HTTP/1.1 HTTP/1.1 200 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 348 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /virtualbox/{uuid} { @@ -21,6 +21,6 @@ X-ROUTE: /virtualbox/{uuid} "headless": false, "name": "VMTEST", "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "uuid": "0d15855e-8fb4-41af-81b0-a150c7576d5a", + "uuid": "c6b74706-54b6-405d-8aea-9c48a94987e8", "vmname": "VMTEST" } diff --git a/docs/api/examples/get_vpcsuuid.txt b/docs/api/examples/get_vpcsuuid.txt index 61a263a0..5aab7abd 100644 --- a/docs/api/examples/get_vpcsuuid.txt +++ b/docs/api/examples/get_vpcsuuid.txt @@ -5,11 +5,11 @@ GET /vpcs/{uuid} HTTP/1.1 HTTP/1.1 200 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 213 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /vpcs/{uuid} { @@ -18,5 +18,5 @@ X-ROUTE: /vpcs/{uuid} "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, "startup_script": null, - "uuid": "2ddb0fa3-1010-4f48-b295-caa1c414b4a2" + "uuid": "aceec3c0-f02d-44ee-88a6-85c3120c22ca" } diff --git a/docs/api/examples/post_portsudp.txt b/docs/api/examples/post_portsudp.txt new file mode 100644 index 00000000..8e7ea7f1 --- /dev/null +++ b/docs/api/examples/post_portsudp.txt @@ -0,0 +1,17 @@ +curl -i -X POST 'http://localhost:8000/ports/udp' -d '{}' + +POST /ports/udp HTTP/1.1 +{} + + +HTTP/1.1 201 +CONNECTION: keep-alive +CONTENT-LENGTH: 25 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /ports/udp + +{ + "udp_port": 10000 +} diff --git a/docs/api/examples/post_projectuuidclose.txt b/docs/api/examples/post_projectuuidclose.txt index d038c178..03ae88bc 100644 --- a/docs/api/examples/post_projectuuidclose.txt +++ b/docs/api/examples/post_projectuuidclose.txt @@ -5,9 +5,9 @@ POST /project/{uuid}/close HTTP/1.1 HTTP/1.1 204 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /project/{uuid}/close diff --git a/docs/api/examples/post_projectuuidcommit.txt b/docs/api/examples/post_projectuuidcommit.txt index b5d0c2d9..2fe9c38f 100644 --- a/docs/api/examples/post_projectuuidcommit.txt +++ b/docs/api/examples/post_projectuuidcommit.txt @@ -5,9 +5,9 @@ POST /project/{uuid}/commit HTTP/1.1 HTTP/1.1 204 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /project/{uuid}/commit diff --git a/docs/api/examples/post_version.txt b/docs/api/examples/post_version.txt index 45ef1069..1d4ecc7d 100644 --- a/docs/api/examples/post_version.txt +++ b/docs/api/examples/post_version.txt @@ -7,11 +7,11 @@ POST /version HTTP/1.1 HTTP/1.1 200 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 29 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /version { diff --git a/docs/api/examples/post_virtualbox.txt b/docs/api/examples/post_virtualbox.txt index 25c025f8..62e5fe4e 100644 --- a/docs/api/examples/post_virtualbox.txt +++ b/docs/api/examples/post_virtualbox.txt @@ -10,11 +10,11 @@ POST /virtualbox HTTP/1.1 HTTP/1.1 201 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 342 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /virtualbox { @@ -26,6 +26,6 @@ X-ROUTE: /virtualbox "headless": false, "name": "VM1", "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "uuid": "33217631-7c24-4acf-b462-c40e0012537a", + "uuid": "7ba9ddf4-0a6a-48a0-8483-3450a9f305df", "vmname": "VM1" } diff --git a/docs/api/examples/post_virtualboxuuidadaptersadapteriddnio.txt b/docs/api/examples/post_virtualboxuuidadaptersadapteriddnio.txt new file mode 100644 index 00000000..f3caae78 --- /dev/null +++ b/docs/api/examples/post_virtualboxuuidadaptersadapteriddnio.txt @@ -0,0 +1,25 @@ +curl -i -X POST 'http://localhost:8000/virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio' -d '{"lport": 4242, "rhost": "127.0.0.1", "rport": 4343, "type": "nio_udp"}' + +POST /virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio HTTP/1.1 +{ + "lport": 4242, + "rhost": "127.0.0.1", + "rport": 4343, + "type": "nio_udp" +} + + +HTTP/1.1 201 +CONNECTION: keep-alive +CONTENT-LENGTH: 89 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio + +{ + "lport": 4242, + "rhost": "127.0.0.1", + "rport": 4343, + "type": "nio_udp" +} diff --git a/docs/api/examples/post_vpcs.txt b/docs/api/examples/post_vpcs.txt index 75186c33..054660c9 100644 --- a/docs/api/examples/post_vpcs.txt +++ b/docs/api/examples/post_vpcs.txt @@ -8,11 +8,11 @@ POST /vpcs HTTP/1.1 HTTP/1.1 201 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 213 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /vpcs { @@ -21,5 +21,5 @@ X-ROUTE: /vpcs "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, "startup_script": null, - "uuid": "b0ed8aae-4d50-4a0c-9d91-0875a5aca533" + "uuid": "896e7bbd-936c-4a6f-9268-c89f8eb91f5e" } diff --git a/docs/api/examples/post_vpcsuuidportsportnumberdnio.txt b/docs/api/examples/post_vpcsuuidportsportnumberdnio.txt new file mode 100644 index 00000000..b3bbb41f --- /dev/null +++ b/docs/api/examples/post_vpcsuuidportsportnumberdnio.txt @@ -0,0 +1,25 @@ +curl -i -X POST 'http://localhost:8000/vpcs/{uuid}/ports/{port_number:\d+}/nio' -d '{"lport": 4242, "rhost": "127.0.0.1", "rport": 4343, "type": "nio_udp"}' + +POST /vpcs/{uuid}/ports/{port_number:\d+}/nio HTTP/1.1 +{ + "lport": 4242, + "rhost": "127.0.0.1", + "rport": 4343, + "type": "nio_udp" +} + + +HTTP/1.1 201 +CONNECTION: keep-alive +CONTENT-LENGTH: 89 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /vpcs/{uuid}/ports/{port_number:\d+}/nio + +{ + "lport": 4242, + "rhost": "127.0.0.1", + "rport": 4343, + "type": "nio_udp" +} diff --git a/docs/api/examples/put_projectuuid.txt b/docs/api/examples/put_projectuuid.txt index d629a7c1..91147567 100644 --- a/docs/api/examples/put_projectuuid.txt +++ b/docs/api/examples/put_projectuuid.txt @@ -7,15 +7,15 @@ PUT /project/{uuid} HTTP/1.1 HTTP/1.1 200 -CONNECTION: close -CONTENT-LENGTH: 114 +CONNECTION: keep-alive +CONTENT-LENGTH: 158 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /project/{uuid} { - "location": "/tmp/tmpi95b_dec", + "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmphb8dmyls", "temporary": false, - "uuid": "e30bf585-b257-4e14-8f7f-a5e0bdd126dd" + "uuid": "1aa054dd-e672-4961-90c3-fef730fc6301" } diff --git a/docs/api/portsudp.rst b/docs/api/portsudp.rst new file mode 100644 index 00000000..a2ecdae7 --- /dev/null +++ b/docs/api/portsudp.rst @@ -0,0 +1,19 @@ +/ports/udp +--------------------------------------------- + +.. contents:: + +POST /ports/udp +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Allocate an UDP port on the server + +Response status codes +********************** +- **201**: UDP port allocated + +Sample session +*************** + + +.. literalinclude:: examples/post_portsudp.txt + diff --git a/docs/api/project.rst b/docs/api/project.rst index 15a2aa08..8edefbbe 100644 --- a/docs/api/project.rst +++ b/docs/api/project.rst @@ -19,7 +19,7 @@ Input - +
Name Mandatory Type Description
adapter_start_index integer adapter index from which to start using adapters
adapter_type string VirtualBox adapter type
adapters integer number of adapters
console integer console TCP port
enable_remote_console boolean enable the remote console
headless boolean headless mode
linked_clone boolean either the VM is a linked clone or not
name string VirtualBox VM instance name
project_uuid string Project UUID
console integer console TCP port
enable_remote_console boolean enable the remote console
headless boolean headless mode
linked_clone boolean either the VM is a linked clone or not
name string VirtualBox VM instance name
project_uuid string Project UUID
uuid string VirtualBox VM instance UUID
console integer console TCP port
enable_remote_console boolean enable the remote console
headless boolean headless mode
linked_clone boolean either the VM is a linked clone or not
name string VirtualBox VM instance name
project_uuid string Project UUID
uuid string VirtualBox VM instance UUID
console integer console TCP port
enable_remote_console boolean enable the remote console
headless boolean headless mode
linked_clone boolean either the VM is a linked clone or not
name string VirtualBox VM instance name
project_uuid string Project UUID
uuid string VirtualBox VM instance UUID
Name Mandatory Type Description
location string Base directory where the project should be created on remote server
temporary boolean If project is a temporary project
uuid string Project UUID
uuid ['string', 'null'] Project UUID
Output diff --git a/docs/api/virtualboxuuidadaptersadapteriddnio.rst b/docs/api/virtualboxuuidadaptersadapteriddnio.rst new file mode 100644 index 00000000..6aa6a0c7 --- /dev/null +++ b/docs/api/virtualboxuuidadaptersadapteriddnio.rst @@ -0,0 +1,48 @@ +/virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio +--------------------------------------------- + +.. contents:: + +POST /virtualbox/**{uuid}**/adapters/**{adapter_id:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Add a NIO to a VirtualBox VM instance + +Parameters +********** +- **adapter_id**: Adapter where the nio should be added +- **uuid**: Instance UUID + +Response status codes +********************** +- **400**: Invalid instance UUID +- **201**: NIO created +- **404**: Instance doesn't exist + +Sample session +*************** + + +.. literalinclude:: examples/post_virtualboxuuidadaptersadapteriddnio.txt + + +DELETE /virtualbox/**{uuid}**/adapters/**{adapter_id:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Remove a NIO from a VirtualBox VM instance + +Parameters +********** +- **adapter_id**: Adapter from where the nio should be removed +- **uuid**: Instance UUID + +Response status codes +********************** +- **400**: Invalid instance UUID +- **404**: Instance doesn't exist +- **204**: NIO deleted + +Sample session +*************** + + +.. literalinclude:: examples/delete_virtualboxuuidadaptersadapteriddnio.txt + diff --git a/docs/api/virtualboxuuidcaptureadapteriddstart.rst b/docs/api/virtualboxuuidcaptureadapteriddstart.rst new file mode 100644 index 00000000..f3581528 --- /dev/null +++ b/docs/api/virtualboxuuidcaptureadapteriddstart.rst @@ -0,0 +1,29 @@ +/virtualbox/{uuid}/capture/{adapter_id:\d+}/start +--------------------------------------------- + +.. contents:: + +POST /virtualbox/**{uuid}**/capture/**{adapter_id:\d+}**/start +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Start a packet capture on a VirtualBox VM instance + +Parameters +********** +- **adapter_id**: Adapter to start a packet capture +- **uuid**: Instance UUID + +Response status codes +********************** +- **200**: Capture started +- **400**: Invalid instance UUID +- **404**: Instance doesn't exist + +Input +******* +.. raw:: html + + + + +
Name Mandatory Type Description
capture_filename string Capture file name
+ diff --git a/docs/api/virtualboxuuidcaptureadapteriddstop.rst b/docs/api/virtualboxuuidcaptureadapteriddstop.rst new file mode 100644 index 00000000..dddfcd95 --- /dev/null +++ b/docs/api/virtualboxuuidcaptureadapteriddstop.rst @@ -0,0 +1,20 @@ +/virtualbox/{uuid}/capture/{adapter_id:\d+}/stop +--------------------------------------------- + +.. contents:: + +POST /virtualbox/**{uuid}**/capture/**{adapter_id:\d+}**/stop +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Stop a packet capture on a VirtualBox VM instance + +Parameters +********** +- **adapter_id**: Adapter to stop a packet capture +- **uuid**: Instance UUID + +Response status codes +********************** +- **400**: Invalid instance UUID +- **404**: Instance doesn't exist +- **204**: Capture stopped + diff --git a/docs/api/vpcs.rst b/docs/api/vpcs.rst index 69318c8b..8344add3 100644 --- a/docs/api/vpcs.rst +++ b/docs/api/vpcs.rst @@ -22,7 +22,6 @@ Input console ['integer', 'null'] console TCP port name ✔ string VPCS device name project_uuid ✔ string Project UUID - script_file ['string', 'null'] VPCS startup script startup_script ['string', 'null'] Content of the VPCS startup script uuid string VPCS device UUID vpcs_id integer VPCS device instance ID (for project created before GNS3 1.3) diff --git a/docs/api/vpcsuuid.rst b/docs/api/vpcsuuid.rst index d13361a7..d0b3f136 100644 --- a/docs/api/vpcsuuid.rst +++ b/docs/api/vpcsuuid.rst @@ -59,7 +59,6 @@ Input Name Mandatory Type Description console ['integer', 'null'] console TCP port name ['string', 'null'] VPCS device name - script_file ['string', 'null'] VPCS startup script startup_script ['string', 'null'] Content of the VPCS startup script diff --git a/docs/api/vpcsuuidportsportnumberdnio.rst b/docs/api/vpcsuuidportsportnumberdnio.rst new file mode 100644 index 00000000..dc3870b1 --- /dev/null +++ b/docs/api/vpcsuuidportsportnumberdnio.rst @@ -0,0 +1,48 @@ +/vpcs/{uuid}/ports/{port_number:\d+}/nio +--------------------------------------------- + +.. contents:: + +POST /vpcs/**{uuid}**/ports/**{port_number:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Add a NIO to a VPCS instance + +Parameters +********** +- **port_number**: Port where the nio should be added +- **uuid**: Instance UUID + +Response status codes +********************** +- **400**: Invalid instance UUID +- **201**: NIO created +- **404**: Instance doesn't exist + +Sample session +*************** + + +.. literalinclude:: examples/post_vpcsuuidportsportnumberdnio.txt + + +DELETE /vpcs/**{uuid}**/ports/**{port_number:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Remove a NIO from a VPCS instance + +Parameters +********** +- **port_number**: Port from where the nio should be removed +- **uuid**: Instance UUID + +Response status codes +********************** +- **400**: Invalid instance UUID +- **404**: Instance doesn't exist +- **204**: NIO deleted + +Sample session +*************** + + +.. literalinclude:: examples/delete_vpcsuuidportsportnumberdnio.txt + From aeb83a79459aa267bf51a763fcde19f5e0755114 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 3 Feb 2015 21:48:20 +0100 Subject: [PATCH 171/485] Support %h in VPCS config file --- gns3server/modules/vpcs/vpcs_vm.py | 16 ++-------------- tests/modules/vpcs/test_vpcs_vm.py | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index 73dac3d1..7f4c4aa8 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -189,10 +189,11 @@ class VPCSVM(BaseVM): if self._script_file is None: self._script_file = os.path.join(self.working_dir, 'startup.vpcs') try: - with open(self._script_file, '+w') as f: + with open(self._script_file, 'w+') as f: if startup_script is None: f.write('') else: + startup_script = startup_script.replace("%h", self._name) f.write(startup_script) except OSError as e: raise VPCSError("Can't write VPCS startup file '{}'".format(self._script_file)) @@ -426,16 +427,3 @@ class VPCSVM(BaseVM): """ return self._script_file - - @script_file.setter - def script_file(self, script_file): - """ - Sets the script-file for this VPCS instance. - - :param script_file: path to base-script-file - """ - - self._script_file = script_file - log.info("VPCS {name} [{uuid}]: script_file set to {config}".format(name=self._name, - uuid=self.uuid, - config=self._script_file)) diff --git a/tests/modules/vpcs/test_vpcs_vm.py b/tests/modules/vpcs/test_vpcs_vm.py index b61caf38..f6197aa4 100644 --- a/tests/modules/vpcs/test_vpcs_vm.py +++ b/tests/modules/vpcs/test_vpcs_vm.py @@ -149,6 +149,15 @@ def test_update_startup_script(vm): assert f.read() == content +def test_update_startup_script_h(vm): + content = "setname %h\n" + vm.name = "pc1" + vm.startup_script = content + assert os.path.exists(vm.script_file) + with open(vm.script_file) as f: + assert f.read() == "setname pc1\n" + + def test_get_startup_script(vm): content = "echo GNS3 VPCS\nip 192.168.1.2\n" vm.startup_script = content @@ -159,7 +168,7 @@ def test_get_startup_script_using_default_script(vm): content = "echo GNS3 VPCS\nip 192.168.1.2\n" # Reset script file location - vm.script_file = None + vm._script_file = None filepath = os.path.join(vm.working_dir, 'startup.vpc') with open(filepath, 'w+') as f: @@ -187,19 +196,13 @@ def test_change_name(vm, tmpdir): vm.name = "world" with open(path, 'w+') as f: f.write("name world") - vm.script_file = path + vm._script_file = path vm.name = "hello" assert vm.name == "hello" with open(path) as f: assert f.read() == "name hello" -def test_change_script_file(vm, tmpdir): - path = os.path.join(str(tmpdir), 'startup2.vpcs') - vm.script_file = path - assert vm.script_file == path - - def test_close(vm, port_manager): with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM._check_requirements", return_value=True): with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): From d2699f051db3de98a392748a60d8c92363ef74fc Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 3 Feb 2015 18:23:11 -0700 Subject: [PATCH 172/485] Change URL for projects: /project becomes /projects and project_id is used instead of uuid. --- gns3server/handlers/project_handler.py | 59 +++++++++++++------------- gns3server/modules/project.py | 2 +- gns3server/schemas/project.py | 6 +-- tests/api/test_project.py | 44 +++++++++---------- tests/modules/test_project.py | 2 +- 5 files changed, 56 insertions(+), 57 deletions(-) diff --git a/gns3server/handlers/project_handler.py b/gns3server/handlers/project_handler.py index e72396f3..aab739fd 100644 --- a/gns3server/handlers/project_handler.py +++ b/gns3server/handlers/project_handler.py @@ -18,15 +18,14 @@ from ..web.route import Route from ..schemas.project import PROJECT_OBJECT_SCHEMA, PROJECT_CREATE_SCHEMA, PROJECT_UPDATE_SCHEMA from ..modules.project_manager import ProjectManager -from aiohttp.web import HTTPConflict class ProjectHandler: @classmethod @Route.post( - r"/project", - description="Create a project on the server", + r"/projects", + description="Create a new project on the server", output=PROJECT_OBJECT_SCHEMA, input=PROJECT_CREATE_SCHEMA) def create_project(request, response): @@ -34,99 +33,99 @@ class ProjectHandler: pm = ProjectManager.instance() p = pm.create_project( location=request.json.get("location"), - uuid=request.json.get("uuid"), + uuid=request.json.get("project_id"), temporary=request.json.get("temporary", False) ) response.json(p) @classmethod @Route.get( - r"/project/{uuid}", + r"/projects/{project_id}", description="Get project information", parameters={ - "uuid": "Project instance UUID", + "project_id": "The UUID of the project", }, status_codes={ - 200: "OK", - 404: "Project instance doesn't exist" + 200: "Success", + 404: "The project doesn't exist" }, output=PROJECT_OBJECT_SCHEMA) def show(request, response): pm = ProjectManager.instance() - project = pm.get_project(request.match_info["uuid"]) + project = pm.get_project(request.match_info["project_id"]) response.json(project) @classmethod @Route.put( - r"/project/{uuid}", + r"/projects/{project_id}", description="Update a project", parameters={ - "uuid": "Project instance UUID", + "project_id": "The UUID of the project", }, status_codes={ - 200: "Project updated", - 404: "Project instance doesn't exist" + 200: "The project has been updated", + 404: "The project doesn't exist" }, output=PROJECT_OBJECT_SCHEMA, input=PROJECT_UPDATE_SCHEMA) def update(request, response): pm = ProjectManager.instance() - project = pm.get_project(request.match_info["uuid"]) + project = pm.get_project(request.match_info["project_id"]) project.temporary = request.json.get("temporary", project.temporary) response.json(project) @classmethod @Route.post( - r"/project/{uuid}/commit", + r"/projects/{project_id}/commit", description="Write changes on disk", parameters={ - "uuid": "Project instance UUID", + "project_id": "The UUID of the project", }, status_codes={ - 204: "Changes write on disk", - 404: "Project instance doesn't exist" + 204: "Changes have been written on disk", + 404: "The project doesn't exist" }) def commit(request, response): pm = ProjectManager.instance() - project = pm.get_project(request.match_info["uuid"]) + project = pm.get_project(request.match_info["project_id"]) yield from project.commit() response.set_status(204) @classmethod @Route.post( - r"/project/{uuid}/close", - description="Close project", + r"/projects/{project_id}/close", + description="Close a project", parameters={ - "uuid": "Project instance UUID", + "project_id": "The UUID of the project", }, status_codes={ - 204: "Project closed", - 404: "Project instance doesn't exist" + 204: "The project has been closed", + 404: "The project doesn't exist" }) def close(request, response): pm = ProjectManager.instance() - project = pm.get_project(request.match_info["uuid"]) + project = pm.get_project(request.match_info["project_id"]) yield from project.close() response.set_status(204) @classmethod @Route.delete( - r"/project/{uuid}", + r"/projects/{project_id}", description="Delete a project from disk", parameters={ - "uuid": "Project instance UUID", + "project_id": "The UUID of the project", }, status_codes={ - 204: "Changes write on disk", - 404: "Project instance doesn't exist" + 204: "Changes have been written on disk", + 404: "The project doesn't exist" }) def delete(request, response): pm = ProjectManager.instance() - project = pm.get_project(request.match_info["uuid"]) + project = pm.get_project(request.match_info["project_id"]) yield from project.delete() response.set_status(204) diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index d385f143..f78e78f4 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -152,7 +152,7 @@ class Project: def __json__(self): return { - "uuid": self._uuid, + "project_id": self._uuid, "location": self._location, "temporary": self._temporary } diff --git a/gns3server/schemas/project.py b/gns3server/schemas/project.py index 8bb1f7de..f610b637 100644 --- a/gns3server/schemas/project.py +++ b/gns3server/schemas/project.py @@ -26,7 +26,7 @@ PROJECT_CREATE_SCHEMA = { "type": "string", "minLength": 1 }, - "uuid": { + "project_id": { "description": "Project UUID", "type": ["string", "null"], "minLength": 36, @@ -64,7 +64,7 @@ PROJECT_OBJECT_SCHEMA = { "type": "string", "minLength": 1 }, - "uuid": { + "project_id": { "description": "Project UUID", "type": "string", "minLength": 36, @@ -77,5 +77,5 @@ PROJECT_OBJECT_SCHEMA = { }, }, "additionalProperties": False, - "required": ["location", "uuid", "temporary"] + "required": ["location", "project_id", "temporary"] } diff --git a/tests/api/test_project.py b/tests/api/test_project.py index ee57954c..ec92a98d 100644 --- a/tests/api/test_project.py +++ b/tests/api/test_project.py @@ -26,89 +26,89 @@ from tests.utils import asyncio_patch def test_create_project_with_dir(server, tmpdir): with patch("gns3server.config.Config.get_section_config", return_value={"local": True}): - response = server.post("/project", {"location": str(tmpdir)}) + response = server.post("/projects", {"location": str(tmpdir)}) assert response.status == 200 assert response.json["location"] == str(tmpdir) def test_create_project_without_dir(server): query = {} - response = server.post("/project", query) + response = server.post("/projects", query) assert response.status == 200 - assert response.json["uuid"] is not None + assert response.json["project_id"] is not None assert response.json["temporary"] is False def test_create_temporary_project(server): query = {"temporary": True} - response = server.post("/project", query) + response = server.post("/projects", query) assert response.status == 200 - assert response.json["uuid"] is not None + assert response.json["project_id"] is not None assert response.json["temporary"] is True def test_create_project_with_uuid(server): - query = {"uuid": "00010203-0405-0607-0809-0a0b0c0d0e0f"} - response = server.post("/project", query) + query = {"project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f"} + response = server.post("/projects", query) assert response.status == 200 - assert response.json["uuid"] == "00010203-0405-0607-0809-0a0b0c0d0e0f" + assert response.json["project_id"] == "00010203-0405-0607-0809-0a0b0c0d0e0f" def test_show_project(server): - query = {"uuid": "00010203-0405-0607-0809-0a0b0c0d0e0f", "location": "/tmp", "temporary": False} + query = {"project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f", "location": "/tmp", "temporary": False} with patch("gns3server.config.Config.get_section_config", return_value={"local": True}): - response = server.post("/project", query) + response = server.post("/projects", query) assert response.status == 200 - response = server.get("/project/00010203-0405-0607-0809-0a0b0c0d0e0f", example=True) + response = server.get("/projects/00010203-0405-0607-0809-0a0b0c0d0e0f", example=True) assert response.json == query def test_show_project_invalid_uuid(server): - response = server.get("/project/00010203-0405-0607-0809-0a0b0c0d0e42") + response = server.get("/projects/00010203-0405-0607-0809-0a0b0c0d0e42") assert response.status == 404 def test_update_temporary_project(server): query = {"temporary": True} - response = server.post("/project", query) + response = server.post("/projects", query) assert response.status == 200 query = {"temporary": False} - response = server.put("/project/{uuid}".format(uuid=response.json["uuid"]), query, example=True) + response = server.put("/projects/{project_id}".format(project_id=response.json["project_id"]), query, example=True) assert response.status == 200 assert response.json["temporary"] is False def test_commit_project(server, project): with asyncio_patch("gns3server.modules.project.Project.commit", return_value=True) as mock: - response = server.post("/project/{uuid}/commit".format(uuid=project.uuid), example=True) + response = server.post("/projects/{project_id}/commit".format(project_id=project.uuid), example=True) assert response.status == 204 assert mock.called -def test_commit_project_invalid_project_uuid(server, project): - response = server.post("/project/{uuid}/commit".format(uuid=uuid.uuid4())) +def test_commit_project_invalid_uuid(server): + response = server.post("/projects/{project_id}/commit".format(project_id=uuid.uuid4())) assert response.status == 404 def test_delete_project(server, project): with asyncio_patch("gns3server.modules.project.Project.delete", return_value=True) as mock: - response = server.delete("/project/{uuid}".format(uuid=project.uuid), example=True) + response = server.delete("/projects/{project_id}".format(project_id=project.uuid), example=True) assert response.status == 204 assert mock.called -def test_delete_project_invalid_uuid(server, project): - response = server.delete("/project/{uuid}".format(uuid=uuid.uuid4())) +def test_delete_project_invalid_uuid(server): + response = server.delete("/projects/{project_id}".format(project_id=uuid.uuid4())) assert response.status == 404 def test_close_project(server, project): with asyncio_patch("gns3server.modules.project.Project.close", return_value=True) as mock: - response = server.post("/project/{uuid}/close".format(uuid=project.uuid), example=True) + response = server.post("/projects/{project_id}/close".format(project_id=project.uuid), example=True) assert response.status == 204 assert mock.called def test_close_project_invalid_uuid(server, project): - response = server.post("/project/{uuid}/close".format(uuid=uuid.uuid4())) + response = server.post("/projects/{project_id}/close".format(project_id=uuid.uuid4())) assert response.status == 404 diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py index f3f7b084..3c93da88 100644 --- a/tests/modules/test_project.py +++ b/tests/modules/test_project.py @@ -68,7 +68,7 @@ def test_changing_location_not_allowed(tmpdir): def test_json(tmpdir): p = Project() - assert p.__json__() == {"location": p.location, "uuid": p.uuid, "temporary": False} + assert p.__json__() == {"location": p.location, "project_id": p.uuid, "temporary": False} def test_vm_working_directory(tmpdir, vm): From 59c82e26df0c05be5c6a2a3e37f27a3aee43224d Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 3 Feb 2015 18:40:13 -0700 Subject: [PATCH 173/485] Use project_id instead of project_uuid for the API. --- gns3server/handlers/vpcs_handler.py | 2 +- gns3server/modules/project_manager.py | 14 +++++++------- gns3server/modules/virtualbox/virtualbox_vm.py | 2 +- gns3server/modules/vpcs/vpcs_vm.py | 2 +- gns3server/schemas/virtualbox.py | 8 ++++---- gns3server/schemas/vpcs.py | 8 ++++---- tests/api/test_virtualbox.py | 8 ++++---- tests/api/test_vpcs.py | 16 ++++++++-------- 8 files changed, 30 insertions(+), 30 deletions(-) diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index a63f475a..cbd78c83 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -44,7 +44,7 @@ class VPCSHandler: vpcs = VPCS.instance() vm = yield from vpcs.create_vm(request.json["name"], - request.json["project_uuid"], + request.json["project_id"], request.json.get("uuid"), console=request.json.get("console"), startup_script=request.json.get("startup_script")) diff --git a/gns3server/modules/project_manager.py b/gns3server/modules/project_manager.py index edcfe425..1aa264b1 100644 --- a/gns3server/modules/project_manager.py +++ b/gns3server/modules/project_manager.py @@ -42,23 +42,23 @@ class ProjectManager: cls._instance = cls() return cls._instance - def get_project(self, project_uuid): + def get_project(self, uuid): """ Returns a Project instance. - :param project_uuid: Project UUID + :param uuid: Project UUID :returns: Project instance """ try: - UUID(project_uuid, version=4) + UUID(uuid, version=4) except ValueError: - raise aiohttp.web.HTTPBadRequest(text="{} is not a valid UUID".format(project_uuid)) + raise aiohttp.web.HTTPBadRequest(text="{} is not a valid UUID".format(uuid)) - if project_uuid not in self._projects: - raise aiohttp.web.HTTPNotFound(text="Project UUID {} doesn't exist".format(project_uuid)) - return self._projects[project_uuid] + if uuid not in self._projects: + raise aiohttp.web.HTTPNotFound(text="Project UUID {} doesn't exist".format(uuid)) + return self._projects[uuid] def create_project(self, **kwargs): """ diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index a5a126c6..018527c1 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -79,7 +79,7 @@ class VirtualBoxVM(BaseVM): return {"name": self.name, "uuid": self.uuid, "console": self.console, - "project_uuid": self.project.uuid, + "project_id": self.project.uuid, "vmname": self.vmname, "headless": self.headless, "enable_remote_console": self.enable_remote_console, diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index 7f4c4aa8..ce930410 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -107,7 +107,7 @@ class VPCSVM(BaseVM): return {"name": self.name, "uuid": self.uuid, "console": self._console, - "project_uuid": self.project.uuid, + "project_id": self.project.uuid, "script_file": self.script_file, "startup_script": self.startup_script} diff --git a/gns3server/schemas/virtualbox.py b/gns3server/schemas/virtualbox.py index 713d6f74..1898a280 100644 --- a/gns3server/schemas/virtualbox.py +++ b/gns3server/schemas/virtualbox.py @@ -32,7 +32,7 @@ VBOX_CREATE_SCHEMA = { "description": "VirtualBox VM instance ID (for project created before GNS3 1.3)", "type": "integer" }, - "project_uuid": { + "project_id": { "description": "Project UUID", "type": "string", "minLength": 36, @@ -86,7 +86,7 @@ VBOX_CREATE_SCHEMA = { }, }, "additionalProperties": False, - "required": ["name", "vmname", "linked_clone", "project_uuid"], + "required": ["name", "vmname", "linked_clone", "project_id"], } VBOX_UPDATE_SCHEMA = { @@ -211,7 +211,7 @@ VBOX_OBJECT_SCHEMA = { "maxLength": 36, "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" }, - "project_uuid": { + "project_id": { "description": "Project UUID", "type": "string", "minLength": 36, @@ -256,5 +256,5 @@ VBOX_OBJECT_SCHEMA = { }, }, "additionalProperties": False, - "required": ["name", "uuid", "project_uuid"] + "required": ["name", "uuid", "project_id"] } diff --git a/gns3server/schemas/vpcs.py b/gns3server/schemas/vpcs.py index b4841034..fae10fe9 100644 --- a/gns3server/schemas/vpcs.py +++ b/gns3server/schemas/vpcs.py @@ -37,7 +37,7 @@ VPCS_CREATE_SCHEMA = { "maxLength": 36, "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" }, - "project_uuid": { + "project_id": { "description": "Project UUID", "type": "string", "minLength": 36, @@ -56,7 +56,7 @@ VPCS_CREATE_SCHEMA = { }, }, "additionalProperties": False, - "required": ["name", "project_uuid"] + "required": ["name", "project_id"] } VPCS_UPDATE_SCHEMA = { @@ -162,7 +162,7 @@ VPCS_OBJECT_SCHEMA = { "maximum": 65535, "type": "integer" }, - "project_uuid": { + "project_id": { "description": "Project UUID", "type": "string", "minLength": 36, @@ -179,5 +179,5 @@ VPCS_OBJECT_SCHEMA = { }, }, "additionalProperties": False, - "required": ["name", "uuid", "console", "project_uuid"] + "required": ["name", "uuid", "console", "project_id"] } diff --git a/tests/api/test_virtualbox.py b/tests/api/test_virtualbox.py index 4c7a7e5c..08783af3 100644 --- a/tests/api/test_virtualbox.py +++ b/tests/api/test_virtualbox.py @@ -25,7 +25,7 @@ def vm(server, project): response = server.post("/virtualbox", {"name": "VMTEST", "vmname": "VMTEST", "linked_clone": False, - "project_uuid": project.uuid}) + "project_id": project.uuid}) assert mock.called assert response.status == 201 return response.json @@ -37,11 +37,11 @@ def test_vbox_create(server, project): response = server.post("/virtualbox", {"name": "VM1", "vmname": "VM1", "linked_clone": False, - "project_uuid": project.uuid}, + "project_id": project.uuid}, example=True) assert response.status == 201 assert response.json["name"] == "VM1" - assert response.json["project_uuid"] == project.uuid + assert response.json["project_id"] == project.uuid def test_vbox_get(server, project, vm): @@ -49,7 +49,7 @@ def test_vbox_get(server, project, vm): assert response.status == 200 assert response.route == "/virtualbox/{uuid}" assert response.json["name"] == "VMTEST" - assert response.json["project_uuid"] == project.uuid + assert response.json["project_id"] == project.uuid def test_vbox_start(server, vm): diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index 56c970a2..7304f143 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -23,17 +23,17 @@ from unittest.mock import patch @pytest.fixture(scope="module") def vm(server, project): - response = server.post("/vpcs", {"name": "PC TEST 1", "project_uuid": project.uuid}) + response = server.post("/vpcs", {"name": "PC TEST 1", "project_id": project.uuid}) assert response.status == 201 return response.json def test_vpcs_create(server, project): - response = server.post("/vpcs", {"name": "PC TEST 1", "project_uuid": project.uuid}, example=True) + response = server.post("/vpcs", {"name": "PC TEST 1", "project_id": project.uuid}, example=True) assert response.status == 201 assert response.route == "/vpcs" assert response.json["name"] == "PC TEST 1" - assert response.json["project_uuid"] == project.uuid + assert response.json["project_id"] == project.uuid assert response.json["script_file"] is None @@ -42,24 +42,24 @@ def test_vpcs_get(server, project, vm): assert response.status == 200 assert response.route == "/vpcs/{uuid}" assert response.json["name"] == "PC TEST 1" - assert response.json["project_uuid"] == project.uuid + assert response.json["project_id"] == project.uuid def test_vpcs_create_startup_script(server, project): - response = server.post("/vpcs", {"name": "PC TEST 1", "project_uuid": project.uuid, "startup_script": "ip 192.168.1.2\necho TEST"}) + response = server.post("/vpcs", {"name": "PC TEST 1", "project_id": project.uuid, "startup_script": "ip 192.168.1.2\necho TEST"}) assert response.status == 201 assert response.route == "/vpcs" assert response.json["name"] == "PC TEST 1" - assert response.json["project_uuid"] == project.uuid + assert response.json["project_id"] == project.uuid assert response.json["startup_script"] == "ip 192.168.1.2\necho TEST" def test_vpcs_create_port(server, project, free_console_port): - response = server.post("/vpcs", {"name": "PC TEST 1", "project_uuid": project.uuid, "console": free_console_port}) + response = server.post("/vpcs", {"name": "PC TEST 1", "project_id": project.uuid, "console": free_console_port}) assert response.status == 201 assert response.route == "/vpcs" assert response.json["name"] == "PC TEST 1" - assert response.json["project_uuid"] == project.uuid + assert response.json["project_id"] == project.uuid assert response.json["console"] == free_console_port From 119bebee25b2f5359ca05cd6f739551aaeaf3515 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 3 Feb 2015 18:44:04 -0700 Subject: [PATCH 174/485] Documentation. --- ...te_virtualboxuuidadaptersadapteriddnio.txt | 4 +- .../delete_vpcsuuidportsportnumberdnio.txt | 4 +- docs/api/examples/get_interfaces.txt | 50 +++++-------------- docs/api/examples/get_version.txt | 4 +- docs/api/examples/get_virtualboxuuid.txt | 10 ++-- docs/api/examples/get_vpcsuuid.txt | 10 ++-- docs/api/examples/post_portsudp.txt | 4 +- docs/api/examples/post_version.txt | 4 +- docs/api/examples/post_virtualbox.txt | 14 +++--- ...st_virtualboxuuidadaptersadapteriddnio.txt | 4 +- docs/api/examples/post_vpcs.txt | 14 +++--- .../post_vpcsuuidportsportnumberdnio.txt | 4 +- docs/api/virtualbox.rst | 4 +- docs/api/virtualboxuuid.rst | 4 +- .../virtualboxuuidadaptersadapteriddnio.rst | 4 +- .../virtualboxuuidcaptureadapteriddstart.rst | 2 +- .../virtualboxuuidcaptureadapteriddstop.rst | 2 +- docs/api/vpcs.rst | 4 +- docs/api/vpcsuuid.rst | 4 +- docs/api/vpcsuuidportsportnumberdnio.rst | 4 +- gns3server/handlers/virtualbox_handler.py | 2 +- 21 files changed, 66 insertions(+), 90 deletions(-) diff --git a/docs/api/examples/delete_virtualboxuuidadaptersadapteriddnio.txt b/docs/api/examples/delete_virtualboxuuidadaptersadapteriddnio.txt index cf775417..2ad343a5 100644 --- a/docs/api/examples/delete_virtualboxuuidadaptersadapteriddnio.txt +++ b/docs/api/examples/delete_virtualboxuuidadaptersadapteriddnio.txt @@ -5,9 +5,9 @@ DELETE /virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio HTTP/1.1 HTTP/1.1 204 -CONNECTION: keep-alive +CONNECTION: close CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio diff --git a/docs/api/examples/delete_vpcsuuidportsportnumberdnio.txt b/docs/api/examples/delete_vpcsuuidportsportnumberdnio.txt index c2a772f2..3a17f8b1 100644 --- a/docs/api/examples/delete_vpcsuuidportsportnumberdnio.txt +++ b/docs/api/examples/delete_vpcsuuidportsportnumberdnio.txt @@ -5,9 +5,9 @@ DELETE /vpcs/{uuid}/ports/{port_number:\d+}/nio HTTP/1.1 HTTP/1.1 204 -CONNECTION: keep-alive +CONNECTION: close CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /vpcs/{uuid}/ports/{port_number:\d+}/nio diff --git a/docs/api/examples/get_interfaces.txt b/docs/api/examples/get_interfaces.txt index 74381d7e..289c74e0 100644 --- a/docs/api/examples/get_interfaces.txt +++ b/docs/api/examples/get_interfaces.txt @@ -5,56 +5,32 @@ GET /interfaces HTTP/1.1 HTTP/1.1 200 -CONNECTION: keep-alive -CONTENT-LENGTH: 652 +CONNECTION: close +CONTENT-LENGTH: 298 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /interfaces [ { - "id": "lo0", - "name": "lo0" + "id": "lo", + "name": "lo" }, { - "id": "gif0", - "name": "gif0" + "id": "eth0", + "name": "eth0" }, { - "id": "stf0", - "name": "stf0" + "id": "wlan0", + "name": "wlan0" }, { - "id": "en0", - "name": "en0" + "id": "vmnet1", + "name": "vmnet1" }, { - "id": "en1", - "name": "en1" - }, - { - "id": "fw0", - "name": "fw0" - }, - { - "id": "en2", - "name": "en2" - }, - { - "id": "p2p0", - "name": "p2p0" - }, - { - "id": "bridge0", - "name": "bridge0" - }, - { - "id": "vboxnet0", - "name": "vboxnet0" - }, - { - "id": "vboxnet1", - "name": "vboxnet1" + "id": "vmnet8", + "name": "vmnet8" } ] diff --git a/docs/api/examples/get_version.txt b/docs/api/examples/get_version.txt index 39509cda..59bdb128 100644 --- a/docs/api/examples/get_version.txt +++ b/docs/api/examples/get_version.txt @@ -5,11 +5,11 @@ GET /version HTTP/1.1 HTTP/1.1 200 -CONNECTION: keep-alive +CONNECTION: close CONTENT-LENGTH: 29 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /version { diff --git a/docs/api/examples/get_virtualboxuuid.txt b/docs/api/examples/get_virtualboxuuid.txt index 4322211e..3ed0b071 100644 --- a/docs/api/examples/get_virtualboxuuid.txt +++ b/docs/api/examples/get_virtualboxuuid.txt @@ -5,11 +5,11 @@ GET /virtualbox/{uuid} HTTP/1.1 HTTP/1.1 200 -CONNECTION: keep-alive -CONTENT-LENGTH: 348 +CONNECTION: close +CONTENT-LENGTH: 346 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /virtualbox/{uuid} { @@ -20,7 +20,7 @@ X-ROUTE: /virtualbox/{uuid} "enable_remote_console": false, "headless": false, "name": "VMTEST", - "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "uuid": "c6b74706-54b6-405d-8aea-9c48a94987e8", + "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", + "uuid": "f246f72b-9f79-4a60-a8fb-0b2375d29d28", "vmname": "VMTEST" } diff --git a/docs/api/examples/get_vpcsuuid.txt b/docs/api/examples/get_vpcsuuid.txt index 5aab7abd..2d191100 100644 --- a/docs/api/examples/get_vpcsuuid.txt +++ b/docs/api/examples/get_vpcsuuid.txt @@ -5,18 +5,18 @@ GET /vpcs/{uuid} HTTP/1.1 HTTP/1.1 200 -CONNECTION: keep-alive -CONTENT-LENGTH: 213 +CONNECTION: close +CONTENT-LENGTH: 211 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /vpcs/{uuid} { "console": 2003, "name": "PC TEST 1", - "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", + "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, "startup_script": null, - "uuid": "aceec3c0-f02d-44ee-88a6-85c3120c22ca" + "uuid": "0300d8a7-e971-4402-bd85-8c12384d308d" } diff --git a/docs/api/examples/post_portsudp.txt b/docs/api/examples/post_portsudp.txt index 8e7ea7f1..ff13ecb6 100644 --- a/docs/api/examples/post_portsudp.txt +++ b/docs/api/examples/post_portsudp.txt @@ -5,11 +5,11 @@ POST /ports/udp HTTP/1.1 HTTP/1.1 201 -CONNECTION: keep-alive +CONNECTION: close CONTENT-LENGTH: 25 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /ports/udp { diff --git a/docs/api/examples/post_version.txt b/docs/api/examples/post_version.txt index 1d4ecc7d..45ef1069 100644 --- a/docs/api/examples/post_version.txt +++ b/docs/api/examples/post_version.txt @@ -7,11 +7,11 @@ POST /version HTTP/1.1 HTTP/1.1 200 -CONNECTION: keep-alive +CONNECTION: close CONTENT-LENGTH: 29 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /version { diff --git a/docs/api/examples/post_virtualbox.txt b/docs/api/examples/post_virtualbox.txt index 62e5fe4e..4ed3b787 100644 --- a/docs/api/examples/post_virtualbox.txt +++ b/docs/api/examples/post_virtualbox.txt @@ -1,20 +1,20 @@ -curl -i -X POST 'http://localhost:8000/virtualbox' -d '{"linked_clone": false, "name": "VM1", "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "vmname": "VM1"}' +curl -i -X POST 'http://localhost:8000/virtualbox' -d '{"linked_clone": false, "name": "VM1", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "vmname": "VM1"}' POST /virtualbox HTTP/1.1 { "linked_clone": false, "name": "VM1", - "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", + "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "vmname": "VM1" } HTTP/1.1 201 -CONNECTION: keep-alive -CONTENT-LENGTH: 342 +CONNECTION: close +CONTENT-LENGTH: 340 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /virtualbox { @@ -25,7 +25,7 @@ X-ROUTE: /virtualbox "enable_remote_console": false, "headless": false, "name": "VM1", - "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "uuid": "7ba9ddf4-0a6a-48a0-8483-3450a9f305df", + "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", + "uuid": "210fbbba-4025-4286-81dc-1f07cc494cc9", "vmname": "VM1" } diff --git a/docs/api/examples/post_virtualboxuuidadaptersadapteriddnio.txt b/docs/api/examples/post_virtualboxuuidadaptersadapteriddnio.txt index f3caae78..04674eef 100644 --- a/docs/api/examples/post_virtualboxuuidadaptersadapteriddnio.txt +++ b/docs/api/examples/post_virtualboxuuidadaptersadapteriddnio.txt @@ -10,11 +10,11 @@ POST /virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio HTTP/1.1 HTTP/1.1 201 -CONNECTION: keep-alive +CONNECTION: close CONTENT-LENGTH: 89 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio { diff --git a/docs/api/examples/post_vpcs.txt b/docs/api/examples/post_vpcs.txt index 054660c9..c2ea6413 100644 --- a/docs/api/examples/post_vpcs.txt +++ b/docs/api/examples/post_vpcs.txt @@ -1,25 +1,25 @@ -curl -i -X POST 'http://localhost:8000/vpcs' -d '{"name": "PC TEST 1", "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80"}' +curl -i -X POST 'http://localhost:8000/vpcs' -d '{"name": "PC TEST 1", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80"}' POST /vpcs HTTP/1.1 { "name": "PC TEST 1", - "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80" + "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80" } HTTP/1.1 201 -CONNECTION: keep-alive -CONTENT-LENGTH: 213 +CONNECTION: close +CONTENT-LENGTH: 211 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /vpcs { "console": 2001, "name": "PC TEST 1", - "project_uuid": "a1e920ca-338a-4e9f-b363-aa607b09dd80", + "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, "startup_script": null, - "uuid": "896e7bbd-936c-4a6f-9268-c89f8eb91f5e" + "uuid": "92ff89ed-aed2-487c-b893-5559ca258d0f" } diff --git a/docs/api/examples/post_vpcsuuidportsportnumberdnio.txt b/docs/api/examples/post_vpcsuuidportsportnumberdnio.txt index b3bbb41f..82abf86c 100644 --- a/docs/api/examples/post_vpcsuuidportsportnumberdnio.txt +++ b/docs/api/examples/post_vpcsuuidportsportnumberdnio.txt @@ -10,11 +10,11 @@ POST /vpcs/{uuid}/ports/{port_number:\d+}/nio HTTP/1.1 HTTP/1.1 201 -CONNECTION: keep-alive +CONNECTION: close CONTENT-LENGTH: 89 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /vpcs/{uuid}/ports/{port_number:\d+}/nio { diff --git a/docs/api/virtualbox.rst b/docs/api/virtualbox.rst index d1f40822..d85ef101 100644 --- a/docs/api/virtualbox.rst +++ b/docs/api/virtualbox.rst @@ -27,7 +27,7 @@ Input headless boolean headless mode linked_clone ✔ boolean either the VM is a linked clone or not name ✔ string VirtualBox VM instance name - project_uuid ✔ string Project UUID + project_id ✔ string Project UUID uuid string VirtualBox VM instance UUID vbox_id integer VirtualBox VM instance ID (for project created before GNS3 1.3) vmname ✔ string VirtualBox VM name (in VirtualBox itself) @@ -46,7 +46,7 @@ Output enable_remote_console boolean enable the remote console headless boolean headless mode name ✔ string VirtualBox VM instance name - project_uuid ✔ string Project UUID + project_id ✔ string Project UUID uuid ✔ string VirtualBox VM instance UUID vmname string VirtualBox VM name (in VirtualBox itself) diff --git a/docs/api/virtualboxuuid.rst b/docs/api/virtualboxuuid.rst index aea02b3a..c9dfaa58 100644 --- a/docs/api/virtualboxuuid.rst +++ b/docs/api/virtualboxuuid.rst @@ -29,7 +29,7 @@ Output enable_remote_console boolean enable the remote console headless boolean headless mode name ✔ string VirtualBox VM instance name - project_uuid ✔ string Project UUID + project_id ✔ string Project UUID uuid ✔ string VirtualBox VM instance UUID vmname string VirtualBox VM name (in VirtualBox itself) @@ -84,7 +84,7 @@ Output enable_remote_console boolean enable the remote console headless boolean headless mode name ✔ string VirtualBox VM instance name - project_uuid ✔ string Project UUID + project_id ✔ string Project UUID uuid ✔ string VirtualBox VM instance UUID vmname string VirtualBox VM name (in VirtualBox itself) diff --git a/docs/api/virtualboxuuidadaptersadapteriddnio.rst b/docs/api/virtualboxuuidadaptersadapteriddnio.rst index 6aa6a0c7..843fcd0b 100644 --- a/docs/api/virtualboxuuidadaptersadapteriddnio.rst +++ b/docs/api/virtualboxuuidadaptersadapteriddnio.rst @@ -9,8 +9,8 @@ Add a NIO to a VirtualBox VM instance Parameters ********** -- **adapter_id**: Adapter where the nio should be added - **uuid**: Instance UUID +- **adapter_id**: Adapter where the nio should be added Response status codes ********************** @@ -31,8 +31,8 @@ Remove a NIO from a VirtualBox VM instance Parameters ********** -- **adapter_id**: Adapter from where the nio should be removed - **uuid**: Instance UUID +- **adapter_id**: Adapter from where the nio should be removed Response status codes ********************** diff --git a/docs/api/virtualboxuuidcaptureadapteriddstart.rst b/docs/api/virtualboxuuidcaptureadapteriddstart.rst index f3581528..d7db9429 100644 --- a/docs/api/virtualboxuuidcaptureadapteriddstart.rst +++ b/docs/api/virtualboxuuidcaptureadapteriddstart.rst @@ -9,8 +9,8 @@ Start a packet capture on a VirtualBox VM instance Parameters ********** -- **adapter_id**: Adapter to start a packet capture - **uuid**: Instance UUID +- **adapter_id**: Adapter to start a packet capture Response status codes ********************** diff --git a/docs/api/virtualboxuuidcaptureadapteriddstop.rst b/docs/api/virtualboxuuidcaptureadapteriddstop.rst index dddfcd95..c4067108 100644 --- a/docs/api/virtualboxuuidcaptureadapteriddstop.rst +++ b/docs/api/virtualboxuuidcaptureadapteriddstop.rst @@ -9,8 +9,8 @@ Stop a packet capture on a VirtualBox VM instance Parameters ********** -- **adapter_id**: Adapter to stop a packet capture - **uuid**: Instance UUID +- **adapter_id**: Adapter to stop a packet capture Response status codes ********************** diff --git a/docs/api/vpcs.rst b/docs/api/vpcs.rst index 8344add3..36c55b71 100644 --- a/docs/api/vpcs.rst +++ b/docs/api/vpcs.rst @@ -21,7 +21,7 @@ Input Name Mandatory Type Description console ['integer', 'null'] console TCP port name ✔ string VPCS device name - project_uuid ✔ string Project UUID + project_id ✔ string Project UUID startup_script ['string', 'null'] Content of the VPCS startup script uuid string VPCS device UUID vpcs_id integer VPCS device instance ID (for project created before GNS3 1.3) @@ -35,7 +35,7 @@ Output Name Mandatory Type Description console ✔ integer console TCP port name ✔ string VPCS device name - project_uuid ✔ string Project UUID + project_id ✔ string Project UUID script_file ['string', 'null'] VPCS startup script startup_script ['string', 'null'] Content of the VPCS startup script uuid ✔ string VPCS device UUID diff --git a/docs/api/vpcsuuid.rst b/docs/api/vpcsuuid.rst index d0b3f136..67c2883a 100644 --- a/docs/api/vpcsuuid.rst +++ b/docs/api/vpcsuuid.rst @@ -24,7 +24,7 @@ Output Name Mandatory Type Description console ✔ integer console TCP port name ✔ string VPCS device name - project_uuid ✔ string Project UUID + project_id ✔ string Project UUID script_file ['string', 'null'] VPCS startup script startup_script ['string', 'null'] Content of the VPCS startup script uuid ✔ string VPCS device UUID @@ -70,7 +70,7 @@ Output Name Mandatory Type Description console ✔ integer console TCP port name ✔ string VPCS device name - project_uuid ✔ string Project UUID + project_id ✔ string Project UUID script_file ['string', 'null'] VPCS startup script startup_script ['string', 'null'] Content of the VPCS startup script uuid ✔ string VPCS device UUID diff --git a/docs/api/vpcsuuidportsportnumberdnio.rst b/docs/api/vpcsuuidportsportnumberdnio.rst index dc3870b1..e16cd3f9 100644 --- a/docs/api/vpcsuuidportsportnumberdnio.rst +++ b/docs/api/vpcsuuidportsportnumberdnio.rst @@ -9,8 +9,8 @@ Add a NIO to a VPCS instance Parameters ********** -- **port_number**: Port where the nio should be added - **uuid**: Instance UUID +- **port_number**: Port where the nio should be added Response status codes ********************** @@ -31,8 +31,8 @@ Remove a NIO from a VPCS instance Parameters ********** -- **port_number**: Port from where the nio should be removed - **uuid**: Instance UUID +- **port_number**: Port from where the nio should be removed Response status codes ********************** diff --git a/gns3server/handlers/virtualbox_handler.py b/gns3server/handlers/virtualbox_handler.py index dfb8da7a..74202c50 100644 --- a/gns3server/handlers/virtualbox_handler.py +++ b/gns3server/handlers/virtualbox_handler.py @@ -59,7 +59,7 @@ class VirtualBoxHandler: vbox_manager = VirtualBox.instance() vm = yield from vbox_manager.create_vm(request.json.pop("name"), - request.json.pop("project_uuid"), + request.json.pop("project_id"), request.json.get("uuid"), request.json.pop("vmname"), request.json.pop("linked_clone"), From 08158884a4fae431a515f9b47173ebb76cc0fdcb Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 4 Feb 2015 10:24:59 +0100 Subject: [PATCH 175/485] Add api versionning --- gns3server/web/route.py | 3 ++- tests/api/base.py | 4 ++-- tox.ini | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/gns3server/web/route.py b/gns3server/web/route.py index 1aef6ed9..10c28205 100644 --- a/gns3server/web/route.py +++ b/gns3server/web/route.py @@ -84,7 +84,8 @@ class Route(object): # This block is executed only the first time output_schema = kw.get("output", {}) input_schema = kw.get("input", {}) - cls._path = path + api_version = kw.get("version", 1) + cls._path = "/v{version}{path}".format(path=path, version=api_version) cls._documentation.setdefault(cls._path, {"methods": []}) def register(func): diff --git a/tests/api/base.py b/tests/api/base.py index 01d460d1..41a553d2 100644 --- a/tests/api/base.py +++ b/tests/api/base.py @@ -45,7 +45,7 @@ class Query: return self._fetch("DELETE", path, **kwargs) def _get_url(self, path): - return "http://{}:{}{}".format(self._host, self._port, path) + return "http://{}:{}/v1{}".format(self._host, self._port, path) def _fetch(self, method, path, body=None, **kwargs): """Fetch an url, parse the JSON and return response @@ -74,7 +74,7 @@ class Query: asyncio.async(go(future, response)) self._loop.run_until_complete(future) response.body = future.result() - response.route = response.headers.get('X-Route', None) + response.route = response.headers.get('X-Route', None).replace("/v1", "") if response.body is not None: try: diff --git a/tox.ini b/tox.ini index a6c23dee..09c25a0c 100644 --- a/tox.ini +++ b/tox.ini @@ -10,4 +10,4 @@ ignore = E501 [pytest] norecursedirs = old_tests .tox -timeout = 10 +timeout = 1 From ca354ae7f2b212c0b9f342589e462fd6a70ddba6 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 4 Feb 2015 10:31:31 +0100 Subject: [PATCH 176/485] Doc API V1 --- .../api/examples/delete_projectsprojectid.txt | 13 +++++ docs/api/examples/delete_projectuuid.txt | 13 ----- ...te_virtualboxuuidadaptersadapteriddnio.txt | 6 +- .../delete_virtualboxuuidportsportiddnio.txt | 13 ----- .../examples/delete_vpcsiddportsportidnio.txt | 15 ----- .../delete_vpcsuuidportsportiddnio.txt | 13 ----- .../delete_vpcsuuidportsportidnio.txt | 13 ----- .../delete_vpcsuuidportsportnumberdnio.txt | 6 +- .../delete_vpcsvpcsidportsportidnio.txt | 15 ----- docs/api/examples/get_interfaces.txt | 52 ++++++++++++----- docs/api/examples/get_projectsprojectid.txt | 19 +++++++ docs/api/examples/get_projectuuid.txt | 19 ------- docs/api/examples/get_version.txt | 6 +- docs/api/examples/get_virtualboxuuid.txt | 8 +-- docs/api/examples/get_vpcsuuid.txt | 8 +-- docs/api/examples/post_portsudp.txt | 6 +- docs/api/examples/post_project.txt | 22 -------- .../examples/post_projectsprojectidclose.txt | 13 +++++ .../examples/post_projectsprojectidcommit.txt | 13 +++++ docs/api/examples/post_projectuuidclose.txt | 13 ----- docs/api/examples/post_projectuuidcommit.txt | 13 ----- docs/api/examples/post_udp.txt | 17 ------ docs/api/examples/post_version.txt | 6 +- docs/api/examples/post_virtualbox.txt | 8 +-- ...st_virtualboxuuidadaptersadapteriddnio.txt | 6 +- .../post_virtualboxuuidportsportiddnio.txt | 25 --------- .../api/examples/post_virtualboxuuidstart.txt | 13 ----- docs/api/examples/post_virtualboxuuidstop.txt | 13 ----- docs/api/examples/post_vpcs.txt | 8 +-- .../examples/post_vpcsiddportsportidnio.txt | 25 --------- .../examples/post_vpcsuuidportsportiddnio.txt | 25 --------- .../examples/post_vpcsuuidportsportidnio.txt | 25 --------- .../post_vpcsuuidportsportnumberdnio.txt | 6 +- docs/api/examples/post_vpcsvpcsidnio.txt | 28 ---------- .../post_vpcsvpcsidportsportidnio.txt | 25 --------- docs/api/examples/put_projectsprojectid.txt | 21 +++++++ docs/api/examples/put_projectuuid.txt | 21 ------- docs/api/interfaces.rst | 19 ------- docs/api/portsudp.rst | 19 ------- docs/api/projectuuidclose.rst | 24 -------- docs/api/projectuuidcommit.rst | 24 -------- docs/api/udp.rst | 19 ------- docs/api/v1interfaces.rst | 13 +++++ docs/api/v1portsudp.rst | 13 +++++ docs/api/{project.rst => v1projects.rst} | 20 +++---- ...rojectuuid.rst => v1projectsprojectid.rst} | 56 +++++++------------ docs/api/v1projectsprojectidclose.rst | 18 ++++++ docs/api/v1projectsprojectidcommit.rst | 18 ++++++ docs/api/{version.rst => v1version.rst} | 24 ++------ docs/api/{virtualbox.rst => v1virtualbox.rst} | 14 ++--- ...irtualboxuuid.rst => v1virtualboxuuid.rst} | 22 +++----- ...v1virtualboxuuidadaptersadapteriddnio.rst} | 24 ++------ ...1virtualboxuuidcaptureadapteriddstart.rst} | 8 +-- ...v1virtualboxuuidcaptureadapteriddstop.rst} | 8 +-- ...dreload.rst => v1virtualboxuuidreload.rst} | 8 +-- ...dresume.rst => v1virtualboxuuidresume.rst} | 8 +-- docs/api/v1virtualboxuuidstart.rst | 19 +++++++ docs/api/v1virtualboxuuidstop.rst | 19 +++++++ ...uspend.rst => v1virtualboxuuidsuspend.rst} | 8 +-- docs/api/v1virtualboxvms.rst | 13 +++++ docs/api/{vpcs.rst => v1vpcs.rst} | 14 ++--- docs/api/{vpcsuuid.rst => v1vpcsuuid.rst} | 22 +++----- ....rst => v1vpcsuuidportsportnumberdnio.rst} | 24 ++------ ...pcsuuidreload.rst => v1vpcsuuidreload.rst} | 8 +-- ...{vpcsuuidstart.rst => v1vpcsuuidstart.rst} | 8 +-- .../{vpcsuuidstop.rst => v1vpcsuuidstop.rst} | 8 +-- docs/api/virtualboxlist.rst | 13 ----- .../api/virtualboxuuidcaptureportiddstart.rst | 29 ---------- docs/api/virtualboxuuidcaptureportiddstop.rst | 20 ------- docs/api/virtualboxuuidportsportiddnio.rst | 48 ---------------- docs/api/virtualboxuuidstart.rst | 25 --------- docs/api/virtualboxuuidstop.rst | 25 --------- docs/api/virtualboxvms.rst | 13 ----- docs/api/vpcsuuidportsportiddnio.rst | 48 ---------------- docs/api/vpcsuuidportsportidnio.rst | 48 ---------------- gns3server/web/documentation.py | 4 +- scripts/documentation.sh | 3 + 77 files changed, 372 insertions(+), 977 deletions(-) create mode 100644 docs/api/examples/delete_projectsprojectid.txt delete mode 100644 docs/api/examples/delete_projectuuid.txt delete mode 100644 docs/api/examples/delete_virtualboxuuidportsportiddnio.txt delete mode 100644 docs/api/examples/delete_vpcsiddportsportidnio.txt delete mode 100644 docs/api/examples/delete_vpcsuuidportsportiddnio.txt delete mode 100644 docs/api/examples/delete_vpcsuuidportsportidnio.txt delete mode 100644 docs/api/examples/delete_vpcsvpcsidportsportidnio.txt create mode 100644 docs/api/examples/get_projectsprojectid.txt delete mode 100644 docs/api/examples/get_projectuuid.txt delete mode 100644 docs/api/examples/post_project.txt create mode 100644 docs/api/examples/post_projectsprojectidclose.txt create mode 100644 docs/api/examples/post_projectsprojectidcommit.txt delete mode 100644 docs/api/examples/post_projectuuidclose.txt delete mode 100644 docs/api/examples/post_projectuuidcommit.txt delete mode 100644 docs/api/examples/post_udp.txt delete mode 100644 docs/api/examples/post_virtualboxuuidportsportiddnio.txt delete mode 100644 docs/api/examples/post_virtualboxuuidstart.txt delete mode 100644 docs/api/examples/post_virtualboxuuidstop.txt delete mode 100644 docs/api/examples/post_vpcsiddportsportidnio.txt delete mode 100644 docs/api/examples/post_vpcsuuidportsportiddnio.txt delete mode 100644 docs/api/examples/post_vpcsuuidportsportidnio.txt delete mode 100644 docs/api/examples/post_vpcsvpcsidnio.txt delete mode 100644 docs/api/examples/post_vpcsvpcsidportsportidnio.txt create mode 100644 docs/api/examples/put_projectsprojectid.txt delete mode 100644 docs/api/examples/put_projectuuid.txt delete mode 100644 docs/api/interfaces.rst delete mode 100644 docs/api/portsudp.rst delete mode 100644 docs/api/projectuuidclose.rst delete mode 100644 docs/api/projectuuidcommit.rst delete mode 100644 docs/api/udp.rst create mode 100644 docs/api/v1interfaces.rst create mode 100644 docs/api/v1portsudp.rst rename docs/api/{project.rst => v1projects.rst} (69%) rename docs/api/{projectuuid.rst => v1projectsprojectid.rst} (62%) create mode 100644 docs/api/v1projectsprojectidclose.rst create mode 100644 docs/api/v1projectsprojectidcommit.rst rename docs/api/{version.rst => v1version.rst} (80%) rename docs/api/{virtualbox.rst => v1virtualbox.rst} (95%) rename docs/api/{virtualboxuuid.rst => v1virtualboxuuid.rst} (93%) rename docs/api/{virtualboxuuidadaptersadapteriddnio.rst => v1virtualboxuuidadaptersadapteriddnio.rst} (51%) rename docs/api/{virtualboxuuidcaptureadapteriddstart.rst => v1virtualboxuuidcaptureadapteriddstart.rst} (72%) rename docs/api/{virtualboxuuidcaptureadapteriddstop.rst => v1virtualboxuuidcaptureadapteriddstop.rst} (53%) rename docs/api/{virtualboxuuidreload.rst => v1virtualboxuuidreload.rst} (52%) rename docs/api/{virtualboxuuidresume.rst => v1virtualboxuuidresume.rst} (53%) create mode 100644 docs/api/v1virtualboxuuidstart.rst create mode 100644 docs/api/v1virtualboxuuidstop.rst rename docs/api/{virtualboxuuidsuspend.rst => v1virtualboxuuidsuspend.rst} (52%) create mode 100644 docs/api/v1virtualboxvms.rst rename docs/api/{vpcs.rst => v1vpcs.rst} (93%) rename docs/api/{vpcsuuid.rst => v1vpcsuuid.rst} (90%) rename docs/api/{vpcsuuidportsportnumberdnio.rst => v1vpcsuuidportsportnumberdnio.rst} (52%) rename docs/api/{vpcsuuidreload.rst => v1vpcsuuidreload.rst} (53%) rename docs/api/{vpcsuuidstart.rst => v1vpcsuuidstart.rst} (53%) rename docs/api/{vpcsuuidstop.rst => v1vpcsuuidstop.rst} (53%) delete mode 100644 docs/api/virtualboxlist.rst delete mode 100644 docs/api/virtualboxuuidcaptureportiddstart.rst delete mode 100644 docs/api/virtualboxuuidcaptureportiddstop.rst delete mode 100644 docs/api/virtualboxuuidportsportiddnio.rst delete mode 100644 docs/api/virtualboxuuidstart.rst delete mode 100644 docs/api/virtualboxuuidstop.rst delete mode 100644 docs/api/virtualboxvms.rst delete mode 100644 docs/api/vpcsuuidportsportiddnio.rst delete mode 100644 docs/api/vpcsuuidportsportidnio.rst diff --git a/docs/api/examples/delete_projectsprojectid.txt b/docs/api/examples/delete_projectsprojectid.txt new file mode 100644 index 00000000..45efff6c --- /dev/null +++ b/docs/api/examples/delete_projectsprojectid.txt @@ -0,0 +1,13 @@ +curl -i -X DELETE 'http://localhost:8000/projects/{project_id}' + +DELETE /projects/{project_id} HTTP/1.1 + + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id} + diff --git a/docs/api/examples/delete_projectuuid.txt b/docs/api/examples/delete_projectuuid.txt deleted file mode 100644 index 9d63e2e0..00000000 --- a/docs/api/examples/delete_projectuuid.txt +++ /dev/null @@ -1,13 +0,0 @@ -curl -i -X DELETE 'http://localhost:8000/project/{uuid}' - -DELETE /project/{uuid} HTTP/1.1 - - - -HTTP/1.1 204 -CONNECTION: keep-alive -CONTENT-LENGTH: 0 -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /project/{uuid} - diff --git a/docs/api/examples/delete_virtualboxuuidadaptersadapteriddnio.txt b/docs/api/examples/delete_virtualboxuuidadaptersadapteriddnio.txt index 2ad343a5..cb1c3b9e 100644 --- a/docs/api/examples/delete_virtualboxuuidadaptersadapteriddnio.txt +++ b/docs/api/examples/delete_virtualboxuuidadaptersadapteriddnio.txt @@ -5,9 +5,9 @@ DELETE /virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio HTTP/1.1 HTTP/1.1 204 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 -X-ROUTE: /virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio diff --git a/docs/api/examples/delete_virtualboxuuidportsportiddnio.txt b/docs/api/examples/delete_virtualboxuuidportsportiddnio.txt deleted file mode 100644 index 6cbca129..00000000 --- a/docs/api/examples/delete_virtualboxuuidportsportiddnio.txt +++ /dev/null @@ -1,13 +0,0 @@ -curl -i -X DELETE 'http://localhost:8000/virtualbox/{uuid}/ports/{port_id:\d+}/nio' - -DELETE /virtualbox/{uuid}/ports/{port_id:\d+}/nio HTTP/1.1 - - - -HTTP/1.1 204 -CONNECTION: keep-alive -CONTENT-LENGTH: 0 -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /virtualbox/{uuid}/ports/{port_id:\d+}/nio - diff --git a/docs/api/examples/delete_vpcsiddportsportidnio.txt b/docs/api/examples/delete_vpcsiddportsportidnio.txt deleted file mode 100644 index 6cb18d74..00000000 --- a/docs/api/examples/delete_vpcsiddportsportidnio.txt +++ /dev/null @@ -1,15 +0,0 @@ -curl -i -xDELETE 'http://localhost:8000/vpcs/{id:\d+}/ports/{port_id}/nio' - -DELETE /vpcs/{id:\d+}/ports/{port_id}/nio HTTP/1.1 - - - -HTTP/1.1 200 -CONNECTION: close -CONTENT-LENGTH: 2 -CONTENT-TYPE: application/json -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 -X-ROUTE: /vpcs/{id:\d+}/ports/{port_id}/nio - -{} diff --git a/docs/api/examples/delete_vpcsuuidportsportiddnio.txt b/docs/api/examples/delete_vpcsuuidportsportiddnio.txt deleted file mode 100644 index b5801424..00000000 --- a/docs/api/examples/delete_vpcsuuidportsportiddnio.txt +++ /dev/null @@ -1,13 +0,0 @@ -curl -i -X DELETE 'http://localhost:8000/vpcs/{uuid}/ports/{port_id:\d+}/nio' - -DELETE /vpcs/{uuid}/ports/{port_id:\d+}/nio HTTP/1.1 - - - -HTTP/1.1 204 -CONNECTION: keep-alive -CONTENT-LENGTH: 0 -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /vpcs/{uuid}/ports/{port_id:\d+}/nio - diff --git a/docs/api/examples/delete_vpcsuuidportsportidnio.txt b/docs/api/examples/delete_vpcsuuidportsportidnio.txt deleted file mode 100644 index 34cfef4d..00000000 --- a/docs/api/examples/delete_vpcsuuidportsportidnio.txt +++ /dev/null @@ -1,13 +0,0 @@ -curl -i -X DELETE 'http://localhost:8000/vpcs/{uuid}/ports/{port_id}/nio' - -DELETE /vpcs/{uuid}/ports/{port_id}/nio HTTP/1.1 - - - -HTTP/1.1 204 -CONNECTION: close -CONTENT-LENGTH: 0 -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 -X-ROUTE: /vpcs/{uuid}/ports/{port_id}/nio - diff --git a/docs/api/examples/delete_vpcsuuidportsportnumberdnio.txt b/docs/api/examples/delete_vpcsuuidportsportnumberdnio.txt index 3a17f8b1..26ac313d 100644 --- a/docs/api/examples/delete_vpcsuuidportsportnumberdnio.txt +++ b/docs/api/examples/delete_vpcsuuidportsportnumberdnio.txt @@ -5,9 +5,9 @@ DELETE /vpcs/{uuid}/ports/{port_number:\d+}/nio HTTP/1.1 HTTP/1.1 204 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 -X-ROUTE: /vpcs/{uuid}/ports/{port_number:\d+}/nio +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/vpcs/{uuid}/ports/{port_number:\d+}/nio diff --git a/docs/api/examples/delete_vpcsvpcsidportsportidnio.txt b/docs/api/examples/delete_vpcsvpcsidportsportidnio.txt deleted file mode 100644 index 37bc3fda..00000000 --- a/docs/api/examples/delete_vpcsvpcsidportsportidnio.txt +++ /dev/null @@ -1,15 +0,0 @@ -curl -i -xDELETE 'http://localhost:8000/vpcs/{vpcs_id}/ports/{port_id}/nio' - -DELETE /vpcs/{vpcs_id}/ports/{port_id}/nio HTTP/1.1 - - - -HTTP/1.1 200 -CONNECTION: close -CONTENT-LENGTH: 2 -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}/ports/{port_id}/nio - -{} diff --git a/docs/api/examples/get_interfaces.txt b/docs/api/examples/get_interfaces.txt index 289c74e0..7ac8c74d 100644 --- a/docs/api/examples/get_interfaces.txt +++ b/docs/api/examples/get_interfaces.txt @@ -5,32 +5,56 @@ GET /interfaces HTTP/1.1 HTTP/1.1 200 -CONNECTION: close -CONTENT-LENGTH: 298 +CONNECTION: keep-alive +CONTENT-LENGTH: 652 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 -X-ROUTE: /interfaces +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/interfaces [ { - "id": "lo", - "name": "lo" + "id": "lo0", + "name": "lo0" }, { - "id": "eth0", - "name": "eth0" + "id": "gif0", + "name": "gif0" }, { - "id": "wlan0", - "name": "wlan0" + "id": "stf0", + "name": "stf0" }, { - "id": "vmnet1", - "name": "vmnet1" + "id": "en0", + "name": "en0" }, { - "id": "vmnet8", - "name": "vmnet8" + "id": "en1", + "name": "en1" + }, + { + "id": "fw0", + "name": "fw0" + }, + { + "id": "en2", + "name": "en2" + }, + { + "id": "p2p0", + "name": "p2p0" + }, + { + "id": "bridge0", + "name": "bridge0" + }, + { + "id": "vboxnet0", + "name": "vboxnet0" + }, + { + "id": "vboxnet1", + "name": "vboxnet1" } ] diff --git a/docs/api/examples/get_projectsprojectid.txt b/docs/api/examples/get_projectsprojectid.txt new file mode 100644 index 00000000..152a815a --- /dev/null +++ b/docs/api/examples/get_projectsprojectid.txt @@ -0,0 +1,19 @@ +curl -i -X GET 'http://localhost:8000/projects/{project_id}' + +GET /projects/{project_id} HTTP/1.1 + + + +HTTP/1.1 200 +CONNECTION: keep-alive +CONTENT-LENGTH: 108 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id} + +{ + "location": "/tmp", + "project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f", + "temporary": false +} diff --git a/docs/api/examples/get_projectuuid.txt b/docs/api/examples/get_projectuuid.txt deleted file mode 100644 index 2388771c..00000000 --- a/docs/api/examples/get_projectuuid.txt +++ /dev/null @@ -1,19 +0,0 @@ -curl -i -X GET 'http://localhost:8000/project/{uuid}' - -GET /project/{uuid} HTTP/1.1 - - - -HTTP/1.1 200 -CONNECTION: keep-alive -CONTENT-LENGTH: 102 -CONTENT-TYPE: application/json -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /project/{uuid} - -{ - "location": "/tmp", - "temporary": false, - "uuid": "00010203-0405-0607-0809-0a0b0c0d0e0f" -} diff --git a/docs/api/examples/get_version.txt b/docs/api/examples/get_version.txt index 59bdb128..88017034 100644 --- a/docs/api/examples/get_version.txt +++ b/docs/api/examples/get_version.txt @@ -5,12 +5,12 @@ GET /version HTTP/1.1 HTTP/1.1 200 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 29 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 -X-ROUTE: /version +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/version { "version": "1.3.dev1" diff --git a/docs/api/examples/get_virtualboxuuid.txt b/docs/api/examples/get_virtualboxuuid.txt index 3ed0b071..11853488 100644 --- a/docs/api/examples/get_virtualboxuuid.txt +++ b/docs/api/examples/get_virtualboxuuid.txt @@ -5,12 +5,12 @@ GET /virtualbox/{uuid} HTTP/1.1 HTTP/1.1 200 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 346 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 -X-ROUTE: /virtualbox/{uuid} +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/virtualbox/{uuid} { "adapter_start_index": 0, @@ -21,6 +21,6 @@ X-ROUTE: /virtualbox/{uuid} "headless": false, "name": "VMTEST", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "uuid": "f246f72b-9f79-4a60-a8fb-0b2375d29d28", + "uuid": "aa7a7c1e-6fc2-43b8-b53b-60e605565625", "vmname": "VMTEST" } diff --git a/docs/api/examples/get_vpcsuuid.txt b/docs/api/examples/get_vpcsuuid.txt index 2d191100..19f913bc 100644 --- a/docs/api/examples/get_vpcsuuid.txt +++ b/docs/api/examples/get_vpcsuuid.txt @@ -5,12 +5,12 @@ GET /vpcs/{uuid} HTTP/1.1 HTTP/1.1 200 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 211 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 -X-ROUTE: /vpcs/{uuid} +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/vpcs/{uuid} { "console": 2003, @@ -18,5 +18,5 @@ X-ROUTE: /vpcs/{uuid} "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, "startup_script": null, - "uuid": "0300d8a7-e971-4402-bd85-8c12384d308d" + "uuid": "c505f88b-7fe1-4985-9aca-a3798f6659ab" } diff --git a/docs/api/examples/post_portsudp.txt b/docs/api/examples/post_portsudp.txt index ff13ecb6..3be4b74c 100644 --- a/docs/api/examples/post_portsudp.txt +++ b/docs/api/examples/post_portsudp.txt @@ -5,12 +5,12 @@ POST /ports/udp HTTP/1.1 HTTP/1.1 201 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 25 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 -X-ROUTE: /ports/udp +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/ports/udp { "udp_port": 10000 diff --git a/docs/api/examples/post_project.txt b/docs/api/examples/post_project.txt deleted file mode 100644 index 4f40b3a4..00000000 --- a/docs/api/examples/post_project.txt +++ /dev/null @@ -1,22 +0,0 @@ -curl -i -X POST 'http://localhost:8000/project' -d '{"location": "/tmp", "uuid": "00010203-0405-0607-0809-0a0b0c0d0e0f"}' - -POST /project HTTP/1.1 -{ - "location": "/tmp", - "uuid": "00010203-0405-0607-0809-0a0b0c0d0e0f" -} - - -HTTP/1.1 200 -CONNECTION: close -CONTENT-LENGTH: 102 -CONTENT-TYPE: application/json -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 -X-ROUTE: /project - -{ - "location": "/tmp", - "temporary": false, - "uuid": "00010203-0405-0607-0809-0a0b0c0d0e0f" -} diff --git a/docs/api/examples/post_projectsprojectidclose.txt b/docs/api/examples/post_projectsprojectidclose.txt new file mode 100644 index 00000000..bcc429c9 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidclose.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/close' -d '{}' + +POST /projects/{project_id}/close HTTP/1.1 +{} + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/close + diff --git a/docs/api/examples/post_projectsprojectidcommit.txt b/docs/api/examples/post_projectsprojectidcommit.txt new file mode 100644 index 00000000..0b36f05d --- /dev/null +++ b/docs/api/examples/post_projectsprojectidcommit.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/commit' -d '{}' + +POST /projects/{project_id}/commit HTTP/1.1 +{} + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/commit + diff --git a/docs/api/examples/post_projectuuidclose.txt b/docs/api/examples/post_projectuuidclose.txt deleted file mode 100644 index 03ae88bc..00000000 --- a/docs/api/examples/post_projectuuidclose.txt +++ /dev/null @@ -1,13 +0,0 @@ -curl -i -X POST 'http://localhost:8000/project/{uuid}/close' -d '{}' - -POST /project/{uuid}/close HTTP/1.1 -{} - - -HTTP/1.1 204 -CONNECTION: keep-alive -CONTENT-LENGTH: 0 -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /project/{uuid}/close - diff --git a/docs/api/examples/post_projectuuidcommit.txt b/docs/api/examples/post_projectuuidcommit.txt deleted file mode 100644 index 2fe9c38f..00000000 --- a/docs/api/examples/post_projectuuidcommit.txt +++ /dev/null @@ -1,13 +0,0 @@ -curl -i -X POST 'http://localhost:8000/project/{uuid}/commit' -d '{}' - -POST /project/{uuid}/commit HTTP/1.1 -{} - - -HTTP/1.1 204 -CONNECTION: keep-alive -CONTENT-LENGTH: 0 -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /project/{uuid}/commit - diff --git a/docs/api/examples/post_udp.txt b/docs/api/examples/post_udp.txt deleted file mode 100644 index 7dfd4d75..00000000 --- a/docs/api/examples/post_udp.txt +++ /dev/null @@ -1,17 +0,0 @@ -curl -i -X POST 'http://localhost:8000/udp' -d '{}' - -POST /udp HTTP/1.1 -{} - - -HTTP/1.1 201 -CONNECTION: keep-alive -CONTENT-LENGTH: 25 -CONTENT-TYPE: application/json -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /udp - -{ - "udp_port": 10000 -} diff --git a/docs/api/examples/post_version.txt b/docs/api/examples/post_version.txt index 45ef1069..2f6c1452 100644 --- a/docs/api/examples/post_version.txt +++ b/docs/api/examples/post_version.txt @@ -7,12 +7,12 @@ POST /version HTTP/1.1 HTTP/1.1 200 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 29 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 -X-ROUTE: /version +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/version { "version": "1.3.dev1" diff --git a/docs/api/examples/post_virtualbox.txt b/docs/api/examples/post_virtualbox.txt index 4ed3b787..06dec3c8 100644 --- a/docs/api/examples/post_virtualbox.txt +++ b/docs/api/examples/post_virtualbox.txt @@ -10,12 +10,12 @@ POST /virtualbox HTTP/1.1 HTTP/1.1 201 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 340 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 -X-ROUTE: /virtualbox +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/virtualbox { "adapter_start_index": 0, @@ -26,6 +26,6 @@ X-ROUTE: /virtualbox "headless": false, "name": "VM1", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "uuid": "210fbbba-4025-4286-81dc-1f07cc494cc9", + "uuid": "267f3908-f3dd-4cac-bca3-2e31a1018493", "vmname": "VM1" } diff --git a/docs/api/examples/post_virtualboxuuidadaptersadapteriddnio.txt b/docs/api/examples/post_virtualboxuuidadaptersadapteriddnio.txt index 04674eef..527a758e 100644 --- a/docs/api/examples/post_virtualboxuuidadaptersadapteriddnio.txt +++ b/docs/api/examples/post_virtualboxuuidadaptersadapteriddnio.txt @@ -10,12 +10,12 @@ POST /virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio HTTP/1.1 HTTP/1.1 201 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 89 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 -X-ROUTE: /virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio { "lport": 4242, diff --git a/docs/api/examples/post_virtualboxuuidportsportiddnio.txt b/docs/api/examples/post_virtualboxuuidportsportiddnio.txt deleted file mode 100644 index d754de40..00000000 --- a/docs/api/examples/post_virtualboxuuidportsportiddnio.txt +++ /dev/null @@ -1,25 +0,0 @@ -curl -i -X POST 'http://localhost:8000/virtualbox/{uuid}/ports/{port_id:\d+}/nio' -d '{"lport": 4242, "rhost": "127.0.0.1", "rport": 4343, "type": "nio_udp"}' - -POST /virtualbox/{uuid}/ports/{port_id:\d+}/nio HTTP/1.1 -{ - "lport": 4242, - "rhost": "127.0.0.1", - "rport": 4343, - "type": "nio_udp" -} - - -HTTP/1.1 201 -CONNECTION: keep-alive -CONTENT-LENGTH: 89 -CONTENT-TYPE: application/json -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /virtualbox/{uuid}/ports/{port_id:\d+}/nio - -{ - "lport": 4242, - "rhost": "127.0.0.1", - "rport": 4343, - "type": "nio_udp" -} diff --git a/docs/api/examples/post_virtualboxuuidstart.txt b/docs/api/examples/post_virtualboxuuidstart.txt deleted file mode 100644 index 8f567c27..00000000 --- a/docs/api/examples/post_virtualboxuuidstart.txt +++ /dev/null @@ -1,13 +0,0 @@ -curl -i -X POST 'http://localhost:8000/virtualbox/{uuid}/start' -d '{}' - -POST /virtualbox/{uuid}/start HTTP/1.1 -{} - - -HTTP/1.1 204 -CONNECTION: close -CONTENT-LENGTH: 0 -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 -X-ROUTE: /virtualbox/{uuid}/start - diff --git a/docs/api/examples/post_virtualboxuuidstop.txt b/docs/api/examples/post_virtualboxuuidstop.txt deleted file mode 100644 index 9c4982a7..00000000 --- a/docs/api/examples/post_virtualboxuuidstop.txt +++ /dev/null @@ -1,13 +0,0 @@ -curl -i -X POST 'http://localhost:8000/virtualbox/{uuid}/stop' -d '{}' - -POST /virtualbox/{uuid}/stop HTTP/1.1 -{} - - -HTTP/1.1 204 -CONNECTION: close -CONTENT-LENGTH: 0 -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 -X-ROUTE: /virtualbox/{uuid}/stop - diff --git a/docs/api/examples/post_vpcs.txt b/docs/api/examples/post_vpcs.txt index c2ea6413..c9ff1f35 100644 --- a/docs/api/examples/post_vpcs.txt +++ b/docs/api/examples/post_vpcs.txt @@ -8,12 +8,12 @@ POST /vpcs HTTP/1.1 HTTP/1.1 201 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 211 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 -X-ROUTE: /vpcs +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/vpcs { "console": 2001, @@ -21,5 +21,5 @@ X-ROUTE: /vpcs "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "script_file": null, "startup_script": null, - "uuid": "92ff89ed-aed2-487c-b893-5559ca258d0f" + "uuid": "d7a9ef38-2863-4cf2-97ce-dcf8416a93ae" } diff --git a/docs/api/examples/post_vpcsiddportsportidnio.txt b/docs/api/examples/post_vpcsiddportsportidnio.txt deleted file mode 100644 index 771255e0..00000000 --- a/docs/api/examples/post_vpcsiddportsportidnio.txt +++ /dev/null @@ -1,25 +0,0 @@ -curl -i -xPOST 'http://localhost:8000/vpcs/{id:\d+}/ports/{port_id}/nio' -d '{"lport": 4242, "rhost": "127.0.0.1", "rport": 4343, "type": "nio_udp"}' - -POST /vpcs/{id:\d+}/ports/{port_id}/nio HTTP/1.1 -{ - "lport": 4242, - "rhost": "127.0.0.1", - "rport": 4343, - "type": "nio_udp" -} - - -HTTP/1.1 200 -CONNECTION: close -CONTENT-LENGTH: 89 -CONTENT-TYPE: application/json -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 -X-ROUTE: /vpcs/{id:\d+}/ports/{port_id}/nio - -{ - "lport": 4242, - "rhost": "127.0.0.1", - "rport": 4343, - "type": "nio_udp" -} diff --git a/docs/api/examples/post_vpcsuuidportsportiddnio.txt b/docs/api/examples/post_vpcsuuidportsportiddnio.txt deleted file mode 100644 index 057c1385..00000000 --- a/docs/api/examples/post_vpcsuuidportsportiddnio.txt +++ /dev/null @@ -1,25 +0,0 @@ -curl -i -X POST 'http://localhost:8000/vpcs/{uuid}/ports/{port_id:\d+}/nio' -d '{"lport": 4242, "rhost": "127.0.0.1", "rport": 4343, "type": "nio_udp"}' - -POST /vpcs/{uuid}/ports/{port_id:\d+}/nio HTTP/1.1 -{ - "lport": 4242, - "rhost": "127.0.0.1", - "rport": 4343, - "type": "nio_udp" -} - - -HTTP/1.1 201 -CONNECTION: keep-alive -CONTENT-LENGTH: 89 -CONTENT-TYPE: application/json -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /vpcs/{uuid}/ports/{port_id:\d+}/nio - -{ - "lport": 4242, - "rhost": "127.0.0.1", - "rport": 4343, - "type": "nio_udp" -} diff --git a/docs/api/examples/post_vpcsuuidportsportidnio.txt b/docs/api/examples/post_vpcsuuidportsportidnio.txt deleted file mode 100644 index da7e6fd9..00000000 --- a/docs/api/examples/post_vpcsuuidportsportidnio.txt +++ /dev/null @@ -1,25 +0,0 @@ -curl -i -X POST 'http://localhost:8000/vpcs/{uuid}/ports/{port_id}/nio' -d '{"lport": 4242, "rhost": "127.0.0.1", "rport": 4343, "type": "nio_udp"}' - -POST /vpcs/{uuid}/ports/{port_id}/nio HTTP/1.1 -{ - "lport": 4242, - "rhost": "127.0.0.1", - "rport": 4343, - "type": "nio_udp" -} - - -HTTP/1.1 201 -CONNECTION: close -CONTENT-LENGTH: 89 -CONTENT-TYPE: application/json -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 -X-ROUTE: /vpcs/{uuid}/ports/{port_id}/nio - -{ - "lport": 4242, - "rhost": "127.0.0.1", - "rport": 4343, - "type": "nio_udp" -} diff --git a/docs/api/examples/post_vpcsuuidportsportnumberdnio.txt b/docs/api/examples/post_vpcsuuidportsportnumberdnio.txt index 82abf86c..83bf344c 100644 --- a/docs/api/examples/post_vpcsuuidportsportnumberdnio.txt +++ b/docs/api/examples/post_vpcsuuidportsportnumberdnio.txt @@ -10,12 +10,12 @@ POST /vpcs/{uuid}/ports/{port_number:\d+}/nio HTTP/1.1 HTTP/1.1 201 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 89 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 -X-ROUTE: /vpcs/{uuid}/ports/{port_number:\d+}/nio +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/vpcs/{uuid}/ports/{port_number:\d+}/nio { "lport": 4242, diff --git a/docs/api/examples/post_vpcsvpcsidnio.txt b/docs/api/examples/post_vpcsvpcsidnio.txt deleted file mode 100644 index 84a739d6..00000000 --- a/docs/api/examples/post_vpcsvpcsidnio.txt +++ /dev/null @@ -1,28 +0,0 @@ -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/examples/post_vpcsvpcsidportsportidnio.txt b/docs/api/examples/post_vpcsvpcsidportsportidnio.txt deleted file mode 100644 index 06fbc0fb..00000000 --- a/docs/api/examples/post_vpcsvpcsidportsportidnio.txt +++ /dev/null @@ -1,25 +0,0 @@ -curl -i -xPOST 'http://localhost:8000/vpcs/{vpcs_id}/ports/{port_id}/nio' -d '{"lport": 4242, "rhost": "127.0.0.1", "rport": 4343, "type": "nio_udp"}' - -POST /vpcs/{vpcs_id}/ports/{port_id}/nio HTTP/1.1 -{ - "lport": 4242, - "rhost": "127.0.0.1", - "rport": 4343, - "type": "nio_udp" -} - - -HTTP/1.1 200 -CONNECTION: close -CONTENT-LENGTH: 89 -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}/ports/{port_id}/nio - -{ - "lport": 4242, - "rhost": "127.0.0.1", - "rport": 4343, - "type": "nio_udp" -} diff --git a/docs/api/examples/put_projectsprojectid.txt b/docs/api/examples/put_projectsprojectid.txt new file mode 100644 index 00000000..e0366b62 --- /dev/null +++ b/docs/api/examples/put_projectsprojectid.txt @@ -0,0 +1,21 @@ +curl -i -X PUT 'http://localhost:8000/projects/{project_id}' -d '{"temporary": false}' + +PUT /projects/{project_id} HTTP/1.1 +{ + "temporary": false +} + + +HTTP/1.1 200 +CONNECTION: keep-alive +CONTENT-LENGTH: 164 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id} + +{ + "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmptf8_s67s", + "project_id": "12d03846-c355-4da9-b708-cd45e5d30a50", + "temporary": false +} diff --git a/docs/api/examples/put_projectuuid.txt b/docs/api/examples/put_projectuuid.txt deleted file mode 100644 index 91147567..00000000 --- a/docs/api/examples/put_projectuuid.txt +++ /dev/null @@ -1,21 +0,0 @@ -curl -i -X PUT 'http://localhost:8000/project/{uuid}' -d '{"temporary": false}' - -PUT /project/{uuid} HTTP/1.1 -{ - "temporary": false -} - - -HTTP/1.1 200 -CONNECTION: keep-alive -CONTENT-LENGTH: 158 -CONTENT-TYPE: application/json -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /project/{uuid} - -{ - "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmphb8dmyls", - "temporary": false, - "uuid": "1aa054dd-e672-4961-90c3-fef730fc6301" -} diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst deleted file mode 100644 index 946e4c15..00000000 --- a/docs/api/interfaces.rst +++ /dev/null @@ -1,19 +0,0 @@ -/interfaces ---------------------------------------------- - -.. contents:: - -GET /interfaces -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -List all the network interfaces available on the server - -Response status codes -********************** -- **200**: OK - -Sample session -*************** - - -.. literalinclude:: examples/get_interfaces.txt - diff --git a/docs/api/portsudp.rst b/docs/api/portsudp.rst deleted file mode 100644 index a2ecdae7..00000000 --- a/docs/api/portsudp.rst +++ /dev/null @@ -1,19 +0,0 @@ -/ports/udp ---------------------------------------------- - -.. contents:: - -POST /ports/udp -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Allocate an UDP port on the server - -Response status codes -********************** -- **201**: UDP port allocated - -Sample session -*************** - - -.. literalinclude:: examples/post_portsudp.txt - diff --git a/docs/api/projectuuidclose.rst b/docs/api/projectuuidclose.rst deleted file mode 100644 index dffca28c..00000000 --- a/docs/api/projectuuidclose.rst +++ /dev/null @@ -1,24 +0,0 @@ -/project/{uuid}/close ---------------------------------------------- - -.. contents:: - -POST /project/**{uuid}**/close -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Close project - -Parameters -********** -- **uuid**: Project instance UUID - -Response status codes -********************** -- **404**: Project instance doesn't exist -- **204**: Project closed - -Sample session -*************** - - -.. literalinclude:: examples/post_projectuuidclose.txt - diff --git a/docs/api/projectuuidcommit.rst b/docs/api/projectuuidcommit.rst deleted file mode 100644 index 6bbd35f1..00000000 --- a/docs/api/projectuuidcommit.rst +++ /dev/null @@ -1,24 +0,0 @@ -/project/{uuid}/commit ---------------------------------------------- - -.. contents:: - -POST /project/**{uuid}**/commit -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Write changes on disk - -Parameters -********** -- **uuid**: Project instance UUID - -Response status codes -********************** -- **404**: Project instance doesn't exist -- **204**: Changes write on disk - -Sample session -*************** - - -.. literalinclude:: examples/post_projectuuidcommit.txt - diff --git a/docs/api/udp.rst b/docs/api/udp.rst deleted file mode 100644 index 6ed3ac5d..00000000 --- a/docs/api/udp.rst +++ /dev/null @@ -1,19 +0,0 @@ -/udp ---------------------------------------------- - -.. contents:: - -POST /udp -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Allocate an UDP port on the server - -Response status codes -********************** -- **201**: UDP port allocated - -Sample session -*************** - - -.. literalinclude:: examples/post_udp.txt - diff --git a/docs/api/v1interfaces.rst b/docs/api/v1interfaces.rst new file mode 100644 index 00000000..fc32b2ee --- /dev/null +++ b/docs/api/v1interfaces.rst @@ -0,0 +1,13 @@ +/v1/interfaces +----------------------------------------------------------- + +.. contents:: + +GET /v1/interfaces +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +List all the network interfaces available on the server + +Response status codes +********************** +- **200**: OK + diff --git a/docs/api/v1portsudp.rst b/docs/api/v1portsudp.rst new file mode 100644 index 00000000..3451314a --- /dev/null +++ b/docs/api/v1portsudp.rst @@ -0,0 +1,13 @@ +/v1/ports/udp +----------------------------------------------------------- + +.. contents:: + +POST /v1/ports/udp +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Allocate an UDP port on the server + +Response status codes +********************** +- **201**: UDP port allocated + diff --git a/docs/api/project.rst b/docs/api/v1projects.rst similarity index 69% rename from docs/api/project.rst rename to docs/api/v1projects.rst index 8edefbbe..303c52a9 100644 --- a/docs/api/project.rst +++ b/docs/api/v1projects.rst @@ -1,11 +1,11 @@ -/project ---------------------------------------------- +/v1/projects +----------------------------------------------------------- .. contents:: -POST /project -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Create a project on the server +POST /v1/projects +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Create a new project on the server Response status codes ********************** @@ -18,8 +18,8 @@ Input + -
Name Mandatory Type Description
location string Base directory where the project should be created on remote server
project_id ['string', 'null'] Project UUID
temporary boolean If project is a temporary project
uuid ['string', 'null'] Project UUID
Output @@ -29,13 +29,7 @@ Output + -
Name Mandatory Type Description
location string Base directory where the project should be created on remote server
project_id string Project UUID
temporary boolean If project is a temporary project
uuid string Project UUID
-Sample session -*************** - - -.. literalinclude:: examples/post_project.txt - diff --git a/docs/api/projectuuid.rst b/docs/api/v1projectsprojectid.rst similarity index 62% rename from docs/api/projectuuid.rst rename to docs/api/v1projectsprojectid.rst index fd846a10..0b872074 100644 --- a/docs/api/projectuuid.rst +++ b/docs/api/v1projectsprojectid.rst @@ -1,20 +1,20 @@ -/project/{uuid} ---------------------------------------------- +/v1/projects/{project_id} +----------------------------------------------------------- .. contents:: -GET /project/**{uuid}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +GET /v1/projects/**{project_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Get project information Parameters ********** -- **uuid**: Project instance UUID +- **project_id**: The UUID of the project Response status codes ********************** -- **200**: OK -- **404**: Project instance doesn't exist +- **200**: Success +- **404**: The project doesn't exist Output ******* @@ -23,29 +23,23 @@ Output + -
Name Mandatory Type Description
location string Base directory where the project should be created on remote server
project_id string Project UUID
temporary boolean If project is a temporary project
uuid string Project UUID
-Sample session -*************** - -.. literalinclude:: examples/get_projectuuid.txt - - -PUT /project/**{uuid}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +PUT /v1/projects/**{project_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Update a project Parameters ********** -- **uuid**: Project instance UUID +- **project_id**: The UUID of the project Response status codes ********************** -- **200**: Project updated -- **404**: Project instance doesn't exist +- **200**: The project has been updated +- **404**: The project doesn't exist Input ******* @@ -63,33 +57,21 @@ Output + -
Name Mandatory Type Description
location string Base directory where the project should be created on remote server
project_id string Project UUID
temporary boolean If project is a temporary project
uuid string Project UUID
-Sample session -*************** - - -.. literalinclude:: examples/put_projectuuid.txt - -DELETE /project/**{uuid}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +DELETE /v1/projects/**{project_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Delete a project from disk Parameters ********** -- **uuid**: Project instance UUID +- **project_id**: The UUID of the project Response status codes ********************** -- **404**: Project instance doesn't exist -- **204**: Changes write on disk - -Sample session -*************** - - -.. literalinclude:: examples/delete_projectuuid.txt +- **404**: The project doesn't exist +- **204**: Changes have been written on disk diff --git a/docs/api/v1projectsprojectidclose.rst b/docs/api/v1projectsprojectidclose.rst new file mode 100644 index 00000000..e9b4feb8 --- /dev/null +++ b/docs/api/v1projectsprojectidclose.rst @@ -0,0 +1,18 @@ +/v1/projects/{project_id}/close +----------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/close +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Close a project + +Parameters +********** +- **project_id**: The UUID of the project + +Response status codes +********************** +- **404**: The project doesn't exist +- **204**: The project has been closed + diff --git a/docs/api/v1projectsprojectidcommit.rst b/docs/api/v1projectsprojectidcommit.rst new file mode 100644 index 00000000..4909e569 --- /dev/null +++ b/docs/api/v1projectsprojectidcommit.rst @@ -0,0 +1,18 @@ +/v1/projects/{project_id}/commit +----------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/commit +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Write changes on disk + +Parameters +********** +- **project_id**: The UUID of the project + +Response status codes +********************** +- **404**: The project doesn't exist +- **204**: Changes have been written on disk + diff --git a/docs/api/version.rst b/docs/api/v1version.rst similarity index 80% rename from docs/api/version.rst rename to docs/api/v1version.rst index f98b52f5..e0946a33 100644 --- a/docs/api/version.rst +++ b/docs/api/v1version.rst @@ -1,10 +1,10 @@ -/version ---------------------------------------------- +/v1/version +----------------------------------------------------------- .. contents:: -GET /version -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +GET /v1/version +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Retrieve the server version number Response status codes @@ -20,15 +20,9 @@ Output version ✔ string Version number human readable -Sample session -*************** - -.. literalinclude:: examples/get_version.txt - - -POST /version -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +POST /v1/version +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Check if version is the same as the server Response status codes @@ -54,9 +48,3 @@ Output version ✔ string Version number human readable -Sample session -*************** - - -.. literalinclude:: examples/post_version.txt - diff --git a/docs/api/virtualbox.rst b/docs/api/v1virtualbox.rst similarity index 95% rename from docs/api/virtualbox.rst rename to docs/api/v1virtualbox.rst index d85ef101..754badb1 100644 --- a/docs/api/virtualbox.rst +++ b/docs/api/v1virtualbox.rst @@ -1,10 +1,10 @@ -/virtualbox ---------------------------------------------- +/v1/virtualbox +----------------------------------------------------------- .. contents:: -POST /virtualbox -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +POST /v1/virtualbox +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Create a new VirtualBox VM instance Response status codes @@ -51,9 +51,3 @@ Output vmname string VirtualBox VM name (in VirtualBox itself) -Sample session -*************** - - -.. literalinclude:: examples/post_virtualbox.txt - diff --git a/docs/api/virtualboxuuid.rst b/docs/api/v1virtualboxuuid.rst similarity index 93% rename from docs/api/virtualboxuuid.rst rename to docs/api/v1virtualboxuuid.rst index c9dfaa58..90b30f7c 100644 --- a/docs/api/virtualboxuuid.rst +++ b/docs/api/v1virtualboxuuid.rst @@ -1,10 +1,10 @@ -/virtualbox/{uuid} ---------------------------------------------- +/v1/virtualbox/{uuid} +----------------------------------------------------------- .. contents:: -GET /virtualbox/**{uuid}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +GET /v1/virtualbox/**{uuid}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Get a VirtualBox VM instance Parameters @@ -34,15 +34,9 @@ Output vmname string VirtualBox VM name (in VirtualBox itself) -Sample session -*************** - -.. literalinclude:: examples/get_virtualboxuuid.txt - - -PUT /virtualbox/**{uuid}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +PUT /v1/virtualbox/**{uuid}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Update a VirtualBox VM instance Parameters @@ -90,8 +84,8 @@ Output -DELETE /virtualbox/**{uuid}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +DELETE /v1/virtualbox/**{uuid}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Delete a VirtualBox VM instance Parameters diff --git a/docs/api/virtualboxuuidadaptersadapteriddnio.rst b/docs/api/v1virtualboxuuidadaptersadapteriddnio.rst similarity index 51% rename from docs/api/virtualboxuuidadaptersadapteriddnio.rst rename to docs/api/v1virtualboxuuidadaptersadapteriddnio.rst index 843fcd0b..dc6649f7 100644 --- a/docs/api/virtualboxuuidadaptersadapteriddnio.rst +++ b/docs/api/v1virtualboxuuidadaptersadapteriddnio.rst @@ -1,10 +1,10 @@ -/virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio ---------------------------------------------- +/v1/virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio +----------------------------------------------------------- .. contents:: -POST /virtualbox/**{uuid}**/adapters/**{adapter_id:\d+}**/nio -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +POST /v1/virtualbox/**{uuid}**/adapters/**{adapter_id:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Add a NIO to a VirtualBox VM instance Parameters @@ -18,15 +18,9 @@ Response status codes - **201**: NIO created - **404**: Instance doesn't exist -Sample session -*************** - -.. literalinclude:: examples/post_virtualboxuuidadaptersadapteriddnio.txt - - -DELETE /virtualbox/**{uuid}**/adapters/**{adapter_id:\d+}**/nio -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +DELETE /v1/virtualbox/**{uuid}**/adapters/**{adapter_id:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Remove a NIO from a VirtualBox VM instance Parameters @@ -40,9 +34,3 @@ Response status codes - **404**: Instance doesn't exist - **204**: NIO deleted -Sample session -*************** - - -.. literalinclude:: examples/delete_virtualboxuuidadaptersadapteriddnio.txt - diff --git a/docs/api/virtualboxuuidcaptureadapteriddstart.rst b/docs/api/v1virtualboxuuidcaptureadapteriddstart.rst similarity index 72% rename from docs/api/virtualboxuuidcaptureadapteriddstart.rst rename to docs/api/v1virtualboxuuidcaptureadapteriddstart.rst index d7db9429..db6f0f61 100644 --- a/docs/api/virtualboxuuidcaptureadapteriddstart.rst +++ b/docs/api/v1virtualboxuuidcaptureadapteriddstart.rst @@ -1,10 +1,10 @@ -/virtualbox/{uuid}/capture/{adapter_id:\d+}/start ---------------------------------------------- +/v1/virtualbox/{uuid}/capture/{adapter_id:\d+}/start +----------------------------------------------------------- .. contents:: -POST /virtualbox/**{uuid}**/capture/**{adapter_id:\d+}**/start -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +POST /v1/virtualbox/**{uuid}**/capture/**{adapter_id:\d+}**/start +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Start a packet capture on a VirtualBox VM instance Parameters diff --git a/docs/api/virtualboxuuidcaptureadapteriddstop.rst b/docs/api/v1virtualboxuuidcaptureadapteriddstop.rst similarity index 53% rename from docs/api/virtualboxuuidcaptureadapteriddstop.rst rename to docs/api/v1virtualboxuuidcaptureadapteriddstop.rst index c4067108..eacc8212 100644 --- a/docs/api/virtualboxuuidcaptureadapteriddstop.rst +++ b/docs/api/v1virtualboxuuidcaptureadapteriddstop.rst @@ -1,10 +1,10 @@ -/virtualbox/{uuid}/capture/{adapter_id:\d+}/stop ---------------------------------------------- +/v1/virtualbox/{uuid}/capture/{adapter_id:\d+}/stop +----------------------------------------------------------- .. contents:: -POST /virtualbox/**{uuid}**/capture/**{adapter_id:\d+}**/stop -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +POST /v1/virtualbox/**{uuid}**/capture/**{adapter_id:\d+}**/stop +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Stop a packet capture on a VirtualBox VM instance Parameters diff --git a/docs/api/virtualboxuuidreload.rst b/docs/api/v1virtualboxuuidreload.rst similarity index 52% rename from docs/api/virtualboxuuidreload.rst rename to docs/api/v1virtualboxuuidreload.rst index 9a56133f..3ad6c3b7 100644 --- a/docs/api/virtualboxuuidreload.rst +++ b/docs/api/v1virtualboxuuidreload.rst @@ -1,10 +1,10 @@ -/virtualbox/{uuid}/reload ---------------------------------------------- +/v1/virtualbox/{uuid}/reload +----------------------------------------------------------- .. contents:: -POST /virtualbox/**{uuid}**/reload -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +POST /v1/virtualbox/**{uuid}**/reload +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Reload a VirtualBox VM instance Parameters diff --git a/docs/api/virtualboxuuidresume.rst b/docs/api/v1virtualboxuuidresume.rst similarity index 53% rename from docs/api/virtualboxuuidresume.rst rename to docs/api/v1virtualboxuuidresume.rst index dc8e616d..97bfdbf8 100644 --- a/docs/api/virtualboxuuidresume.rst +++ b/docs/api/v1virtualboxuuidresume.rst @@ -1,10 +1,10 @@ -/virtualbox/{uuid}/resume ---------------------------------------------- +/v1/virtualbox/{uuid}/resume +----------------------------------------------------------- .. contents:: -POST /virtualbox/**{uuid}**/resume -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +POST /v1/virtualbox/**{uuid}**/resume +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Resume a suspended VirtualBox VM instance Parameters diff --git a/docs/api/v1virtualboxuuidstart.rst b/docs/api/v1virtualboxuuidstart.rst new file mode 100644 index 00000000..178ed7d4 --- /dev/null +++ b/docs/api/v1virtualboxuuidstart.rst @@ -0,0 +1,19 @@ +/v1/virtualbox/{uuid}/start +----------------------------------------------------------- + +.. contents:: + +POST /v1/virtualbox/**{uuid}**/start +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Start a VirtualBox VM instance + +Parameters +********** +- **uuid**: Instance UUID + +Response status codes +********************** +- **400**: Invalid instance UUID +- **404**: Instance doesn't exist +- **204**: Instance started + diff --git a/docs/api/v1virtualboxuuidstop.rst b/docs/api/v1virtualboxuuidstop.rst new file mode 100644 index 00000000..8cbe9441 --- /dev/null +++ b/docs/api/v1virtualboxuuidstop.rst @@ -0,0 +1,19 @@ +/v1/virtualbox/{uuid}/stop +----------------------------------------------------------- + +.. contents:: + +POST /v1/virtualbox/**{uuid}**/stop +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Stop a VirtualBox VM instance + +Parameters +********** +- **uuid**: Instance UUID + +Response status codes +********************** +- **400**: Invalid instance UUID +- **404**: Instance doesn't exist +- **204**: Instance stopped + diff --git a/docs/api/virtualboxuuidsuspend.rst b/docs/api/v1virtualboxuuidsuspend.rst similarity index 52% rename from docs/api/virtualboxuuidsuspend.rst rename to docs/api/v1virtualboxuuidsuspend.rst index 90512c7a..3c14bc16 100644 --- a/docs/api/virtualboxuuidsuspend.rst +++ b/docs/api/v1virtualboxuuidsuspend.rst @@ -1,10 +1,10 @@ -/virtualbox/{uuid}/suspend ---------------------------------------------- +/v1/virtualbox/{uuid}/suspend +----------------------------------------------------------- .. contents:: -POST /virtualbox/**{uuid}**/suspend -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +POST /v1/virtualbox/**{uuid}**/suspend +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Suspend a VirtualBox VM instance Parameters diff --git a/docs/api/v1virtualboxvms.rst b/docs/api/v1virtualboxvms.rst new file mode 100644 index 00000000..18a82b95 --- /dev/null +++ b/docs/api/v1virtualboxvms.rst @@ -0,0 +1,13 @@ +/v1/virtualbox/vms +----------------------------------------------------------- + +.. contents:: + +GET /v1/virtualbox/vms +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Get all VirtualBox VMs available + +Response status codes +********************** +- **200**: Success + diff --git a/docs/api/vpcs.rst b/docs/api/v1vpcs.rst similarity index 93% rename from docs/api/vpcs.rst rename to docs/api/v1vpcs.rst index 36c55b71..3e26b9ce 100644 --- a/docs/api/vpcs.rst +++ b/docs/api/v1vpcs.rst @@ -1,10 +1,10 @@ -/vpcs ---------------------------------------------- +/v1/vpcs +----------------------------------------------------------- .. contents:: -POST /vpcs -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +POST /v1/vpcs +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Create a new VPCS instance Response status codes @@ -41,9 +41,3 @@ Output uuid ✔ string VPCS device UUID -Sample session -*************** - - -.. literalinclude:: examples/post_vpcs.txt - diff --git a/docs/api/vpcsuuid.rst b/docs/api/v1vpcsuuid.rst similarity index 90% rename from docs/api/vpcsuuid.rst rename to docs/api/v1vpcsuuid.rst index 67c2883a..0bc9e4e5 100644 --- a/docs/api/vpcsuuid.rst +++ b/docs/api/v1vpcsuuid.rst @@ -1,10 +1,10 @@ -/vpcs/{uuid} ---------------------------------------------- +/v1/vpcs/{uuid} +----------------------------------------------------------- .. contents:: -GET /vpcs/**{uuid}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +GET /v1/vpcs/**{uuid}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Get a VPCS instance Parameters @@ -30,15 +30,9 @@ Output uuid ✔ string VPCS device UUID -Sample session -*************** - -.. literalinclude:: examples/get_vpcsuuid.txt - - -PUT /vpcs/**{uuid}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +PUT /v1/vpcs/**{uuid}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Update a VPCS instance Parameters @@ -77,8 +71,8 @@ Output -DELETE /vpcs/**{uuid}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +DELETE /v1/vpcs/**{uuid}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Delete a VPCS instance Parameters diff --git a/docs/api/vpcsuuidportsportnumberdnio.rst b/docs/api/v1vpcsuuidportsportnumberdnio.rst similarity index 52% rename from docs/api/vpcsuuidportsportnumberdnio.rst rename to docs/api/v1vpcsuuidportsportnumberdnio.rst index e16cd3f9..72a5b999 100644 --- a/docs/api/vpcsuuidportsportnumberdnio.rst +++ b/docs/api/v1vpcsuuidportsportnumberdnio.rst @@ -1,10 +1,10 @@ -/vpcs/{uuid}/ports/{port_number:\d+}/nio ---------------------------------------------- +/v1/vpcs/{uuid}/ports/{port_number:\d+}/nio +----------------------------------------------------------- .. contents:: -POST /vpcs/**{uuid}**/ports/**{port_number:\d+}**/nio -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +POST /v1/vpcs/**{uuid}**/ports/**{port_number:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Add a NIO to a VPCS instance Parameters @@ -18,15 +18,9 @@ Response status codes - **201**: NIO created - **404**: Instance doesn't exist -Sample session -*************** - -.. literalinclude:: examples/post_vpcsuuidportsportnumberdnio.txt - - -DELETE /vpcs/**{uuid}**/ports/**{port_number:\d+}**/nio -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +DELETE /v1/vpcs/**{uuid}**/ports/**{port_number:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Remove a NIO from a VPCS instance Parameters @@ -40,9 +34,3 @@ Response status codes - **404**: Instance doesn't exist - **204**: NIO deleted -Sample session -*************** - - -.. literalinclude:: examples/delete_vpcsuuidportsportnumberdnio.txt - diff --git a/docs/api/vpcsuuidreload.rst b/docs/api/v1vpcsuuidreload.rst similarity index 53% rename from docs/api/vpcsuuidreload.rst rename to docs/api/v1vpcsuuidreload.rst index 93c24a52..5495cc6e 100644 --- a/docs/api/vpcsuuidreload.rst +++ b/docs/api/v1vpcsuuidreload.rst @@ -1,10 +1,10 @@ -/vpcs/{uuid}/reload ---------------------------------------------- +/v1/vpcs/{uuid}/reload +----------------------------------------------------------- .. contents:: -POST /vpcs/**{uuid}**/reload -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +POST /v1/vpcs/**{uuid}**/reload +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Reload a VPCS instance Parameters diff --git a/docs/api/vpcsuuidstart.rst b/docs/api/v1vpcsuuidstart.rst similarity index 53% rename from docs/api/vpcsuuidstart.rst rename to docs/api/v1vpcsuuidstart.rst index aa02e25d..60b9af17 100644 --- a/docs/api/vpcsuuidstart.rst +++ b/docs/api/v1vpcsuuidstart.rst @@ -1,10 +1,10 @@ -/vpcs/{uuid}/start ---------------------------------------------- +/v1/vpcs/{uuid}/start +----------------------------------------------------------- .. contents:: -POST /vpcs/**{uuid}**/start -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +POST /v1/vpcs/**{uuid}**/start +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Start a VPCS instance Parameters diff --git a/docs/api/vpcsuuidstop.rst b/docs/api/v1vpcsuuidstop.rst similarity index 53% rename from docs/api/vpcsuuidstop.rst rename to docs/api/v1vpcsuuidstop.rst index a11f183f..90f96f06 100644 --- a/docs/api/vpcsuuidstop.rst +++ b/docs/api/v1vpcsuuidstop.rst @@ -1,10 +1,10 @@ -/vpcs/{uuid}/stop ---------------------------------------------- +/v1/vpcs/{uuid}/stop +----------------------------------------------------------- .. contents:: -POST /vpcs/**{uuid}**/stop -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +POST /v1/vpcs/**{uuid}**/stop +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Stop a VPCS instance Parameters diff --git a/docs/api/virtualboxlist.rst b/docs/api/virtualboxlist.rst deleted file mode 100644 index f6944d15..00000000 --- a/docs/api/virtualboxlist.rst +++ /dev/null @@ -1,13 +0,0 @@ -/virtualbox/list ---------------------------------------------- - -.. contents:: - -GET /virtualbox/list -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Get all VirtualBox VMs available - -Response status codes -********************** -- **200**: Success - diff --git a/docs/api/virtualboxuuidcaptureportiddstart.rst b/docs/api/virtualboxuuidcaptureportiddstart.rst deleted file mode 100644 index 342f1893..00000000 --- a/docs/api/virtualboxuuidcaptureportiddstart.rst +++ /dev/null @@ -1,29 +0,0 @@ -/virtualbox/{uuid}/capture/{port_id:\d+}/start ---------------------------------------------- - -.. contents:: - -POST /virtualbox/**{uuid}**/capture/**{port_id:\d+}**/start -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Start a packet capture on a VirtualBox VM instance - -Parameters -********** -- **uuid**: Instance UUID -- **port_id**: ID of the port to start a packet capture - -Response status codes -********************** -- **200**: Capture started -- **400**: Invalid instance UUID -- **404**: Instance doesn't exist - -Input -******* -.. raw:: html - - - - -
Name Mandatory Type Description
capture_filename string Capture file name
- diff --git a/docs/api/virtualboxuuidcaptureportiddstop.rst b/docs/api/virtualboxuuidcaptureportiddstop.rst deleted file mode 100644 index a4a35c47..00000000 --- a/docs/api/virtualboxuuidcaptureportiddstop.rst +++ /dev/null @@ -1,20 +0,0 @@ -/virtualbox/{uuid}/capture/{port_id:\d+}/stop ---------------------------------------------- - -.. contents:: - -POST /virtualbox/**{uuid}**/capture/**{port_id:\d+}**/stop -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Stop a packet capture on a VirtualBox VM instance - -Parameters -********** -- **uuid**: Instance UUID -- **port_id**: ID of the port to stop a packet capture - -Response status codes -********************** -- **400**: Invalid instance UUID -- **404**: Instance doesn't exist -- **204**: Capture stopped - diff --git a/docs/api/virtualboxuuidportsportiddnio.rst b/docs/api/virtualboxuuidportsportiddnio.rst deleted file mode 100644 index 160e5272..00000000 --- a/docs/api/virtualboxuuidportsportiddnio.rst +++ /dev/null @@ -1,48 +0,0 @@ -/virtualbox/{uuid}/ports/{port_id:\d+}/nio ---------------------------------------------- - -.. contents:: - -POST /virtualbox/**{uuid}**/ports/**{port_id:\d+}**/nio -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Add a NIO to a VirtualBox VM instance - -Parameters -********** -- **uuid**: Instance UUID -- **port_id**: ID of the port where the nio should be added - -Response status codes -********************** -- **400**: Invalid instance UUID -- **201**: NIO created -- **404**: Instance doesn't exist - -Sample session -*************** - - -.. literalinclude:: examples/post_virtualboxuuidportsportiddnio.txt - - -DELETE /virtualbox/**{uuid}**/ports/**{port_id:\d+}**/nio -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Remove a NIO from a VirtualBox VM instance - -Parameters -********** -- **uuid**: Instance UUID -- **port_id**: ID of the port from where the nio should be removed - -Response status codes -********************** -- **400**: Invalid instance UUID -- **404**: Instance doesn't exist -- **204**: NIO deleted - -Sample session -*************** - - -.. literalinclude:: examples/delete_virtualboxuuidportsportiddnio.txt - diff --git a/docs/api/virtualboxuuidstart.rst b/docs/api/virtualboxuuidstart.rst deleted file mode 100644 index 2e9662d7..00000000 --- a/docs/api/virtualboxuuidstart.rst +++ /dev/null @@ -1,25 +0,0 @@ -/virtualbox/{uuid}/start ---------------------------------------------- - -.. contents:: - -POST /virtualbox/**{uuid}**/start -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Start a VirtualBox VM instance - -Parameters -********** -- **uuid**: Instance UUID - -Response status codes -********************** -- **400**: Invalid instance UUID -- **404**: Instance doesn't exist -- **204**: Instance started - -Sample session -*************** - - -.. literalinclude:: examples/post_virtualboxuuidstart.txt - diff --git a/docs/api/virtualboxuuidstop.rst b/docs/api/virtualboxuuidstop.rst deleted file mode 100644 index e19340b9..00000000 --- a/docs/api/virtualboxuuidstop.rst +++ /dev/null @@ -1,25 +0,0 @@ -/virtualbox/{uuid}/stop ---------------------------------------------- - -.. contents:: - -POST /virtualbox/**{uuid}**/stop -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Stop a VirtualBox VM instance - -Parameters -********** -- **uuid**: Instance UUID - -Response status codes -********************** -- **400**: Invalid instance UUID -- **404**: Instance doesn't exist -- **204**: Instance stopped - -Sample session -*************** - - -.. literalinclude:: examples/post_virtualboxuuidstop.txt - diff --git a/docs/api/virtualboxvms.rst b/docs/api/virtualboxvms.rst deleted file mode 100644 index c8f79412..00000000 --- a/docs/api/virtualboxvms.rst +++ /dev/null @@ -1,13 +0,0 @@ -/virtualbox/vms ---------------------------------------------- - -.. contents:: - -GET /virtualbox/vms -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Get all VirtualBox VMs available - -Response status codes -********************** -- **200**: Success - diff --git a/docs/api/vpcsuuidportsportiddnio.rst b/docs/api/vpcsuuidportsportiddnio.rst deleted file mode 100644 index 6226c3cf..00000000 --- a/docs/api/vpcsuuidportsportiddnio.rst +++ /dev/null @@ -1,48 +0,0 @@ -/vpcs/{uuid}/ports/{port_id:\d+}/nio ---------------------------------------------- - -.. contents:: - -POST /vpcs/**{uuid}**/ports/**{port_id:\d+}**/nio -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Add a NIO to a VPCS instance - -Parameters -********** -- **uuid**: Instance UUID -- **port_id**: ID of the port where the nio should be added - -Response status codes -********************** -- **400**: Invalid instance UUID -- **201**: NIO created -- **404**: Instance doesn't exist - -Sample session -*************** - - -.. literalinclude:: examples/post_vpcsuuidportsportiddnio.txt - - -DELETE /vpcs/**{uuid}**/ports/**{port_id:\d+}**/nio -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Remove a NIO from a VPCS instance - -Parameters -********** -- **uuid**: Instance UUID -- **port_id**: ID of the port from where the nio should be removed - -Response status codes -********************** -- **400**: Invalid instance UUID -- **404**: Instance doesn't exist -- **204**: NIO deleted - -Sample session -*************** - - -.. literalinclude:: examples/delete_vpcsuuidportsportiddnio.txt - diff --git a/docs/api/vpcsuuidportsportidnio.rst b/docs/api/vpcsuuidportsportidnio.rst deleted file mode 100644 index fd5090de..00000000 --- a/docs/api/vpcsuuidportsportidnio.rst +++ /dev/null @@ -1,48 +0,0 @@ -/vpcs/{uuid}/ports/{port_id}/nio ---------------------------------------------- - -.. contents:: - -POST /vpcs/**{uuid}**/ports/**{port_id}**/nio -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Add a NIO to a VPCS - -Parameters -********** -- **port_id**: Id of the port where the nio should be add -- **uuid**: VPCS instance UUID - -Response status codes -********************** -- **400**: Invalid VPCS instance UUID -- **201**: NIO created -- **404**: VPCS instance doesn't exist - -Sample session -*************** - - -.. literalinclude:: examples/post_vpcsuuidportsportidnio.txt - - -DELETE /vpcs/**{uuid}**/ports/**{port_id}**/nio -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Remove a NIO from a VPCS - -Parameters -********** -- **port_id**: ID of the port where the nio should be removed -- **uuid**: VPCS instance UUID - -Response status codes -********************** -- **400**: Invalid VPCS instance UUID -- **404**: VPCS instance doesn't exist -- **204**: NIO deleted - -Sample session -*************** - - -.. literalinclude:: examples/delete_vpcsuuidportsportidnio.txt - diff --git a/gns3server/web/documentation.py b/gns3server/web/documentation.py index 66ddf1f0..04b5cfd3 100644 --- a/gns3server/web/documentation.py +++ b/gns3server/web/documentation.py @@ -34,11 +34,11 @@ class Documentation(object): 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('{}\n-----------------------------------------------------------\n\n'.format(path)) f.write('.. contents::\n') for method in handler_doc["methods"]: f.write('\n{} {}\n'.format(method["method"], path.replace("{", '**{').replace("}", "}**"))) - f.write('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n') + f.write('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n') f.write('{}\n\n'.format(method["description"])) if len(method["parameters"]) > 0: diff --git a/scripts/documentation.sh b/scripts/documentation.sh index fb7af59c..41111993 100755 --- a/scripts/documentation.sh +++ b/scripts/documentation.sh @@ -25,6 +25,9 @@ echo "WARNING: This script should be run at the root directory of the project" export PYTEST_BUILD_DOCUMENTATION=1 +rm -Rf docs/api/ +mkdir -p docs/api/examples + py.test -v python3 gns3server/web/documentation.py cd docs From 2ace014a3c16478a8ffc13056bb9b88597f90537 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 4 Feb 2015 17:18:53 +0100 Subject: [PATCH 177/485] Cleanup old temporary project at startup --- gns3server/main.py | 4 ++++ gns3server/modules/project.py | 31 ++++++++++++++++++++++++++-- tests/modules/test_project.py | 38 ++++++++++++++++++++++++++++++++++- 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/gns3server/main.py b/gns3server/main.py index 59ca9010..7b0bd3a7 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -26,6 +26,8 @@ from gns3server.server import Server from gns3server.web.logger import init_logger from gns3server.version import __version__ from gns3server.config import Config +from gns3server.modules.project import Project + import logging log = logging.getLogger(__name__) @@ -140,6 +142,8 @@ def main(): log.critical("The current working directory doesn't exist") return + Project.clean_project_directory() + host = server_config["host"] port = int(server_config["port"]) server = Server(host, port) diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index f78e78f4..55002bca 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -58,7 +58,6 @@ class Project: if config.get("local", False) is False: raise aiohttp.web.HTTPForbidden(text="You are not allowed to modifiy the project directory location") - self._temporary = temporary self._vms = set() self._vms_to_destroy = set() self._path = os.path.join(self._location, self._uuid) @@ -66,9 +65,11 @@ class Project: os.makedirs(os.path.join(self._path, "vms"), exist_ok=True) except OSError as e: raise aiohttp.web.HTTPInternalServerError(text="Could not create project directory: {}".format(e)) + self.temporary = temporary log.debug("Create project {uuid} in directory {path}".format(path=self._path, uuid=self._uuid)) - def _get_default_project_directory(self): + @classmethod + def _get_default_project_directory(cls): """ Return the default location for the project directory depending of the operating system @@ -109,8 +110,21 @@ class Project: @temporary.setter def temporary(self, temporary): + if hasattr(self, 'temporary') and temporary == self._temporary: + return + self._temporary = temporary + if self._temporary: + try: + with open(os.path.join(self._path, ".gns3_temporary"), 'w+') as f: + f.write("1") + except OSError as e: + raise aiohttp.web.HTTPInternalServerError(text="Could not create temporary project: {}".format(e)) + else: + if os.path.exists(os.path.join(self._path, ".gns3_temporary")): + os.remove(os.path.join(self._path, ".gns3_temporary")) + def vm_working_directory(self, vm): """ Return a working directory for a specific VM. @@ -222,3 +236,16 @@ class Project: """Remove project from disk""" yield from self._close_and_clean(True) + + @classmethod + def clean_project_directory(cls): + """At startup drop old temporary project. After a crash for example""" + + config = Config.instance().get_section_config("Server") + directory = config.get("project_directory", cls._get_default_project_directory()) + if os.path.exists(directory): + for project in os.listdir(directory): + path = os.path.join(directory, project) + if os.path.exists(os.path.join(path, ".gns3_temporary")): + log.warning("Purge old temporary project {}".format(project)) + shutil.rmtree(path) diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py index 3c93da88..25919d68 100644 --- a/tests/modules/test_project.py +++ b/tests/modules/test_project.py @@ -21,6 +21,7 @@ import asyncio import pytest import aiohttp import shutil +from uuid import uuid4 from unittest.mock import patch from gns3server.modules.project import Project @@ -53,11 +54,21 @@ def test_path(tmpdir): assert p.path == os.path.join(str(tmpdir), p.uuid) assert os.path.exists(os.path.join(str(tmpdir), p.uuid)) assert os.path.exists(os.path.join(str(tmpdir), p.uuid, 'vms')) + assert not os.path.exists(os.path.join(p.path, '.gns3_temporary')) def test_temporary_path(): - p = Project() + p = Project(temporary=True) + assert os.path.exists(p.path) + assert os.path.exists(os.path.join(p.path, '.gns3_temporary')) + + +def test_remove_temporary_flag(): + p = Project(temporary=True) assert os.path.exists(p.path) + assert os.path.exists(os.path.join(p.path, '.gns3_temporary')) + p.temporary = False + assert not os.path.exists(os.path.join(p.path, '.gns3_temporary')) def test_changing_location_not_allowed(tmpdir): @@ -164,3 +175,28 @@ def test_get_default_project_directory(): path = os.path.normpath(os.path.expanduser("~/GNS3/projects")) assert project._get_default_project_directory() == path assert os.path.exists(path) + + +def test_clean_project_directory(tmpdir): + + # A non anonymous project with uuid. + project1 = tmpdir / uuid4() + project1.mkdir() + + # A non anonymous project. + oldproject = tmpdir / uuid4() + oldproject.mkdir() + + # an anonymous project + project2 = tmpdir / uuid4() + project2.mkdir() + tmp = (project2 / ".gns3_temporary") + with open(str(tmp), 'w+') as f: + f.write("1") + + with patch("gns3server.config.Config.get_section_config", return_value={"project_directory": str(tmpdir)}): + Project.clean_project_directory() + + assert os.path.exists(str(project1)) + assert os.path.exists(str(oldproject)) + assert not os.path.exists(str(project2)) From 568e203580216cc15feec69b148ebddd26d3c474 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 4 Feb 2015 17:33:58 +0100 Subject: [PATCH 178/485] Increase timeout time for test in order to avoid false negative --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 09c25a0c..53eaab0b 100644 --- a/tox.ini +++ b/tox.ini @@ -10,4 +10,4 @@ ignore = E501 [pytest] norecursedirs = old_tests .tox -timeout = 1 +timeout = 2 From c5c219ffe1bcf63909e17c6dcf6e69c47b3e47f1 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 4 Feb 2015 21:17:00 +0100 Subject: [PATCH 179/485] Allow modification of path from the client --- gns3server/handlers/project_handler.py | 2 ++ gns3server/modules/project.py | 35 ++++++++++++++++++++------ gns3server/schemas/project.py | 9 +++++++ tests/api/test_project.py | 22 ++++++++++++++++ tests/modules/test_project.py | 10 ++++++-- 5 files changed, 68 insertions(+), 10 deletions(-) diff --git a/gns3server/handlers/project_handler.py b/gns3server/handlers/project_handler.py index aab739fd..5f707f4c 100644 --- a/gns3server/handlers/project_handler.py +++ b/gns3server/handlers/project_handler.py @@ -65,6 +65,7 @@ class ProjectHandler: }, status_codes={ 200: "The project has been updated", + 403: "You are not allowed to modify this property", 404: "The project doesn't exist" }, output=PROJECT_OBJECT_SCHEMA, @@ -74,6 +75,7 @@ class ProjectHandler: pm = ProjectManager.instance() project = pm.get_project(request.match_info["project_id"]) project.temporary = request.json.get("temporary", project.temporary) + project.path = request.json.get("path", project.path) response.json(project) @classmethod diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index 55002bca..0d1d7149 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -50,24 +50,26 @@ class Project: raise aiohttp.web.HTTPBadRequest(text="{} is not a valid UUID".format(uuid)) self._uuid = uuid - config = Config.instance().get_section_config("Server") - self._location = location + self._location = None if location is None: - self._location = config.get("project_directory", self._get_default_project_directory()) + self._location = self._config().get("project_directory", self._get_default_project_directory()) else: - if config.get("local", False) is False: - raise aiohttp.web.HTTPForbidden(text="You are not allowed to modifiy the project directory location") + self.location = location self._vms = set() self._vms_to_destroy = set() self._path = os.path.join(self._location, self._uuid) try: - os.makedirs(os.path.join(self._path, "vms"), exist_ok=True) + os.makedirs(self._path, exist_ok=True) except OSError as e: raise aiohttp.web.HTTPInternalServerError(text="Could not create project directory: {}".format(e)) self.temporary = temporary log.debug("Create project {uuid} in directory {path}".format(path=self._path, uuid=self._uuid)) + def _config(self): + + return Config.instance().get_section_config("Server") + @classmethod def _get_default_project_directory(cls): """ @@ -92,11 +94,27 @@ class Project: return self._location + @location.setter + def location(self, location): + + if location != self._location and self._config().get("local", False) is False: + raise aiohttp.web.HTTPForbidden(text="You are not allowed to modifiy the project directory location") + + self._location = location + @property def path(self): return self._path + @path.setter + def path(self, path): + + if path != self._path and self._config().get("local", False) is False: + raise aiohttp.web.HTTPForbidden(text="You are not allowed to modifiy the project directory location") + + self._path = path + @property def vms(self): @@ -167,8 +185,9 @@ class Project: return { "project_id": self._uuid, - "location": self._location, - "temporary": self._temporary + "temporary": self._temporary, + "path": self._path, + "location": self._location } def add_vm(self, vm): diff --git a/gns3server/schemas/project.py b/gns3server/schemas/project.py index f610b637..942b154b 100644 --- a/gns3server/schemas/project.py +++ b/gns3server/schemas/project.py @@ -50,6 +50,10 @@ PROJECT_UPDATE_SCHEMA = { "description": "If project is a temporary project", "type": "boolean" }, + "path": { + "description": "Path of the project on the server (work only with --local)", + "type": ["string", "null"] + }, }, "additionalProperties": False, } @@ -64,6 +68,11 @@ PROJECT_OBJECT_SCHEMA = { "type": "string", "minLength": 1 }, + "path": { + "description": "Directory of the project on the server", + "type": "string", + "minLength": 1 + }, "project_id": { "description": "Project UUID", "type": "string", diff --git a/tests/api/test_project.py b/tests/api/test_project.py index ec92a98d..ee381c99 100644 --- a/tests/api/test_project.py +++ b/tests/api/test_project.py @@ -60,6 +60,7 @@ def test_show_project(server): response = server.post("/projects", query) assert response.status == 200 response = server.get("/projects/00010203-0405-0607-0809-0a0b0c0d0e0f", example=True) + query["path"] = "/tmp/00010203-0405-0607-0809-0a0b0c0d0e0f" assert response.json == query @@ -78,6 +79,27 @@ def test_update_temporary_project(server): assert response.json["temporary"] is False +def test_update_path_project(server, tmpdir): + + with patch("gns3server.config.Config.get_section_config", return_value={"local": True}): + response = server.post("/projects", {}) + assert response.status == 200 + query = {"path": str(tmpdir)} + response = server.put("/projects/{project_id}".format(project_id=response.json["project_id"]), query, example=True) + assert response.status == 200 + assert response.json["path"] == str(tmpdir) + + +def test_update_path_project_non_local(server, tmpdir): + + with patch("gns3server.config.Config.get_section_config", return_value={"local": False}): + response = server.post("/projects", {}) + assert response.status == 200 + query = {"path": str(tmpdir)} + response = server.put("/projects/{project_id}".format(project_id=response.json["project_id"]), query, example=True) + assert response.status == 403 + + def test_commit_project(server, project): with asyncio_patch("gns3server.modules.project.Project.commit", return_value=True) as mock: response = server.post("/projects/{project_id}/commit".format(project_id=project.uuid), example=True) diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py index 25919d68..c50ee3e5 100644 --- a/tests/modules/test_project.py +++ b/tests/modules/test_project.py @@ -53,7 +53,6 @@ def test_path(tmpdir): p = Project(location=str(tmpdir)) assert p.path == os.path.join(str(tmpdir), p.uuid) assert os.path.exists(os.path.join(str(tmpdir), p.uuid)) - assert os.path.exists(os.path.join(str(tmpdir), p.uuid, 'vms')) assert not os.path.exists(os.path.join(p.path, '.gns3_temporary')) @@ -77,9 +76,16 @@ def test_changing_location_not_allowed(tmpdir): p = Project(location=str(tmpdir)) +def test_changing_path_not_allowed(tmpdir): + with patch("gns3server.config.Config.get_section_config", return_value={"local": False}): + with pytest.raises(aiohttp.web.HTTPForbidden): + p = Project() + p.path = str(tmpdir) + + def test_json(tmpdir): p = Project() - assert p.__json__() == {"location": p.location, "project_id": p.uuid, "temporary": False} + assert p.__json__() == {"location": p.location, "path": p.path, "project_id": p.uuid, "temporary": False} def test_vm_working_directory(tmpdir, vm): From 1bea78194ca008327828ec6a08fb580ec87c28c4 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 4 Feb 2015 13:48:29 -0700 Subject: [PATCH 180/485] Explicit ID names, remove {uuid} from URLs and add vms in URLs for VMs. --- gns3server/handlers/project_handler.py | 2 +- gns3server/handlers/virtualbox_handler.py | 80 ++++++------ gns3server/handlers/vpcs_handler.py | 52 ++++---- gns3server/modules/base_manager.py | 51 ++++---- gns3server/modules/base_vm.py | 32 ++--- gns3server/modules/project.py | 26 ++-- gns3server/modules/project_manager.py | 16 +-- gns3server/modules/virtualbox/__init__.py | 2 +- .../modules/virtualbox/virtualbox_vm.py | 123 +++++++++--------- gns3server/modules/vpcs/__init__.py | 24 ++-- gns3server/modules/vpcs/vpcs_vm.py | 28 ++-- gns3server/schemas/virtualbox.py | 14 +- gns3server/schemas/vpcs.py | 22 ++-- tests/api/test_project.py | 6 +- tests/api/test_virtualbox.py | 60 ++++----- tests/api/test_vpcs.py | 68 +++++----- tests/conftest.py | 2 +- tests/modules/test_project.py | 16 +-- tests/modules/test_project_manager.py | 2 +- .../modules/virtualbox/test_virtualbox_vm.py | 2 +- tests/modules/vpcs/test_vpcs_manager.py | 46 +++---- tests/modules/vpcs/test_vpcs_vm.py | 6 +- 22 files changed, 336 insertions(+), 344 deletions(-) diff --git a/gns3server/handlers/project_handler.py b/gns3server/handlers/project_handler.py index aab739fd..ad565427 100644 --- a/gns3server/handlers/project_handler.py +++ b/gns3server/handlers/project_handler.py @@ -33,7 +33,7 @@ class ProjectHandler: pm = ProjectManager.instance() p = pm.create_project( location=request.json.get("location"), - uuid=request.json.get("project_id"), + project_id=request.json.get("project_id"), temporary=request.json.get("temporary", False) ) response.json(p) diff --git a/gns3server/handlers/virtualbox_handler.py b/gns3server/handlers/virtualbox_handler.py index 74202c50..00518700 100644 --- a/gns3server/handlers/virtualbox_handler.py +++ b/gns3server/handlers/virtualbox_handler.py @@ -33,7 +33,7 @@ class VirtualBoxHandler: @classmethod @Route.get( - r"/virtualbox/vms", + r"/virtualbox/vms_tmp", status_codes={ 200: "Success", }, @@ -46,10 +46,10 @@ class VirtualBoxHandler: @classmethod @Route.post( - r"/virtualbox", + r"/virtualbox/vms", status_codes={ 201: "Instance created", - 400: "Invalid project UUID", + 400: "Invalid project ID", 409: "Conflict" }, description="Create a new VirtualBox VM instance", @@ -60,7 +60,7 @@ class VirtualBoxHandler: vbox_manager = VirtualBox.instance() vm = yield from vbox_manager.create_vm(request.json.pop("name"), request.json.pop("project_id"), - request.json.get("uuid"), + request.json.get("vm_id"), request.json.pop("vmname"), request.json.pop("linked_clone"), adapters=request.json.get("adapters", 0)) @@ -74,9 +74,9 @@ class VirtualBoxHandler: @classmethod @Route.get( - r"/virtualbox/{uuid}", + r"/virtualbox/vms/{vm_id}", parameters={ - "uuid": "Instance UUID" + "vm_id": "UUID for the instance" }, status_codes={ 200: "Success", @@ -87,14 +87,14 @@ class VirtualBoxHandler: def show(request, response): vbox_manager = VirtualBox.instance() - vm = vbox_manager.get_vm(request.match_info["uuid"]) + vm = vbox_manager.get_vm(request.match_info["vm_id"]) response.json(vm) @classmethod @Route.put( - r"/virtualbox/{uuid}", + r"/virtualbox/vms/{vm_id}", parameters={ - "uuid": "Instance UUID" + "vm_id": "UUID for the instance" }, status_codes={ 200: "Instance updated", @@ -107,7 +107,7 @@ class VirtualBoxHandler: def update(request, response): vbox_manager = VirtualBox.instance() - vm = vbox_manager.get_vm(request.match_info["uuid"]) + vm = vbox_manager.get_vm(request.match_info["vm_id"]) for name, value in request.json.items(): if hasattr(vm, name) and getattr(vm, name) != value: @@ -118,9 +118,9 @@ class VirtualBoxHandler: @classmethod @Route.delete( - r"/virtualbox/{uuid}", + r"/virtualbox/vms/{vm_id}", parameters={ - "uuid": "Instance UUID" + "vm_id": "UUID for the instance" }, status_codes={ 204: "Instance deleted", @@ -129,14 +129,14 @@ class VirtualBoxHandler: description="Delete a VirtualBox VM instance") def delete(request, response): - yield from VirtualBox.instance().delete_vm(request.match_info["uuid"]) + yield from VirtualBox.instance().delete_vm(request.match_info["vm_id"]) response.set_status(204) @classmethod @Route.post( - r"/virtualbox/{uuid}/start", + r"/virtualbox/vms/{vm_id}/start", parameters={ - "uuid": "Instance UUID" + "vm_id": "UUID for the instance" }, status_codes={ 204: "Instance started", @@ -147,15 +147,15 @@ class VirtualBoxHandler: def start(request, response): vbox_manager = VirtualBox.instance() - vm = vbox_manager.get_vm(request.match_info["uuid"]) + vm = vbox_manager.get_vm(request.match_info["vm_id"]) yield from vm.start() response.set_status(204) @classmethod @Route.post( - r"/virtualbox/{uuid}/stop", + r"/virtualbox/vms/{vm_id}/stop", parameters={ - "uuid": "Instance UUID" + "vm_id": "UUID for the instance" }, status_codes={ 204: "Instance stopped", @@ -166,15 +166,15 @@ class VirtualBoxHandler: def stop(request, response): vbox_manager = VirtualBox.instance() - vm = vbox_manager.get_vm(request.match_info["uuid"]) + vm = vbox_manager.get_vm(request.match_info["vm_id"]) yield from vm.stop() response.set_status(204) @classmethod @Route.post( - r"/virtualbox/{uuid}/suspend", + r"/virtualbox/vms/{vm_id}/suspend", parameters={ - "uuid": "Instance UUID" + "vm_id": "UUID for the instance" }, status_codes={ 204: "Instance suspended", @@ -185,15 +185,15 @@ class VirtualBoxHandler: def suspend(request, response): vbox_manager = VirtualBox.instance() - vm = vbox_manager.get_vm(request.match_info["uuid"]) + vm = vbox_manager.get_vm(request.match_info["vm_id"]) yield from vm.suspend() response.set_status(204) @classmethod @Route.post( - r"/virtualbox/{uuid}/resume", + r"/virtualbox/vms/{vm_id}/resume", parameters={ - "uuid": "Instance UUID" + "vm_id": "UUID for the instance" }, status_codes={ 204: "Instance resumed", @@ -204,15 +204,15 @@ class VirtualBoxHandler: def suspend(request, response): vbox_manager = VirtualBox.instance() - vm = vbox_manager.get_vm(request.match_info["uuid"]) + vm = vbox_manager.get_vm(request.match_info["vm_id"]) yield from vm.resume() response.set_status(204) @classmethod @Route.post( - r"/virtualbox/{uuid}/reload", + r"/virtualbox/vms/{vm_id}/reload", parameters={ - "uuid": "Instance UUID" + "vm_id": "UUID for the instance" }, status_codes={ 204: "Instance reloaded", @@ -223,14 +223,14 @@ class VirtualBoxHandler: def suspend(request, response): vbox_manager = VirtualBox.instance() - vm = vbox_manager.get_vm(request.match_info["uuid"]) + vm = vbox_manager.get_vm(request.match_info["vm_id"]) yield from vm.reload() response.set_status(204) @Route.post( - r"/virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio", + r"/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/nio", parameters={ - "uuid": "Instance UUID", + "vm_id": "UUID for the instance", "adapter_id": "Adapter where the nio should be added" }, status_codes={ @@ -244,7 +244,7 @@ class VirtualBoxHandler: def create_nio(request, response): vbox_manager = VirtualBox.instance() - vm = vbox_manager.get_vm(request.match_info["uuid"]) + vm = vbox_manager.get_vm(request.match_info["vm_id"]) nio = vbox_manager.create_nio(vbox_manager.vboxmanage_path, request.json) vm.port_add_nio_binding(int(request.match_info["adapter_id"]), nio) response.set_status(201) @@ -252,9 +252,9 @@ class VirtualBoxHandler: @classmethod @Route.delete( - r"/virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio", + r"/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/nio", parameters={ - "uuid": "Instance UUID", + "vm_id": "UUID for the instance", "adapter_id": "Adapter from where the nio should be removed" }, status_codes={ @@ -266,14 +266,14 @@ class VirtualBoxHandler: def delete_nio(request, response): vbox_manager = VirtualBox.instance() - vm = vbox_manager.get_vm(request.match_info["uuid"]) + vm = vbox_manager.get_vm(request.match_info["vm_id"]) vm.port_remove_nio_binding(int(request.match_info["adapter_id"])) response.set_status(204) @Route.post( - r"/virtualbox/{uuid}/capture/{adapter_id:\d+}/start", + r"/virtualbox/{vm_id}/capture/{adapter_id:\d+}/start", parameters={ - "uuid": "Instance UUID", + "vm_id": "UUID for the instance", "adapter_id": "Adapter to start a packet capture" }, status_codes={ @@ -286,16 +286,16 @@ class VirtualBoxHandler: def start_capture(request, response): vbox_manager = VirtualBox.instance() - vm = vbox_manager.get_vm(request.match_info["uuid"]) + vm = vbox_manager.get_vm(request.match_info["vm_id"]) adapter_id = int(request.match_info["adapter_id"]) pcap_file_path = os.path.join(vm.project.capture_working_directory(), request.json["filename"]) vm.start_capture(adapter_id, pcap_file_path) response.json({"pcap_file_path": pcap_file_path}) @Route.post( - r"/virtualbox/{uuid}/capture/{adapter_id:\d+}/stop", + r"/virtualbox/{vm_id}/capture/{adapter_id:\d+}/stop", parameters={ - "uuid": "Instance UUID", + "vm_id": "UUID for the instance", "adapter_id": "Adapter to stop a packet capture" }, status_codes={ @@ -307,6 +307,6 @@ class VirtualBoxHandler: def start_capture(request, response): vbox_manager = VirtualBox.instance() - vm = vbox_manager.get_vm(request.match_info["uuid"]) + vm = vbox_manager.get_vm(request.match_info["vm_id"]) vm.stop_capture(int(request.match_info["adapter_id"])) response.set_status(204) diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index cbd78c83..539ebb68 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -31,7 +31,7 @@ class VPCSHandler: @classmethod @Route.post( - r"/vpcs", + r"/vpcs/vms", status_codes={ 201: "Instance created", 400: "Invalid project UUID", @@ -45,7 +45,7 @@ class VPCSHandler: vpcs = VPCS.instance() vm = yield from vpcs.create_vm(request.json["name"], request.json["project_id"], - request.json.get("uuid"), + request.json.get("vm_id"), console=request.json.get("console"), startup_script=request.json.get("startup_script")) response.set_status(201) @@ -53,9 +53,9 @@ class VPCSHandler: @classmethod @Route.get( - r"/vpcs/{uuid}", + r"/vpcs/vms/{vm_id}", parameters={ - "uuid": "Instance UUID" + "vm_id": "UUID for the instance" }, status_codes={ 200: "Success", @@ -66,14 +66,14 @@ class VPCSHandler: def show(request, response): vpcs_manager = VPCS.instance() - vm = vpcs_manager.get_vm(request.match_info["uuid"]) + vm = vpcs_manager.get_vm(request.match_info["vm_id"]) response.json(vm) @classmethod @Route.put( - r"/vpcs/{uuid}", + r"/vpcs/vms/{vm_id}", parameters={ - "uuid": "Instance UUID" + "vm_id": "UUID for the instance" }, status_codes={ 200: "Instance updated", @@ -86,7 +86,7 @@ class VPCSHandler: def update(request, response): vpcs_manager = VPCS.instance() - vm = vpcs_manager.get_vm(request.match_info["uuid"]) + vm = vpcs_manager.get_vm(request.match_info["vm_id"]) vm.name = request.json.get("name", vm.name) vm.console = request.json.get("console", vm.console) vm.startup_script = request.json.get("startup_script", vm.startup_script) @@ -94,9 +94,9 @@ class VPCSHandler: @classmethod @Route.delete( - r"/vpcs/{uuid}", + r"/vpcs/vms/{vm_id}", parameters={ - "uuid": "Instance UUID" + "vm_id": "UUID for the instance" }, status_codes={ 204: "Instance deleted", @@ -105,14 +105,14 @@ class VPCSHandler: description="Delete a VPCS instance") def delete(request, response): - yield from VPCS.instance().delete_vm(request.match_info["uuid"]) + yield from VPCS.instance().delete_vm(request.match_info["vm_id"]) response.set_status(204) @classmethod @Route.post( - r"/vpcs/{uuid}/start", + r"/vpcs/vms/{vm_id}/start", parameters={ - "uuid": "Instance UUID" + "vm_id": "UUID for the instance" }, status_codes={ 204: "Instance started", @@ -123,15 +123,15 @@ class VPCSHandler: def start(request, response): vpcs_manager = VPCS.instance() - vm = vpcs_manager.get_vm(request.match_info["uuid"]) + vm = vpcs_manager.get_vm(request.match_info["vm_id"]) yield from vm.start() response.set_status(204) @classmethod @Route.post( - r"/vpcs/{uuid}/stop", + r"/vpcs/vms/{vm_id}/stop", parameters={ - "uuid": "Instance UUID" + "vm_id": "UUID for the instance" }, status_codes={ 204: "Instance stopped", @@ -142,15 +142,15 @@ class VPCSHandler: def stop(request, response): vpcs_manager = VPCS.instance() - vm = vpcs_manager.get_vm(request.match_info["uuid"]) + vm = vpcs_manager.get_vm(request.match_info["vm_id"]) yield from vm.stop() response.set_status(204) @classmethod @Route.post( - r"/vpcs/{uuid}/reload", + r"/vpcs/vms/{vm_id}/reload", parameters={ - "uuid": "Instance UUID", + "vm_id": "UUID for the instance", }, status_codes={ 204: "Instance reloaded", @@ -161,14 +161,14 @@ class VPCSHandler: def reload(request, response): vpcs_manager = VPCS.instance() - vm = vpcs_manager.get_vm(request.match_info["uuid"]) + vm = vpcs_manager.get_vm(request.match_info["vm_id"]) yield from vm.reload() response.set_status(204) @Route.post( - r"/vpcs/{uuid}/ports/{port_number:\d+}/nio", + r"/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio", parameters={ - "uuid": "Instance UUID", + "vm_id": "UUID for the instance", "port_number": "Port where the nio should be added" }, status_codes={ @@ -182,7 +182,7 @@ class VPCSHandler: def create_nio(request, response): vpcs_manager = VPCS.instance() - vm = vpcs_manager.get_vm(request.match_info["uuid"]) + vm = vpcs_manager.get_vm(request.match_info["vm_id"]) nio = vpcs_manager.create_nio(vm.vpcs_path, request.json) vm.port_add_nio_binding(int(request.match_info["port_number"]), nio) response.set_status(201) @@ -190,9 +190,9 @@ class VPCSHandler: @classmethod @Route.delete( - r"/vpcs/{uuid}/ports/{port_number:\d+}/nio", + r"/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio", parameters={ - "uuid": "Instance UUID", + "vm_id": "UUID for the instance", "port_number": "Port from where the nio should be removed" }, status_codes={ @@ -204,6 +204,6 @@ class VPCSHandler: def delete_nio(request, response): vpcs_manager = VPCS.instance() - vm = vpcs_manager.get_vm(request.match_info["uuid"]) + vm = vpcs_manager.get_vm(request.match_info["vm_id"]) vm.port_remove_nio_binding(int(request.match_info["port_number"])) response.set_status(204) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index c2efd9dc..e1cecf22 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -97,71 +97,72 @@ class BaseManager: @asyncio.coroutine def unload(self): - for uuid in self._vms.keys(): + for vm_id in self._vms.keys(): try: - yield from self.close_vm(uuid) + yield from self.close_vm(vm_id) except Exception as e: - log.error("Could not delete VM {}: {}".format(uuid, e), exc_info=1) + log.error("Could not delete VM {}: {}".format(vm_id, e), exc_info=1) continue if hasattr(BaseManager, "_instance"): BaseManager._instance = None log.debug("Module {} unloaded".format(self.module_name)) - def get_vm(self, uuid): + def get_vm(self, vm_id): """ Returns a VM instance. - :param uuid: VM UUID + :param vm_id: VM identifier :returns: VM instance """ try: - UUID(uuid, version=4) + UUID(vm_id, version=4) except ValueError: - raise aiohttp.web.HTTPBadRequest(text="{} is not a valid UUID".format(uuid)) + raise aiohttp.web.HTTPBadRequest(text="{} is not a valid UUID".format(vm_id)) - if uuid not in self._vms: - raise aiohttp.web.HTTPNotFound(text="UUID {} doesn't exist".format(uuid)) - return self._vms[uuid] + if vm_id not in self._vms: + raise aiohttp.web.HTTPNotFound(text="ID {} doesn't exist".format(vm_id)) + return self._vms[vm_id] @asyncio.coroutine - def create_vm(self, name, project_uuid, uuid, *args, **kwargs): + def create_vm(self, name, project_id, vm_id, *args, **kwargs): """ Create a new VM :param name: VM name - :param project_uuid: UUID of Project - :param uuid: restore a VM UUID + :param project_id: Project identifier + :param vm_id: restore a VM identifier """ - project = ProjectManager.instance().get_project(project_uuid) + project = ProjectManager.instance().get_project(project_id) # TODO: support for old projects VM with normal IDs. - if not uuid: - uuid = str(uuid4()) + if not vm_id: + vm_id = str(uuid4()) - vm = self._VM_CLASS(name, uuid, project, self, *args, **kwargs) + vm = self._VM_CLASS(name, vm_id, project, self, *args, **kwargs) if asyncio.iscoroutinefunction(vm.create): yield from vm.create() else: vm.create() - self._vms[vm.uuid] = vm + self._vms[vm.id] = vm project.add_vm(vm) return vm @asyncio.coroutine - def close_vm(self, uuid): + def close_vm(self, vm_id): """ Delete a VM - :param uuid: VM UUID + :param vm_id: VM identifier + :returns: VM instance """ - vm = self.get_vm(uuid) + vm = self.get_vm(vm_id) if asyncio.iscoroutinefunction(vm.close): yield from vm.close() else: @@ -169,18 +170,18 @@ class BaseManager: return vm @asyncio.coroutine - def delete_vm(self, uuid): + def delete_vm(self, vm_id): """ Delete a VM. VM working directory will be destroy when we receive a commit. - :param uuid: VM UUID + :param vm_id: VM identifier :returns: VM instance """ - vm = yield from self.close_vm(uuid) + vm = yield from self.close_vm(vm_id) vm.project.mark_vm_for_destruction(vm) - del self._vms[vm.uuid] + del self._vms[vm.id] return vm @staticmethod diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index 52c003b2..acf04f12 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -21,16 +21,16 @@ log = logging.getLogger(__name__) class BaseVM: - def __init__(self, name, uuid, project, manager): + def __init__(self, name, vm_id, project, manager): self._name = name - self._uuid = uuid + self._id = vm_id self._project = project self._manager = manager - log.debug("{module}: {name} [{uuid}] initialized".format(module=self.manager.module_name, - name=self.name, - uuid=self.uuid)) + log.debug("{module}: {name} [{id}] initialized".format(module=self.manager.module_name, + name=self.name, + id=self.id)) def __del__(self): @@ -64,21 +64,21 @@ class BaseVM: :param new_name: name """ - log.info("{module}: {name} [{uuid}] renamed to {new_name}".format(module=self.manager.module_name, - name=self.name, - uuid=self.uuid, - new_name=new_name)) + log.info("{module}: {name} [{id}] renamed to {new_name}".format(module=self.manager.module_name, + name=self.name, + id=self.id, + new_name=new_name)) self._name = new_name @property - def uuid(self): + def id(self): """ - Returns the UUID for this VM. + Returns the ID for this VM. - :returns: uuid (string) + :returns: VM identifier (string) """ - return self._uuid + return self._id @property def manager(self): @@ -103,9 +103,9 @@ class BaseVM: Creates the VM. """ - log.info("{module}: {name} [{uuid}] created".format(module=self.manager.module_name, - name=self.name, - uuid=self.uuid)) + log.info("{module}: {name} [{id}] created".format(module=self.manager.module_name, + name=self.name, + id=self.id)) def start(self): """ diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index 55002bca..fa8ce135 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -34,21 +34,21 @@ class Project: A project contains a list of VM. In theory VM are isolated project/project. - :param uuid: Force project uuid (None by default auto generate an UUID) + :param project_id: Force project identifier (None by default auto generate an UUID) :param location: Parent path of the project. (None should create a tmp directory) :param temporary: Boolean the project is a temporary project (destroy when closed) """ - def __init__(self, uuid=None, location=None, temporary=False): + def __init__(self, project_id=None, location=None, temporary=False): - if uuid is None: - self._uuid = str(uuid4()) + if project_id is None: + self._id = str(uuid4()) else: try: - UUID(uuid, version=4) + UUID(project_id, version=4) except ValueError: - raise aiohttp.web.HTTPBadRequest(text="{} is not a valid UUID".format(uuid)) - self._uuid = uuid + raise aiohttp.web.HTTPBadRequest(text="{} is not a valid UUID".format(project_id)) + self._id = project_id config = Config.instance().get_section_config("Server") self._location = location @@ -60,13 +60,13 @@ class Project: self._vms = set() self._vms_to_destroy = set() - self._path = os.path.join(self._location, self._uuid) + self._path = os.path.join(self._location, self._id) try: os.makedirs(os.path.join(self._path, "vms"), exist_ok=True) except OSError as e: raise aiohttp.web.HTTPInternalServerError(text="Could not create project directory: {}".format(e)) self.temporary = temporary - log.debug("Create project {uuid} in directory {path}".format(path=self._path, uuid=self._uuid)) + log.debug("Create project {id} in directory {path}".format(path=self._path, id=self._id)) @classmethod def _get_default_project_directory(cls): @@ -83,9 +83,9 @@ class Project: return path @property - def uuid(self): + def id(self): - return self._uuid + return self._id @property def location(self): @@ -134,7 +134,7 @@ class Project: :returns: A string with a VM working directory """ - workdir = os.path.join(self._path, vm.manager.module_name.lower(), vm.uuid) + workdir = os.path.join(self._path, vm.manager.module_name.lower(), vm.id) try: os.makedirs(workdir, exist_ok=True) except OSError as e: @@ -166,7 +166,7 @@ class Project: def __json__(self): return { - "project_id": self._uuid, + "project_id": self._id, "location": self._location, "temporary": self._temporary } diff --git a/gns3server/modules/project_manager.py b/gns3server/modules/project_manager.py index 1aa264b1..55cae7c4 100644 --- a/gns3server/modules/project_manager.py +++ b/gns3server/modules/project_manager.py @@ -42,23 +42,23 @@ class ProjectManager: cls._instance = cls() return cls._instance - def get_project(self, uuid): + def get_project(self, project_id): """ Returns a Project instance. - :param uuid: Project UUID + :param project_id: Project identifier :returns: Project instance """ try: - UUID(uuid, version=4) + UUID(project_id, version=4) except ValueError: - raise aiohttp.web.HTTPBadRequest(text="{} is not a valid UUID".format(uuid)) + raise aiohttp.web.HTTPBadRequest(text="{} is not a valid UUID".format(project_id)) - if uuid not in self._projects: - raise aiohttp.web.HTTPNotFound(text="Project UUID {} doesn't exist".format(uuid)) - return self._projects[uuid] + if project_id not in self._projects: + raise aiohttp.web.HTTPNotFound(text="Project ID {} doesn't exist".format(project_id)) + return self._projects[project_id] def create_project(self, **kwargs): """ @@ -68,5 +68,5 @@ class ProjectManager: """ project = Project(**kwargs) - self._projects[project.uuid] = project + self._projects[project.id] = project return project diff --git a/gns3server/modules/virtualbox/__init__.py b/gns3server/modules/virtualbox/__init__.py index dedc4719..245d8976 100644 --- a/gns3server/modules/virtualbox/__init__.py +++ b/gns3server/modules/virtualbox/__init__.py @@ -119,7 +119,7 @@ class VirtualBox(BaseManager): vms = [] result = yield from self.execute("list", ["vms"]) for line in result: - vmname, uuid = line.rsplit(' ', 1) + vmname, _ = line.rsplit(' ', 1) vmname = vmname.strip('"') if vmname == "": continue # ignore inaccessible VMs diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 018527c1..bc59ab6d 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -48,9 +48,9 @@ class VirtualBoxVM(BaseVM): VirtualBox VM implementation. """ - def __init__(self, name, uuid, project, manager, vmname, linked_clone, adapters=0): + def __init__(self, name, vm_id, project, manager, vmname, linked_clone, adapters=0): - super().__init__(name, uuid, project, manager) + super().__init__(name, vm_id, project, manager) self._maximum_adapters = 8 self._linked_clone = linked_clone @@ -77,9 +77,9 @@ class VirtualBoxVM(BaseVM): def __json__(self): return {"name": self.name, - "uuid": self.uuid, + "vm_id": self.id, "console": self.console, - "project_id": self.project.uuid, + "project_id": self.project.id, "vmname": self.vmname, "headless": self.headless, "enable_remote_console": self.enable_remote_console, @@ -144,10 +144,10 @@ class VirtualBoxVM(BaseVM): yield from self._get_system_properties() if parse_version(self._system_properties["API version"]) < parse_version("4_3"): raise VirtualBoxError("The VirtualBox API version is lower than 4.3") - log.info("VirtualBox VM '{name}' [{uuid}] created".format(name=self.name, uuid=self.uuid)) + log.info("VirtualBox VM '{name}' [{id}] created".format(name=self.name, id=self.id)) if self._linked_clone: - if self.uuid and os.path.isdir(os.path.join(self.working_dir, self._vmname)): + if self.id and os.path.isdir(os.path.join(self.working_dir, self._vmname)): vbox_file = os.path.join(self.working_dir, self._vmname, self._vmname + ".vbox") yield from self.manager.execute("registervm", [vbox_file]) yield from self._reattach_hdds() @@ -180,7 +180,7 @@ class VirtualBoxVM(BaseVM): if self._headless: args.extend(["--type", "headless"]) result = yield from self.manager.execute("startvm", args) - log.info("VirtualBox VM '{name}' [{uuid}] started".format(name=self.name, uuid=self.uuid)) + log.info("VirtualBox VM '{name}' [{id}] started".format(name=self.name, id=self.id)) log.debug("Start result: {}".format(result)) # add a guest property to let the VM know about the GNS3 name @@ -202,7 +202,7 @@ class VirtualBoxVM(BaseVM): if vm_state == "running" or vm_state == "paused" or vm_state == "stuck": # power off the VM result = yield from self._control_vm("poweroff") - log.info("VirtualBox VM '{name}' [{uuid}] stopped".format(name=self.name, uuid=self.uuid)) + log.info("VirtualBox VM '{name}' [{id}] stopped".format(name=self.name, id=self.id)) log.debug("Stop result: {}".format(result)) yield from asyncio.sleep(0.5) # give some time for VirtualBox to unlock the VM @@ -228,11 +228,11 @@ class VirtualBoxVM(BaseVM): vm_state = yield from self._get_vm_state() if vm_state == "running": yield from self._control_vm("pause") - log.info("VirtualBox VM '{name}' [{uuid}] suspended".format(name=self.name, uuid=self.uuid)) + log.info("VirtualBox VM '{name}' [{id}] suspended".format(name=self.name, id=self.id)) else: - log.warn("VirtualBox VM '{name}' [{uuid}] cannot be suspended, current state: {state}".format(name=self.name, - uuid=self.uuid, - state=vm_state)) + log.warn("VirtualBox VM '{name}' [{id}] cannot be suspended, current state: {state}".format(name=self.name, + id=self.id, + state=vm_state)) @asyncio.coroutine def resume(self): @@ -241,7 +241,7 @@ class VirtualBoxVM(BaseVM): """ yield from self._control_vm("resume") - log.info("VirtualBox VM '{name}' [{uuid}] resumed".format(name=self.name, uuid=self.uuid)) + log.info("VirtualBox VM '{name}' [{id}] resumed".format(name=self.name, id=self.id)) @asyncio.coroutine def reload(self): @@ -250,7 +250,7 @@ class VirtualBoxVM(BaseVM): """ result = yield from self._control_vm("reset") - log.info("VirtualBox VM '{name}' [{uuid}] reloaded".format(name=self.name, uuid=self.uuid)) + log.info("VirtualBox VM '{name}' [{id}] reloaded".format(name=self.name, id=self.id)) log.debug("Reload result: {}".format(result)) @property @@ -275,9 +275,9 @@ class VirtualBoxVM(BaseVM): self._manager.port_manager.release_console_port(self._console) self._console = self._manager.port_manager.reserve_console_port(console) - log.info("VirtualBox VM '{name}' [{uuid}]: console port set to {port}".format(name=self.name, - uuid=self.uuid, - port=console)) + log.info("VirtualBox VM '{name}' [{id}]: console port set to {port}".format(name=self.name, + id=self.id, + port=console)) @asyncio.coroutine def _get_all_hdd_files(self): @@ -306,12 +306,12 @@ class VirtualBoxVM(BaseVM): for hdd_info in hdd_table: hdd_file = os.path.join(self.working_dir, self._vmname, "Snapshots", hdd_info["hdd"]) if os.path.exists(hdd_file): - log.info("VirtualBox VM '{name}' [{uuid}] attaching HDD {controller} {port} {device} {medium}".format(name=self.name, - uuid=self.uuid, - controller=hdd_info["controller"], - port=hdd_info["port"], - device=hdd_info["device"], - medium=hdd_file)) + log.info("VirtualBox VM '{name}' [{id}] attaching HDD {controller} {port} {device} {medium}".format(name=self.name, + id=self.id, + controller=hdd_info["controller"], + port=hdd_info["port"], + device=hdd_info["device"], + medium=hdd_file)) yield from self._storage_attach('--storagectl "{}" --port {} --device {} --type hdd --medium "{}"'.format(hdd_info["controller"], hdd_info["port"], hdd_info["device"], @@ -345,11 +345,11 @@ class VirtualBoxVM(BaseVM): port = match.group(2) device = match.group(3) if value in hdd_files: - log.info("VirtualBox VM '{name}' [{uuid}] detaching HDD {controller} {port} {device}".format(name=self.name, - uuid=self.uuid, - controller=controller, - port=port, - device=device)) + log.info("VirtualBox VM '{name}' [{id}] detaching HDD {controller} {port} {device}".format(name=self.name, + id=self.id, + controller=controller, + port=port, + device=device)) yield from self._storage_attach('--storagectl "{}" --port {} --device {} --type hdd --medium none'.format(controller, port, device)) hdd_table.append( { @@ -360,7 +360,7 @@ class VirtualBoxVM(BaseVM): } ) - log.info("VirtualBox VM '{name}' [{uuid}] unregistering".format(name=self.name, uuid=self.uuid)) + log.info("VirtualBox VM '{name}' [{id}] unregistering".format(name=self.name, id=self.id)) yield from self.manager.execute("unregistervm", [self._name]) if hdd_table: @@ -369,12 +369,11 @@ class VirtualBoxVM(BaseVM): with open(hdd_info_file, "w") as f: json.dump(hdd_table, f, indent=4) except OSError as e: - log.warning("VirtualBox VM '{name}' [{uuid}] could not write HHD info file: {error}".format(name=self.name, - uuid=self.uuid, - error=e.strerror)) + log.warning("VirtualBox VM '{name}' [{id}] could not write HHD info file: {error}".format(name=self.name, + id=self.id, + error=e.strerror)) - log.info("VirtualBox VM '{name}' [{uuid}] closed".format(name=self.name, - uuid=self.uuid)) + log.info("VirtualBox VM '{name}' [{id}] closed".format(name=self.name, id=self.id)) self._closed = True @property @@ -396,9 +395,9 @@ class VirtualBoxVM(BaseVM): """ if headless: - log.info("VirtualBox VM '{name}' [{uuid}] has enabled the headless mode".format(name=self.name, uuid=self.uuid)) + log.info("VirtualBox VM '{name}' [{id}] has enabled the headless mode".format(name=self.name, id=self.id)) else: - log.info("VirtualBox VM '{name}' [{uuid}] has disabled the headless mode".format(name=self.name, uuid=self.uuid)) + log.info("VirtualBox VM '{name}' [{id}] has disabled the headless mode".format(name=self.name, id=self.id)) self._headless = headless @property @@ -420,10 +419,10 @@ class VirtualBoxVM(BaseVM): """ if enable_remote_console: - log.info("VirtualBox VM '{name}' [{uuid}] has enabled the console".format(name=self.name, uuid=self.uuid)) + log.info("VirtualBox VM '{name}' [{id}] has enabled the console".format(name=self.name, id=self.id)) # self._start_remote_console() else: - log.info("VirtualBox VM '{name}' [{uuid}] has disabled the console".format(name=self.name, uuid=self.uuid)) + log.info("VirtualBox VM '{name}' [{id}] has disabled the console".format(name=self.name, id=self.id)) # self._stop_remote_console() self._enable_remote_console = enable_remote_console @@ -445,7 +444,7 @@ class VirtualBoxVM(BaseVM): :param vmname: VirtualBox VM name """ - log.info("VirtualBox VM '{name}' [{uuid}] has set the VM name to '{vmname}'".format(name=self.name, uuid=self.uuid, vmname=vmname)) + log.info("VirtualBox VM '{name}' [{id}] has set the VM name to '{vmname}'".format(name=self.name, id=self.id, vmname=vmname)) # TODO: test linked clone # if self._linked_clone: # yield from self._modify_vm('--name "{}"'.format(vmname)) @@ -472,9 +471,9 @@ class VirtualBoxVM(BaseVM): self._ethernet_adapters.append(EthernetAdapter()) self._adapters = len(self._ethernet_adapters) - log.info("VirtualBox VM '{name}' [{uuid}] has changed the number of Ethernet adapters to {adapters}".format(name=self.name, - uuid=self.uuid, - adapters=adapters)) + log.info("VirtualBox VM '{name}' [{id}] has changed the number of Ethernet adapters to {adapters}".format(name=self.name, + id=self.id, + adapters=adapters)) @property def adapter_start_index(self): @@ -496,9 +495,9 @@ class VirtualBoxVM(BaseVM): self._adapter_start_index = adapter_start_index self.adapters = self.adapters # this forces to recreate the adapter list with the correct index - log.info("VirtualBox VM '{name}' [{uuid}]: adapter start index changed to {index}".format(name=self.name, - uuid=self.uuid, - index=adapter_start_index)) + log.info("VirtualBox VM '{name}' [{id}]: adapter start index changed to {index}".format(name=self.name, + id=self.id, + index=adapter_start_index)) @property def adapter_type(self): @@ -520,9 +519,9 @@ class VirtualBoxVM(BaseVM): self._adapter_type = adapter_type - log.info("VirtualBox VM '{name}' [{uuid}]: adapter type changed to {adapter_type}".format(name=self.name, - uuid=self.uuid, - adapter_type=adapter_type)) + log.info("VirtualBox VM '{name}' [{id}]: adapter type changed to {adapter_type}".format(name=self.name, + id=self.id, + adapter_type=adapter_type)) @asyncio.coroutine def _get_vm_info(self): @@ -779,10 +778,10 @@ class VirtualBoxVM(BaseVM): yield from self._control_vm("setlinkstate{} on".format(adapter_id + 1)) adapter.add_nio(0, nio) - log.info("VirtualBox VM '{name}' [{uuid}]: {nio} added to adapter {adapter_id}".format(name=self.name, - uuid=self.uuid, - nio=nio, - adapter_id=adapter_id)) + log.info("VirtualBox VM '{name}' [{id}]: {nio} added to adapter {adapter_id}".format(name=self.name, + id=self.id, + nio=nio, + adapter_id=adapter_id)) @asyncio.coroutine def port_remove_nio_binding(self, adapter_id): @@ -811,10 +810,10 @@ class VirtualBoxVM(BaseVM): self.manager.port_manager.release_udp_port(nio.lport) adapter.remove_nio(0) - log.info("VirtualBox VM '{name}' [{uuid}]: {nio} removed from adapter {adapter_id}".format(name=self.name, - uuid=self.uuid, - nio=nio, - adapter_id=adapter_id)) + log.info("VirtualBox VM '{name}' [{id}]: {nio} removed from adapter {adapter_id}".format(name=self.name, + id=self.id, + nio=nio, + adapter_id=adapter_id)) return nio def start_capture(self, adapter_id, output_file): @@ -836,9 +835,9 @@ class VirtualBoxVM(BaseVM): raise VirtualBoxError("Packet capture is already activated on adapter {adapter_id}".format(adapter_id=adapter_id)) nio.startPacketCapture(output_file) - log.info("VirtualBox VM '{name}' [{uuid}]: starting packet capture on adapter {adapter_id}".format(name=self.name, - uuid=self.uuid, - adapter_id=adapter_id)) + log.info("VirtualBox VM '{name}' [{id}]: starting packet capture on adapter {adapter_id}".format(name=self.name, + id=self.id, + adapter_id=adapter_id)) def stop_capture(self, adapter_id): """ @@ -856,6 +855,6 @@ class VirtualBoxVM(BaseVM): nio = adapter.get_nio(0) nio.stopPacketCapture() - log.info("VirtualBox VM '{name}' [{uuid}]: stopping packet capture on adapter {adapter_id}".format(name=self.name, - uuid=self.uuid, - adapter_id=adapter_id)) + log.info("VirtualBox VM '{name}' [{id}]: stopping packet capture on adapter {adapter_id}".format(name=self.name, + id=self.id, + adapter_id=adapter_id)) diff --git a/gns3server/modules/vpcs/__init__.py b/gns3server/modules/vpcs/__init__.py index 046eff7f..0741c0ab 100644 --- a/gns3server/modules/vpcs/__init__.py +++ b/gns3server/modules/vpcs/__init__.py @@ -38,28 +38,28 @@ class VPCS(BaseManager): def create_vm(self, *args, **kwargs): vm = yield from super().create_vm(*args, **kwargs) - self._free_mac_ids.setdefault(vm.project.uuid, list(range(0, 255))) + self._free_mac_ids.setdefault(vm.project.id, list(range(0, 255))) try: - self._used_mac_ids[vm.uuid] = self._free_mac_ids[vm.project.uuid].pop(0) + self._used_mac_ids[vm.id] = self._free_mac_ids[vm.project.id].pop(0) except IndexError: raise VPCSError("No mac address available") return vm @asyncio.coroutine - def delete_vm(self, uuid, *args, **kwargs): + def delete_vm(self, vm_id, *args, **kwargs): - vm = self.get_vm(uuid) - i = self._used_mac_ids[uuid] - self._free_mac_ids[vm.project.uuid].insert(0, i) - del self._used_mac_ids[uuid] - yield from super().delete_vm(uuid, *args, **kwargs) + vm = self.get_vm(vm_id) + i = self._used_mac_ids[vm_id] + self._free_mac_ids[vm.project.id].insert(0, i) + del self._used_mac_ids[vm_id] + yield from super().delete_vm(vm_id, *args, **kwargs) - def get_mac_id(self, vm_uuid): + def get_mac_id(self, vm_id): """ Get an unique VPCS mac id - :param vm_uuid: UUID of the VPCS vm - :returns: VPCS Mac id + :param vm_id: ID of the VPCS VM + :returns: VPCS MAC id """ - return self._used_mac_ids.get(vm_uuid, 1) + return self._used_mac_ids.get(vm_id, 1) diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index ce930410..1f1fa796 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -45,7 +45,7 @@ class VPCSVM(BaseVM): VPCS vm implementation. :param name: name of this VPCS vm - :param uuid: VPCS instance UUID + :param vm_id: VPCS instance identifier :param project: Project instance :param manager: parent VM Manager :param console: TCP console port @@ -53,9 +53,9 @@ class VPCSVM(BaseVM): :param startup_script: Content of vpcs startup script file """ - def __init__(self, name, uuid, project, manager, console=None, script_file=None, startup_script=None): + def __init__(self, name, vm_id, project, manager, console=None, script_file=None, startup_script=None): - super().__init__(name, uuid, project, manager) + super().__init__(name, vm_id, project, manager) self._path = manager.config.get_section_config("VPCS").get("vpcs_path", "vpcs") self._console = console @@ -105,9 +105,9 @@ class VPCSVM(BaseVM): def __json__(self): return {"name": self.name, - "uuid": self.uuid, + "vm_id": self.id, "console": self._console, - "project_id": self.project.uuid, + "project_id": self.project.id, "script_file": self.script_file, "startup_script": self.startup_script} @@ -329,10 +329,10 @@ class VPCSVM(BaseVM): port_number=port_number)) self._ethernet_adapter.add_nio(port_number, nio) - log.info("VPCS {name} {uuid}]: {nio} added to port {port_number}".format(name=self._name, - uuid=self.uuid, - nio=nio, - port_number=port_number)) + log.info("VPCS {name} {id}]: {nio} added to port {port_number}".format(name=self._name, + id=self.id, + nio=nio, + port_number=port_number)) return nio def port_remove_nio_binding(self, port_number): @@ -353,10 +353,10 @@ class VPCSVM(BaseVM): self.manager.port_manager.release_udp_port(nio.lport) self._ethernet_adapter.remove_nio(port_number) - log.info("VPCS {name} [{uuid}]: {nio} removed from port {port_number}".format(name=self._name, - uuid=self.uuid, - nio=nio, - port_number=port_number)) + log.info("VPCS {name} [{id}]: {nio} removed from port {port_number}".format(name=self._name, + id=self.id, + nio=nio, + port_number=port_number)) return nio def _build_command(self): @@ -411,7 +411,7 @@ class VPCSVM(BaseVM): command.extend(["-e"]) command.extend(["-d", nio.tap_vm]) - command.extend(["-m", str(self._manager.get_mac_id(self._uuid))]) # the unique ID is used to set the MAC address offset + command.extend(["-m", str(self._manager.get_mac_id(self.id))]) # the unique ID is used to set the MAC address offset command.extend(["-i", "1"]) # option to start only one VPC instance command.extend(["-F"]) # option to avoid the daemonization of VPCS if self._script_file: diff --git a/gns3server/schemas/virtualbox.py b/gns3server/schemas/virtualbox.py index 1898a280..e2fe70aa 100644 --- a/gns3server/schemas/virtualbox.py +++ b/gns3server/schemas/virtualbox.py @@ -21,16 +21,12 @@ VBOX_CREATE_SCHEMA = { "description": "Request validation to create a new VirtualBox VM instance", "type": "object", "properties": { - "uuid": { - "description": "VirtualBox VM instance UUID", + "vm_id": { + "description": "VirtualBox VM instance identifier", "type": "string", "minLength": 36, "maxLength": 36, - "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" - }, - "vbox_id": { - "description": "VirtualBox VM instance ID (for project created before GNS3 1.3)", - "type": "integer" + "pattern": "(^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}|\d+)$" }, "project_id": { "description": "Project UUID", @@ -204,7 +200,7 @@ VBOX_OBJECT_SCHEMA = { "type": "string", "minLength": 1, }, - "uuid": { + "vm_id": { "description": "VirtualBox VM instance UUID", "type": "string", "minLength": 36, @@ -256,5 +252,5 @@ VBOX_OBJECT_SCHEMA = { }, }, "additionalProperties": False, - "required": ["name", "uuid", "project_id"] + "required": ["name", "vm_id", "project_id"] } diff --git a/gns3server/schemas/vpcs.py b/gns3server/schemas/vpcs.py index fae10fe9..30238901 100644 --- a/gns3server/schemas/vpcs.py +++ b/gns3server/schemas/vpcs.py @@ -22,20 +22,16 @@ VPCS_CREATE_SCHEMA = { "type": "object", "properties": { "name": { - "description": "VPCS device name", + "description": "VPCS VM name", "type": "string", "minLength": 1, }, - "vpcs_id": { - "description": "VPCS device instance ID (for project created before GNS3 1.3)", - "type": "integer" - }, - "uuid": { - "description": "VPCS device UUID", + "vm_id": { + "description": "VPCS VM identifier", "type": "string", "minLength": 36, "maxLength": 36, - "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + "pattern": "^([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}|\d+)$" }, "project_id": { "description": "Project UUID", @@ -65,7 +61,7 @@ VPCS_UPDATE_SCHEMA = { "type": "object", "properties": { "name": { - "description": "VPCS device name", + "description": "VPCS VM name", "type": ["string", "null"], "minLength": 1, }, @@ -145,12 +141,12 @@ VPCS_OBJECT_SCHEMA = { "type": "object", "properties": { "name": { - "description": "VPCS device name", + "description": "VPCS VM name", "type": "string", "minLength": 1, }, - "uuid": { - "description": "VPCS device UUID", + "vm_id": { + "description": "VPCS VM UUID", "type": "string", "minLength": 36, "maxLength": 36, @@ -179,5 +175,5 @@ VPCS_OBJECT_SCHEMA = { }, }, "additionalProperties": False, - "required": ["name", "uuid", "console", "project_id"] + "required": ["name", "vm_id", "console", "project_id"] } diff --git a/tests/api/test_project.py b/tests/api/test_project.py index ec92a98d..50adb8c9 100644 --- a/tests/api/test_project.py +++ b/tests/api/test_project.py @@ -80,7 +80,7 @@ def test_update_temporary_project(server): def test_commit_project(server, project): with asyncio_patch("gns3server.modules.project.Project.commit", return_value=True) as mock: - response = server.post("/projects/{project_id}/commit".format(project_id=project.uuid), example=True) + response = server.post("/projects/{project_id}/commit".format(project_id=project.id), example=True) assert response.status == 204 assert mock.called @@ -92,7 +92,7 @@ def test_commit_project_invalid_uuid(server): def test_delete_project(server, project): with asyncio_patch("gns3server.modules.project.Project.delete", return_value=True) as mock: - response = server.delete("/projects/{project_id}".format(project_id=project.uuid), example=True) + response = server.delete("/projects/{project_id}".format(project_id=project.id), example=True) assert response.status == 204 assert mock.called @@ -104,7 +104,7 @@ def test_delete_project_invalid_uuid(server): def test_close_project(server, project): with asyncio_patch("gns3server.modules.project.Project.close", return_value=True) as mock: - response = server.post("/projects/{project_id}/close".format(project_id=project.uuid), example=True) + response = server.post("/projects/{project_id}/close".format(project_id=project.id), example=True) assert response.status == 204 assert mock.called diff --git a/tests/api/test_virtualbox.py b/tests/api/test_virtualbox.py index 08783af3..0cf2cda9 100644 --- a/tests/api/test_virtualbox.py +++ b/tests/api/test_virtualbox.py @@ -22,10 +22,10 @@ from tests.utils import asyncio_patch @pytest.fixture(scope="module") def vm(server, project): with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.create", return_value=True) as mock: - response = server.post("/virtualbox", {"name": "VMTEST", - "vmname": "VMTEST", - "linked_clone": False, - "project_id": project.uuid}) + response = server.post("/virtualbox/vms", {"name": "VMTEST", + "vmname": "VMTEST", + "linked_clone": False, + "project_id": project.id}) assert mock.called assert response.status == 201 return response.json @@ -34,83 +34,83 @@ def vm(server, project): def test_vbox_create(server, project): with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.create", return_value=True): - response = server.post("/virtualbox", {"name": "VM1", - "vmname": "VM1", - "linked_clone": False, - "project_id": project.uuid}, + response = server.post("/virtualbox/vms", {"name": "VM1", + "vmname": "VM1", + "linked_clone": False, + "project_id": project.id}, example=True) assert response.status == 201 assert response.json["name"] == "VM1" - assert response.json["project_id"] == project.uuid + assert response.json["project_id"] == project.id def test_vbox_get(server, project, vm): - response = server.get("/virtualbox/{}".format(vm["uuid"]), example=True) + response = server.get("/virtualbox/vms/{}".format(vm["vm_id"]), example=True) assert response.status == 200 - assert response.route == "/virtualbox/{uuid}" + assert response.route == "/virtualbox/vms/{vm_id}" assert response.json["name"] == "VMTEST" - assert response.json["project_id"] == project.uuid + assert response.json["project_id"] == project.id def test_vbox_start(server, vm): with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.start", return_value=True) as mock: - response = server.post("/virtualbox/{}/start".format(vm["uuid"])) + response = server.post("/virtualbox/vms/{}/start".format(vm["vm_id"])) assert mock.called assert response.status == 204 def test_vbox_stop(server, vm): with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.stop", return_value=True) as mock: - response = server.post("/virtualbox/{}/stop".format(vm["uuid"])) + response = server.post("/virtualbox/vms/{}/stop".format(vm["vm_id"])) assert mock.called assert response.status == 204 def test_vbox_suspend(server, vm): with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.suspend", return_value=True) as mock: - response = server.post("/virtualbox/{}/suspend".format(vm["uuid"])) + response = server.post("/virtualbox/vms/{}/suspend".format(vm["vm_id"])) assert mock.called assert response.status == 204 def test_vbox_resume(server, vm): with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.resume", return_value=True) as mock: - response = server.post("/virtualbox/{}/resume".format(vm["uuid"])) + response = server.post("/virtualbox/vms/{}/resume".format(vm["vm_id"])) assert mock.called assert response.status == 204 def test_vbox_reload(server, vm): with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.reload", return_value=True) as mock: - response = server.post("/virtualbox/{}/reload".format(vm["uuid"])) + response = server.post("/virtualbox/vms/{}/reload".format(vm["vm_id"])) assert mock.called assert response.status == 204 def test_vbox_nio_create_udp(server, vm): - response = server.post("/virtualbox/{}/adapters/0/nio".format(vm["uuid"]), {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"}, + response = server.post("/virtualbox/vms/{}/adapters/0/nio".format(vm["vm_id"]), {"type": "nio_udp", + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}, example=True) assert response.status == 201 - assert response.route == "/virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio" + assert response.route == "/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/nio" assert response.json["type"] == "nio_udp" def test_vbox_delete_nio(server, vm): - server.post("/virtualbox/{}/adapters/0/nio".format(vm["uuid"]), {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"}) - response = server.delete("/virtualbox/{}/adapters/0/nio".format(vm["uuid"]), example=True) + server.post("/virtualbox/vms/{}/adapters/0/nio".format(vm["vm_id"]), {"type": "nio_udp", + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}) + response = server.delete("/virtualbox/vms/{}/adapters/0/nio".format(vm["vm_id"]), example=True) assert response.status == 204 - assert response.route == "/virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio" + assert response.route == "/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/nio" def test_vpcs_update(server, vm, free_console_port): - response = server.put("/virtualbox/{}".format(vm["uuid"]), {"name": "test", - "console": free_console_port}) + response = server.put("/virtualbox/vms/{}".format(vm["vm_id"]), {"name": "test", + "console": free_console_port}) assert response.status == 200 assert response.json["name"] == "test" assert response.json["console"] == free_console_port diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index 7304f143..afdb578e 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -23,108 +23,108 @@ from unittest.mock import patch @pytest.fixture(scope="module") def vm(server, project): - response = server.post("/vpcs", {"name": "PC TEST 1", "project_id": project.uuid}) + response = server.post("/vpcs/vms", {"name": "PC TEST 1", "project_id": project.id}) assert response.status == 201 return response.json def test_vpcs_create(server, project): - response = server.post("/vpcs", {"name": "PC TEST 1", "project_id": project.uuid}, example=True) + response = server.post("/vpcs/vms", {"name": "PC TEST 1", "project_id": project.id}, example=True) assert response.status == 201 - assert response.route == "/vpcs" + assert response.route == "/vpcs/vms" assert response.json["name"] == "PC TEST 1" - assert response.json["project_id"] == project.uuid + assert response.json["project_id"] == project.id assert response.json["script_file"] is None def test_vpcs_get(server, project, vm): - response = server.get("/vpcs/{}".format(vm["uuid"]), example=True) + response = server.get("/vpcs/vms/{}".format(vm["vm_id"]), example=True) assert response.status == 200 - assert response.route == "/vpcs/{uuid}" + assert response.route == "/vpcs/vms/{vm_id}" assert response.json["name"] == "PC TEST 1" - assert response.json["project_id"] == project.uuid + assert response.json["project_id"] == project.id def test_vpcs_create_startup_script(server, project): - response = server.post("/vpcs", {"name": "PC TEST 1", "project_id": project.uuid, "startup_script": "ip 192.168.1.2\necho TEST"}) + response = server.post("/vpcs/vms", {"name": "PC TEST 1", "project_id": project.id, "startup_script": "ip 192.168.1.2\necho TEST"}) assert response.status == 201 - assert response.route == "/vpcs" + assert response.route == "/vpcs/vms" assert response.json["name"] == "PC TEST 1" - assert response.json["project_id"] == project.uuid + assert response.json["project_id"] == project.id assert response.json["startup_script"] == "ip 192.168.1.2\necho TEST" def test_vpcs_create_port(server, project, free_console_port): - response = server.post("/vpcs", {"name": "PC TEST 1", "project_id": project.uuid, "console": free_console_port}) + response = server.post("/vpcs/vms", {"name": "PC TEST 1", "project_id": project.id, "console": free_console_port}) assert response.status == 201 - assert response.route == "/vpcs" + assert response.route == "/vpcs/vms" assert response.json["name"] == "PC TEST 1" - assert response.json["project_id"] == project.uuid + assert response.json["project_id"] == project.id assert response.json["console"] == free_console_port def test_vpcs_nio_create_udp(server, vm): - response = server.post("/vpcs/{}/ports/0/nio".format(vm["uuid"]), {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"}, + response = server.post("/vpcs/vms/{}/ports/0/nio".format(vm["vm_id"]), {"type": "nio_udp", + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}, example=True) assert response.status == 201 - assert response.route == "/vpcs/{uuid}/ports/{port_number:\d+}/nio" + assert response.route == "/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio" assert response.json["type"] == "nio_udp" def test_vpcs_nio_create_tap(server, vm): with patch("gns3server.modules.base_manager.BaseManager._has_privileged_access", return_value=True): - response = server.post("/vpcs/{}/ports/0/nio".format(vm["uuid"]), {"type": "nio_tap", - "tap_device": "test"}) + response = server.post("/vpcs/vms/{}/ports/0/nio".format(vm["vm_id"]), {"type": "nio_tap", + "tap_device": "test"}) assert response.status == 201 - assert response.route == "/vpcs/{uuid}/ports/{port_number:\d+}/nio" + assert response.route == "/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio" assert response.json["type"] == "nio_tap" def test_vpcs_delete_nio(server, vm): - server.post("/vpcs/{}/ports/0/nio".format(vm["uuid"]), {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"}) - response = server.delete("/vpcs/{}/ports/0/nio".format(vm["uuid"]), example=True) + server.post("/vpcs/vms/{}/ports/0/nio".format(vm["vm_id"]), {"type": "nio_udp", + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}) + response = server.delete("/vpcs/vms/{}/ports/0/nio".format(vm["vm_id"]), example=True) assert response.status == 204 - assert response.route == "/vpcs/{uuid}/ports/{port_number:\d+}/nio" + assert response.route == "/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio" def test_vpcs_start(server, vm): with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM.start", return_value=True) as mock: - response = server.post("/vpcs/{}/start".format(vm["uuid"])) + response = server.post("/vpcs/vms/{}/start".format(vm["vm_id"])) assert mock.called assert response.status == 204 def test_vpcs_stop(server, vm): with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM.stop", return_value=True) as mock: - response = server.post("/vpcs/{}/stop".format(vm["uuid"])) + response = server.post("/vpcs/vms/{}/stop".format(vm["vm_id"])) assert mock.called assert response.status == 204 def test_vpcs_reload(server, vm): with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM.reload", return_value=True) as mock: - response = server.post("/vpcs/{}/reload".format(vm["uuid"])) + response = server.post("/vpcs/vms/{}/reload".format(vm["vm_id"])) assert mock.called assert response.status == 204 def test_vpcs_delete(server, vm): with asyncio_patch("gns3server.modules.vpcs.VPCS.delete_vm", return_value=True) as mock: - response = server.delete("/vpcs/{}".format(vm["uuid"])) + response = server.delete("/vpcs/vms/{}".format(vm["vm_id"])) assert mock.called assert response.status == 204 def test_vpcs_update(server, vm, tmpdir, free_console_port): - response = server.put("/vpcs/{}".format(vm["uuid"]), {"name": "test", - "console": free_console_port, - "startup_script": "ip 192.168.1.1"}) + response = server.put("/vpcs/vms/{}".format(vm["vm_id"]), {"name": "test", + "console": free_console_port, + "startup_script": "ip 192.168.1.1"}) assert response.status == 200 assert response.json["name"] == "test" assert response.json["console"] == free_console_port diff --git a/tests/conftest.py b/tests/conftest.py index dec63d56..113022ee 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -84,7 +84,7 @@ def server(request, loop, port_manager): def project(): """A GNS3 lab""" - return ProjectManager.instance().create_project(uuid="a1e920ca-338a-4e9f-b363-aa607b09dd80") + return ProjectManager.instance().create_project(project_id="a1e920ca-338a-4e9f-b363-aa607b09dd80") @pytest.fixture(scope="session") diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py index 25919d68..203c0b4c 100644 --- a/tests/modules/test_project.py +++ b/tests/modules/test_project.py @@ -42,18 +42,18 @@ def vm(project, manager): def test_affect_uuid(): p = Project() - assert len(p.uuid) == 36 + assert len(p.id) == 36 - p = Project(uuid='00010203-0405-0607-0809-0a0b0c0d0e0f') - assert p.uuid == '00010203-0405-0607-0809-0a0b0c0d0e0f' + p = Project(project_id='00010203-0405-0607-0809-0a0b0c0d0e0f') + assert p.id == '00010203-0405-0607-0809-0a0b0c0d0e0f' def test_path(tmpdir): with patch("gns3server.config.Config.get_section_config", return_value={"local": True}): p = Project(location=str(tmpdir)) - assert p.path == os.path.join(str(tmpdir), p.uuid) - assert os.path.exists(os.path.join(str(tmpdir), p.uuid)) - assert os.path.exists(os.path.join(str(tmpdir), p.uuid, 'vms')) + assert p.path == os.path.join(str(tmpdir), p.id) + assert os.path.exists(os.path.join(str(tmpdir), p.id)) + assert os.path.exists(os.path.join(str(tmpdir), p.id, 'vms')) assert not os.path.exists(os.path.join(p.path, '.gns3_temporary')) @@ -79,14 +79,14 @@ def test_changing_location_not_allowed(tmpdir): def test_json(tmpdir): p = Project() - assert p.__json__() == {"location": p.location, "project_id": p.uuid, "temporary": False} + assert p.__json__() == {"location": p.location, "project_id": p.id, "temporary": False} def test_vm_working_directory(tmpdir, vm): with patch("gns3server.config.Config.get_section_config", return_value={"local": True}): p = Project(location=str(tmpdir)) assert os.path.exists(p.vm_working_directory(vm)) - assert os.path.exists(os.path.join(str(tmpdir), p.uuid, vm.module_name, vm.uuid)) + assert os.path.exists(os.path.join(str(tmpdir), p.id, vm.module_name, vm.id)) def test_mark_vm_for_destruction(vm): diff --git a/tests/modules/test_project_manager.py b/tests/modules/test_project_manager.py index 9276c6a2..0f2d6ed3 100644 --- a/tests/modules/test_project_manager.py +++ b/tests/modules/test_project_manager.py @@ -22,7 +22,7 @@ from gns3server.modules.project_manager import ProjectManager def test_create_project(): pm = ProjectManager.instance() - project = pm.create_project(uuid='00010203-0405-0607-0809-0a0b0c0d0e0f') + project = pm.create_project(project_id='00010203-0405-0607-0809-0a0b0c0d0e0f') assert project == pm.get_project('00010203-0405-0607-0809-0a0b0c0d0e0f') diff --git a/tests/modules/virtualbox/test_virtualbox_vm.py b/tests/modules/virtualbox/test_virtualbox_vm.py index 049be0d8..8b91431e 100644 --- a/tests/modules/virtualbox/test_virtualbox_vm.py +++ b/tests/modules/virtualbox/test_virtualbox_vm.py @@ -39,7 +39,7 @@ def vm(project, manager): def test_vm(project, manager): vm = VirtualBoxVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager, "test", False) assert vm.name == "test" - assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0f" + assert vm.id == "00010203-0405-0607-0809-0a0b0c0d0e0f" assert vm.vmname == "test" diff --git a/tests/modules/vpcs/test_vpcs_manager.py b/tests/modules/vpcs/test_vpcs_manager.py index eb90876e..c7194de3 100644 --- a/tests/modules/vpcs/test_vpcs_manager.py +++ b/tests/modules/vpcs/test_vpcs_manager.py @@ -30,17 +30,17 @@ def test_get_mac_id(loop, project, port_manager): VPCS._instance = None vpcs = VPCS.instance() vpcs.port_manager = port_manager - vm1_uuid = str(uuid.uuid4()) - vm2_uuid = str(uuid.uuid4()) - vm3_uuid = str(uuid.uuid4()) - loop.run_until_complete(vpcs.create_vm("PC 1", project.uuid, vm1_uuid)) - loop.run_until_complete(vpcs.create_vm("PC 2", project.uuid, vm2_uuid)) - assert vpcs.get_mac_id(vm1_uuid) == 0 - assert vpcs.get_mac_id(vm1_uuid) == 0 - assert vpcs.get_mac_id(vm2_uuid) == 1 - loop.run_until_complete(vpcs.delete_vm(vm1_uuid)) - loop.run_until_complete(vpcs.create_vm("PC 3", project.uuid, vm3_uuid)) - assert vpcs.get_mac_id(vm3_uuid) == 0 + vm1_id = str(uuid.uuid4()) + vm2_id = str(uuid.uuid4()) + vm3_id = str(uuid.uuid4()) + loop.run_until_complete(vpcs.create_vm("PC 1", project.id, vm1_id)) + loop.run_until_complete(vpcs.create_vm("PC 2", project.id, vm2_id)) + assert vpcs.get_mac_id(vm1_id) == 0 + assert vpcs.get_mac_id(vm1_id) == 0 + assert vpcs.get_mac_id(vm2_id) == 1 + loop.run_until_complete(vpcs.delete_vm(vm1_id)) + loop.run_until_complete(vpcs.create_vm("PC 3", project.id, vm3_id)) + assert vpcs.get_mac_id(vm3_id) == 0 def test_get_mac_id_multiple_project(loop, port_manager): @@ -48,17 +48,17 @@ def test_get_mac_id_multiple_project(loop, port_manager): VPCS._instance = None vpcs = VPCS.instance() vpcs.port_manager = port_manager - vm1_uuid = str(uuid.uuid4()) - vm2_uuid = str(uuid.uuid4()) - vm3_uuid = str(uuid.uuid4()) + vm1_id = str(uuid.uuid4()) + vm2_id = str(uuid.uuid4()) + vm3_id = str(uuid.uuid4()) project1 = ProjectManager.instance().create_project() project2 = ProjectManager.instance().create_project() - loop.run_until_complete(vpcs.create_vm("PC 1", project1.uuid, vm1_uuid)) - loop.run_until_complete(vpcs.create_vm("PC 2", project1.uuid, vm2_uuid)) - loop.run_until_complete(vpcs.create_vm("PC 2", project2.uuid, vm3_uuid)) - assert vpcs.get_mac_id(vm1_uuid) == 0 - assert vpcs.get_mac_id(vm2_uuid) == 1 - assert vpcs.get_mac_id(vm3_uuid) == 0 + loop.run_until_complete(vpcs.create_vm("PC 1", project1.id, vm1_id)) + loop.run_until_complete(vpcs.create_vm("PC 2", project1.id, vm2_id)) + loop.run_until_complete(vpcs.create_vm("PC 2", project2.id, vm3_id)) + assert vpcs.get_mac_id(vm1_id) == 0 + assert vpcs.get_mac_id(vm2_id) == 1 + assert vpcs.get_mac_id(vm3_id) == 0 def test_get_mac_id_no_id_available(loop, project, port_manager): @@ -68,6 +68,6 @@ def test_get_mac_id_no_id_available(loop, project, port_manager): vpcs.port_manager = port_manager with pytest.raises(VPCSError): for i in range(0, 256): - vm_uuid = str(uuid.uuid4()) - loop.run_until_complete(vpcs.create_vm("PC {}".format(i), project.uuid, vm_uuid)) - assert vpcs.get_mac_id(vm_uuid) == i + vm_id = str(uuid.uuid4()) + loop.run_until_complete(vpcs.create_vm("PC {}".format(i), project.id, vm_id)) + assert vpcs.get_mac_id(vm_id) == i diff --git a/tests/modules/vpcs/test_vpcs_vm.py b/tests/modules/vpcs/test_vpcs_vm.py index f6197aa4..9017d5d8 100644 --- a/tests/modules/vpcs/test_vpcs_vm.py +++ b/tests/modules/vpcs/test_vpcs_vm.py @@ -43,7 +43,7 @@ def vm(project, manager): def test_vm(project, manager): vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) assert vm.name == "test" - assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0f" + assert vm.id == "00010203-0405-0607-0809-0a0b0c0d0e0f" def test_vm_invalid_vpcs_version(loop, project, manager): @@ -54,7 +54,7 @@ def test_vm_invalid_vpcs_version(loop, project, manager): vm.port_add_nio_binding(0, nio) loop.run_until_complete(asyncio.async(vm.start())) assert vm.name == "test" - assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0f" + assert vm.id == "00010203-0405-0607-0809-0a0b0c0d0e0f" @patch("gns3server.config.Config.get_section_config", return_value={"vpcs_path": "/bin/test_fake"}) @@ -65,7 +65,7 @@ def test_vm_invalid_vpcs_path(project, manager, loop): vm.port_add_nio_binding(0, nio) loop.run_until_complete(asyncio.async(vm.start())) assert vm.name == "test" - assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0e" + assert vm.id == "00010203-0405-0607-0809-0a0b0c0d0e0e" def test_start(loop, vm): From 291fac70846a2ade813fe31cdb1fef0c21e125cd Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 4 Feb 2015 17:13:35 -0700 Subject: [PATCH 181/485] Add project_id in all VM calls. --- .../api/examples/delete_projectsprojectid.txt | 4 +- ...te_virtualboxuuidadaptersadapteriddnio.txt | 13 --- .../delete_vpcsuuidportsportnumberdnio.txt | 13 --- docs/api/examples/get_interfaces.txt | 50 +++------- docs/api/examples/get_projectsprojectid.txt | 7 +- docs/api/examples/get_version.txt | 4 +- docs/api/examples/get_virtualboxuuid.txt | 26 ----- docs/api/examples/get_vpcsuuid.txt | 22 ----- docs/api/examples/post_portsudp.txt | 4 +- .../examples/post_projectsprojectidclose.txt | 4 +- .../examples/post_projectsprojectidcommit.txt | 4 +- docs/api/examples/post_version.txt | 4 +- docs/api/examples/post_virtualbox.txt | 31 ------ ...st_virtualboxuuidadaptersadapteriddnio.txt | 25 ----- docs/api/examples/post_vpcs.txt | 25 ----- .../post_vpcsuuidportsportnumberdnio.txt | 25 ----- docs/api/examples/put_projectsprojectid.txt | 17 ++-- docs/api/v1projects.rst | 1 + docs/api/v1projectsprojectid.rst | 4 + docs/api/v1virtualbox.rst | 53 ---------- docs/api/v1virtualboxuuid.rst | 99 ------------------- .../v1virtualboxuuidadaptersadapteriddnio.rst | 36 ------- ...v1virtualboxuuidcaptureadapteriddstart.rst | 29 ------ .../v1virtualboxuuidcaptureadapteriddstop.rst | 20 ---- docs/api/v1virtualboxuuidreload.rst | 19 ---- docs/api/v1virtualboxuuidresume.rst | 19 ---- docs/api/v1virtualboxuuidstart.rst | 19 ---- docs/api/v1virtualboxuuidstop.rst | 19 ---- docs/api/v1virtualboxuuidsuspend.rst | 19 ---- docs/api/v1vpcs.rst | 43 -------- docs/api/v1vpcsuuid.rst | 86 ---------------- docs/api/v1vpcsuuidportsportnumberdnio.rst | 36 ------- docs/api/v1vpcsuuidreload.rst | 19 ---- docs/api/v1vpcsuuidstart.rst | 19 ---- docs/api/v1vpcsuuidstop.rst | 19 ---- gns3server/handlers/virtualbox_handler.py | 96 +++++++++++------- gns3server/handlers/vpcs_handler.py | 60 ++++++----- gns3server/modules/base_manager.py | 18 +++- gns3server/modules/project_manager.py | 2 +- gns3server/schemas/virtualbox.py | 9 +- gns3server/schemas/vpcs.py | 9 +- tests/api/test_virtualbox.py | 58 +++++------ tests/api/test_vpcs.py | 60 +++++------ 43 files changed, 215 insertions(+), 934 deletions(-) delete mode 100644 docs/api/examples/delete_virtualboxuuidadaptersadapteriddnio.txt delete mode 100644 docs/api/examples/delete_vpcsuuidportsportnumberdnio.txt delete mode 100644 docs/api/examples/get_virtualboxuuid.txt delete mode 100644 docs/api/examples/get_vpcsuuid.txt delete mode 100644 docs/api/examples/post_virtualbox.txt delete mode 100644 docs/api/examples/post_virtualboxuuidadaptersadapteriddnio.txt delete mode 100644 docs/api/examples/post_vpcs.txt delete mode 100644 docs/api/examples/post_vpcsuuidportsportnumberdnio.txt delete mode 100644 docs/api/v1virtualbox.rst delete mode 100644 docs/api/v1virtualboxuuid.rst delete mode 100644 docs/api/v1virtualboxuuidadaptersadapteriddnio.rst delete mode 100644 docs/api/v1virtualboxuuidcaptureadapteriddstart.rst delete mode 100644 docs/api/v1virtualboxuuidcaptureadapteriddstop.rst delete mode 100644 docs/api/v1virtualboxuuidreload.rst delete mode 100644 docs/api/v1virtualboxuuidresume.rst delete mode 100644 docs/api/v1virtualboxuuidstart.rst delete mode 100644 docs/api/v1virtualboxuuidstop.rst delete mode 100644 docs/api/v1virtualboxuuidsuspend.rst delete mode 100644 docs/api/v1vpcs.rst delete mode 100644 docs/api/v1vpcsuuid.rst delete mode 100644 docs/api/v1vpcsuuidportsportnumberdnio.rst delete mode 100644 docs/api/v1vpcsuuidreload.rst delete mode 100644 docs/api/v1vpcsuuidstart.rst delete mode 100644 docs/api/v1vpcsuuidstop.rst diff --git a/docs/api/examples/delete_projectsprojectid.txt b/docs/api/examples/delete_projectsprojectid.txt index 45efff6c..d48bdd35 100644 --- a/docs/api/examples/delete_projectsprojectid.txt +++ b/docs/api/examples/delete_projectsprojectid.txt @@ -5,9 +5,9 @@ DELETE /projects/{project_id} HTTP/1.1 HTTP/1.1 204 -CONNECTION: keep-alive +CONNECTION: close CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /v1/projects/{project_id} diff --git a/docs/api/examples/delete_virtualboxuuidadaptersadapteriddnio.txt b/docs/api/examples/delete_virtualboxuuidadaptersadapteriddnio.txt deleted file mode 100644 index cb1c3b9e..00000000 --- a/docs/api/examples/delete_virtualboxuuidadaptersadapteriddnio.txt +++ /dev/null @@ -1,13 +0,0 @@ -curl -i -X DELETE 'http://localhost:8000/virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio' - -DELETE /virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio HTTP/1.1 - - - -HTTP/1.1 204 -CONNECTION: keep-alive -CONTENT-LENGTH: 0 -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /v1/virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio - diff --git a/docs/api/examples/delete_vpcsuuidportsportnumberdnio.txt b/docs/api/examples/delete_vpcsuuidportsportnumberdnio.txt deleted file mode 100644 index 26ac313d..00000000 --- a/docs/api/examples/delete_vpcsuuidportsportnumberdnio.txt +++ /dev/null @@ -1,13 +0,0 @@ -curl -i -X DELETE 'http://localhost:8000/vpcs/{uuid}/ports/{port_number:\d+}/nio' - -DELETE /vpcs/{uuid}/ports/{port_number:\d+}/nio HTTP/1.1 - - - -HTTP/1.1 204 -CONNECTION: keep-alive -CONTENT-LENGTH: 0 -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /v1/vpcs/{uuid}/ports/{port_number:\d+}/nio - diff --git a/docs/api/examples/get_interfaces.txt b/docs/api/examples/get_interfaces.txt index 7ac8c74d..f0566cea 100644 --- a/docs/api/examples/get_interfaces.txt +++ b/docs/api/examples/get_interfaces.txt @@ -5,56 +5,32 @@ GET /interfaces HTTP/1.1 HTTP/1.1 200 -CONNECTION: keep-alive -CONTENT-LENGTH: 652 +CONNECTION: close +CONTENT-LENGTH: 298 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /v1/interfaces [ { - "id": "lo0", - "name": "lo0" + "id": "lo", + "name": "lo" }, { - "id": "gif0", - "name": "gif0" + "id": "eth0", + "name": "eth0" }, { - "id": "stf0", - "name": "stf0" + "id": "wlan0", + "name": "wlan0" }, { - "id": "en0", - "name": "en0" + "id": "vmnet1", + "name": "vmnet1" }, { - "id": "en1", - "name": "en1" - }, - { - "id": "fw0", - "name": "fw0" - }, - { - "id": "en2", - "name": "en2" - }, - { - "id": "p2p0", - "name": "p2p0" - }, - { - "id": "bridge0", - "name": "bridge0" - }, - { - "id": "vboxnet0", - "name": "vboxnet0" - }, - { - "id": "vboxnet1", - "name": "vboxnet1" + "id": "vmnet8", + "name": "vmnet8" } ] diff --git a/docs/api/examples/get_projectsprojectid.txt b/docs/api/examples/get_projectsprojectid.txt index 152a815a..035e8487 100644 --- a/docs/api/examples/get_projectsprojectid.txt +++ b/docs/api/examples/get_projectsprojectid.txt @@ -5,15 +5,16 @@ GET /projects/{project_id} HTTP/1.1 HTTP/1.1 200 -CONNECTION: keep-alive -CONTENT-LENGTH: 108 +CONNECTION: close +CONTENT-LENGTH: 165 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /v1/projects/{project_id} { "location": "/tmp", + "path": "/tmp/00010203-0405-0607-0809-0a0b0c0d0e0f", "project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f", "temporary": false } diff --git a/docs/api/examples/get_version.txt b/docs/api/examples/get_version.txt index 88017034..ddf810d1 100644 --- a/docs/api/examples/get_version.txt +++ b/docs/api/examples/get_version.txt @@ -5,11 +5,11 @@ GET /version HTTP/1.1 HTTP/1.1 200 -CONNECTION: keep-alive +CONNECTION: close CONTENT-LENGTH: 29 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /v1/version { diff --git a/docs/api/examples/get_virtualboxuuid.txt b/docs/api/examples/get_virtualboxuuid.txt deleted file mode 100644 index 11853488..00000000 --- a/docs/api/examples/get_virtualboxuuid.txt +++ /dev/null @@ -1,26 +0,0 @@ -curl -i -X GET 'http://localhost:8000/virtualbox/{uuid}' - -GET /virtualbox/{uuid} HTTP/1.1 - - - -HTTP/1.1 200 -CONNECTION: keep-alive -CONTENT-LENGTH: 346 -CONTENT-TYPE: application/json -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /v1/virtualbox/{uuid} - -{ - "adapter_start_index": 0, - "adapter_type": "Intel PRO/1000 MT Desktop (82540EM)", - "adapters": 0, - "console": 2001, - "enable_remote_console": false, - "headless": false, - "name": "VMTEST", - "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "uuid": "aa7a7c1e-6fc2-43b8-b53b-60e605565625", - "vmname": "VMTEST" -} diff --git a/docs/api/examples/get_vpcsuuid.txt b/docs/api/examples/get_vpcsuuid.txt deleted file mode 100644 index 19f913bc..00000000 --- a/docs/api/examples/get_vpcsuuid.txt +++ /dev/null @@ -1,22 +0,0 @@ -curl -i -X GET 'http://localhost:8000/vpcs/{uuid}' - -GET /vpcs/{uuid} HTTP/1.1 - - - -HTTP/1.1 200 -CONNECTION: keep-alive -CONTENT-LENGTH: 211 -CONTENT-TYPE: application/json -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /v1/vpcs/{uuid} - -{ - "console": 2003, - "name": "PC TEST 1", - "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "script_file": null, - "startup_script": null, - "uuid": "c505f88b-7fe1-4985-9aca-a3798f6659ab" -} diff --git a/docs/api/examples/post_portsudp.txt b/docs/api/examples/post_portsudp.txt index 3be4b74c..828df022 100644 --- a/docs/api/examples/post_portsudp.txt +++ b/docs/api/examples/post_portsudp.txt @@ -5,11 +5,11 @@ POST /ports/udp HTTP/1.1 HTTP/1.1 201 -CONNECTION: keep-alive +CONNECTION: close CONTENT-LENGTH: 25 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /v1/ports/udp { diff --git a/docs/api/examples/post_projectsprojectidclose.txt b/docs/api/examples/post_projectsprojectidclose.txt index bcc429c9..66366b9c 100644 --- a/docs/api/examples/post_projectsprojectidclose.txt +++ b/docs/api/examples/post_projectsprojectidclose.txt @@ -5,9 +5,9 @@ POST /projects/{project_id}/close HTTP/1.1 HTTP/1.1 204 -CONNECTION: keep-alive +CONNECTION: close CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /v1/projects/{project_id}/close diff --git a/docs/api/examples/post_projectsprojectidcommit.txt b/docs/api/examples/post_projectsprojectidcommit.txt index 0b36f05d..b17f5a85 100644 --- a/docs/api/examples/post_projectsprojectidcommit.txt +++ b/docs/api/examples/post_projectsprojectidcommit.txt @@ -5,9 +5,9 @@ POST /projects/{project_id}/commit HTTP/1.1 HTTP/1.1 204 -CONNECTION: keep-alive +CONNECTION: close CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /v1/projects/{project_id}/commit diff --git a/docs/api/examples/post_version.txt b/docs/api/examples/post_version.txt index 2f6c1452..b6f654df 100644 --- a/docs/api/examples/post_version.txt +++ b/docs/api/examples/post_version.txt @@ -7,11 +7,11 @@ POST /version HTTP/1.1 HTTP/1.1 200 -CONNECTION: keep-alive +CONNECTION: close CONTENT-LENGTH: 29 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /v1/version { diff --git a/docs/api/examples/post_virtualbox.txt b/docs/api/examples/post_virtualbox.txt deleted file mode 100644 index 06dec3c8..00000000 --- a/docs/api/examples/post_virtualbox.txt +++ /dev/null @@ -1,31 +0,0 @@ -curl -i -X POST 'http://localhost:8000/virtualbox' -d '{"linked_clone": false, "name": "VM1", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "vmname": "VM1"}' - -POST /virtualbox HTTP/1.1 -{ - "linked_clone": false, - "name": "VM1", - "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "vmname": "VM1" -} - - -HTTP/1.1 201 -CONNECTION: keep-alive -CONTENT-LENGTH: 340 -CONTENT-TYPE: application/json -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /v1/virtualbox - -{ - "adapter_start_index": 0, - "adapter_type": "Intel PRO/1000 MT Desktop (82540EM)", - "adapters": 0, - "console": 2000, - "enable_remote_console": false, - "headless": false, - "name": "VM1", - "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "uuid": "267f3908-f3dd-4cac-bca3-2e31a1018493", - "vmname": "VM1" -} diff --git a/docs/api/examples/post_virtualboxuuidadaptersadapteriddnio.txt b/docs/api/examples/post_virtualboxuuidadaptersadapteriddnio.txt deleted file mode 100644 index 527a758e..00000000 --- a/docs/api/examples/post_virtualboxuuidadaptersadapteriddnio.txt +++ /dev/null @@ -1,25 +0,0 @@ -curl -i -X POST 'http://localhost:8000/virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio' -d '{"lport": 4242, "rhost": "127.0.0.1", "rport": 4343, "type": "nio_udp"}' - -POST /virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio HTTP/1.1 -{ - "lport": 4242, - "rhost": "127.0.0.1", - "rport": 4343, - "type": "nio_udp" -} - - -HTTP/1.1 201 -CONNECTION: keep-alive -CONTENT-LENGTH: 89 -CONTENT-TYPE: application/json -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /v1/virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio - -{ - "lport": 4242, - "rhost": "127.0.0.1", - "rport": 4343, - "type": "nio_udp" -} diff --git a/docs/api/examples/post_vpcs.txt b/docs/api/examples/post_vpcs.txt deleted file mode 100644 index c9ff1f35..00000000 --- a/docs/api/examples/post_vpcs.txt +++ /dev/null @@ -1,25 +0,0 @@ -curl -i -X POST 'http://localhost:8000/vpcs' -d '{"name": "PC TEST 1", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80"}' - -POST /vpcs HTTP/1.1 -{ - "name": "PC TEST 1", - "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80" -} - - -HTTP/1.1 201 -CONNECTION: keep-alive -CONTENT-LENGTH: 211 -CONTENT-TYPE: application/json -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /v1/vpcs - -{ - "console": 2001, - "name": "PC TEST 1", - "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "script_file": null, - "startup_script": null, - "uuid": "d7a9ef38-2863-4cf2-97ce-dcf8416a93ae" -} diff --git a/docs/api/examples/post_vpcsuuidportsportnumberdnio.txt b/docs/api/examples/post_vpcsuuidportsportnumberdnio.txt deleted file mode 100644 index 83bf344c..00000000 --- a/docs/api/examples/post_vpcsuuidportsportnumberdnio.txt +++ /dev/null @@ -1,25 +0,0 @@ -curl -i -X POST 'http://localhost:8000/vpcs/{uuid}/ports/{port_number:\d+}/nio' -d '{"lport": 4242, "rhost": "127.0.0.1", "rport": 4343, "type": "nio_udp"}' - -POST /vpcs/{uuid}/ports/{port_number:\d+}/nio HTTP/1.1 -{ - "lport": 4242, - "rhost": "127.0.0.1", - "rport": 4343, - "type": "nio_udp" -} - - -HTTP/1.1 201 -CONNECTION: keep-alive -CONTENT-LENGTH: 89 -CONTENT-TYPE: application/json -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /v1/vpcs/{uuid}/ports/{port_number:\d+}/nio - -{ - "lport": 4242, - "rhost": "127.0.0.1", - "rport": 4343, - "type": "nio_udp" -} diff --git a/docs/api/examples/put_projectsprojectid.txt b/docs/api/examples/put_projectsprojectid.txt index e0366b62..b4043f7e 100644 --- a/docs/api/examples/put_projectsprojectid.txt +++ b/docs/api/examples/put_projectsprojectid.txt @@ -1,21 +1,20 @@ -curl -i -X PUT 'http://localhost:8000/projects/{project_id}' -d '{"temporary": false}' +curl -i -X PUT 'http://localhost:8000/projects/{project_id}' -d '{"path": "/tmp/pytest-42/test_update_path_project_non_l0"}' PUT /projects/{project_id} HTTP/1.1 { - "temporary": false + "path": "/tmp/pytest-42/test_update_path_project_non_l0" } -HTTP/1.1 200 -CONNECTION: keep-alive -CONTENT-LENGTH: 164 +HTTP/1.1 403 +CONNECTION: close +CONTENT-LENGTH: 101 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 +SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /v1/projects/{project_id} { - "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmptf8_s67s", - "project_id": "12d03846-c355-4da9-b708-cd45e5d30a50", - "temporary": false + "message": "You are not allowed to modifiy the project directory location", + "status": 403 } diff --git a/docs/api/v1projects.rst b/docs/api/v1projects.rst index 303c52a9..c1fa393c 100644 --- a/docs/api/v1projects.rst +++ b/docs/api/v1projects.rst @@ -29,6 +29,7 @@ Output +
Name Mandatory Type Description
location string Base directory where the project should be created on remote server
path string Directory of the project on the server
project_id string Project UUID
temporary boolean If project is a temporary project
diff --git a/docs/api/v1projectsprojectid.rst b/docs/api/v1projectsprojectid.rst index 0b872074..0ad7f43e 100644 --- a/docs/api/v1projectsprojectid.rst +++ b/docs/api/v1projectsprojectid.rst @@ -23,6 +23,7 @@ Output +
Name Mandatory Type Description
location string Base directory where the project should be created on remote server
path string Directory of the project on the server
project_id string Project UUID
temporary boolean If project is a temporary project
@@ -39,6 +40,7 @@ Parameters Response status codes ********************** - **200**: The project has been updated +- **403**: You are not allowed to modify this property - **404**: The project doesn't exist Input @@ -47,6 +49,7 @@ Input +
Name Mandatory Type Description
path ['string', 'null'] Path of the project on the server (work only with --local)
temporary boolean If project is a temporary project
@@ -57,6 +60,7 @@ Output +
Name Mandatory Type Description
location string Base directory where the project should be created on remote server
path string Directory of the project on the server
project_id string Project UUID
temporary boolean If project is a temporary project
diff --git a/docs/api/v1virtualbox.rst b/docs/api/v1virtualbox.rst deleted file mode 100644 index 754badb1..00000000 --- a/docs/api/v1virtualbox.rst +++ /dev/null @@ -1,53 +0,0 @@ -/v1/virtualbox ------------------------------------------------------------ - -.. contents:: - -POST /v1/virtualbox -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Create a new VirtualBox VM instance - -Response status codes -********************** -- **400**: Invalid project UUID -- **201**: Instance created -- **409**: Conflict - -Input -******* -.. raw:: html - - - - - - - - - - - - - - - -
Name Mandatory Type Description
adapter_start_index integer adapter index from which to start using adapters
adapter_type string VirtualBox adapter type
adapters integer number of adapters
console integer console TCP port
enable_remote_console boolean enable the remote console
headless boolean headless mode
linked_clone boolean either the VM is a linked clone or not
name string VirtualBox VM instance name
project_id string Project UUID
uuid string VirtualBox VM instance UUID
vbox_id integer VirtualBox VM instance ID (for project created before GNS3 1.3)
vmname string VirtualBox VM name (in VirtualBox itself)
- -Output -******* -.. raw:: html - - - - - - - - - - - - - -
Name Mandatory Type Description
adapter_start_index integer adapter index from which to start using adapters
adapter_type string VirtualBox adapter type
adapters integer number of adapters
console integer console TCP port
enable_remote_console boolean enable the remote console
headless boolean headless mode
name string VirtualBox VM instance name
project_id string Project UUID
uuid string VirtualBox VM instance UUID
vmname string VirtualBox VM name (in VirtualBox itself)
- diff --git a/docs/api/v1virtualboxuuid.rst b/docs/api/v1virtualboxuuid.rst deleted file mode 100644 index 90b30f7c..00000000 --- a/docs/api/v1virtualboxuuid.rst +++ /dev/null @@ -1,99 +0,0 @@ -/v1/virtualbox/{uuid} ------------------------------------------------------------ - -.. contents:: - -GET /v1/virtualbox/**{uuid}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Get a VirtualBox VM instance - -Parameters -********** -- **uuid**: Instance UUID - -Response status codes -********************** -- **200**: Success -- **404**: Instance doesn't exist - -Output -******* -.. raw:: html - - - - - - - - - - - - - -
Name Mandatory Type Description
adapter_start_index integer adapter index from which to start using adapters
adapter_type string VirtualBox adapter type
adapters integer number of adapters
console integer console TCP port
enable_remote_console boolean enable the remote console
headless boolean headless mode
name string VirtualBox VM instance name
project_id string Project UUID
uuid string VirtualBox VM instance UUID
vmname string VirtualBox VM name (in VirtualBox itself)
- - -PUT /v1/virtualbox/**{uuid}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Update a VirtualBox VM instance - -Parameters -********** -- **uuid**: Instance UUID - -Response status codes -********************** -- **200**: Instance updated -- **409**: Conflict -- **404**: Instance doesn't exist - -Input -******* -.. raw:: html - - - - - - - - - - - -
Name Mandatory Type Description
adapter_start_index integer adapter index from which to start using adapters
adapter_type string VirtualBox adapter type
adapters integer number of adapters
console integer console TCP port
enable_remote_console boolean enable the remote console
headless boolean headless mode
name string VirtualBox VM instance name
vmname string VirtualBox VM name (in VirtualBox itself)
- -Output -******* -.. raw:: html - - - - - - - - - - - - - -
Name Mandatory Type Description
adapter_start_index integer adapter index from which to start using adapters
adapter_type string VirtualBox adapter type
adapters integer number of adapters
console integer console TCP port
enable_remote_console boolean enable the remote console
headless boolean headless mode
name string VirtualBox VM instance name
project_id string Project UUID
uuid string VirtualBox VM instance UUID
vmname string VirtualBox VM name (in VirtualBox itself)
- - -DELETE /v1/virtualbox/**{uuid}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Delete a VirtualBox VM instance - -Parameters -********** -- **uuid**: Instance UUID - -Response status codes -********************** -- **404**: Instance doesn't exist -- **204**: Instance deleted - diff --git a/docs/api/v1virtualboxuuidadaptersadapteriddnio.rst b/docs/api/v1virtualboxuuidadaptersadapteriddnio.rst deleted file mode 100644 index dc6649f7..00000000 --- a/docs/api/v1virtualboxuuidadaptersadapteriddnio.rst +++ /dev/null @@ -1,36 +0,0 @@ -/v1/virtualbox/{uuid}/adapters/{adapter_id:\d+}/nio ------------------------------------------------------------ - -.. contents:: - -POST /v1/virtualbox/**{uuid}**/adapters/**{adapter_id:\d+}**/nio -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Add a NIO to a VirtualBox VM instance - -Parameters -********** -- **uuid**: Instance UUID -- **adapter_id**: Adapter where the nio should be added - -Response status codes -********************** -- **400**: Invalid instance UUID -- **201**: NIO created -- **404**: Instance doesn't exist - - -DELETE /v1/virtualbox/**{uuid}**/adapters/**{adapter_id:\d+}**/nio -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Remove a NIO from a VirtualBox VM instance - -Parameters -********** -- **uuid**: Instance UUID -- **adapter_id**: Adapter from where the nio should be removed - -Response status codes -********************** -- **400**: Invalid instance UUID -- **404**: Instance doesn't exist -- **204**: NIO deleted - diff --git a/docs/api/v1virtualboxuuidcaptureadapteriddstart.rst b/docs/api/v1virtualboxuuidcaptureadapteriddstart.rst deleted file mode 100644 index db6f0f61..00000000 --- a/docs/api/v1virtualboxuuidcaptureadapteriddstart.rst +++ /dev/null @@ -1,29 +0,0 @@ -/v1/virtualbox/{uuid}/capture/{adapter_id:\d+}/start ------------------------------------------------------------ - -.. contents:: - -POST /v1/virtualbox/**{uuid}**/capture/**{adapter_id:\d+}**/start -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Start a packet capture on a VirtualBox VM instance - -Parameters -********** -- **uuid**: Instance UUID -- **adapter_id**: Adapter to start a packet capture - -Response status codes -********************** -- **200**: Capture started -- **400**: Invalid instance UUID -- **404**: Instance doesn't exist - -Input -******* -.. raw:: html - - - - -
Name Mandatory Type Description
capture_filename string Capture file name
- diff --git a/docs/api/v1virtualboxuuidcaptureadapteriddstop.rst b/docs/api/v1virtualboxuuidcaptureadapteriddstop.rst deleted file mode 100644 index eacc8212..00000000 --- a/docs/api/v1virtualboxuuidcaptureadapteriddstop.rst +++ /dev/null @@ -1,20 +0,0 @@ -/v1/virtualbox/{uuid}/capture/{adapter_id:\d+}/stop ------------------------------------------------------------ - -.. contents:: - -POST /v1/virtualbox/**{uuid}**/capture/**{adapter_id:\d+}**/stop -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Stop a packet capture on a VirtualBox VM instance - -Parameters -********** -- **uuid**: Instance UUID -- **adapter_id**: Adapter to stop a packet capture - -Response status codes -********************** -- **400**: Invalid instance UUID -- **404**: Instance doesn't exist -- **204**: Capture stopped - diff --git a/docs/api/v1virtualboxuuidreload.rst b/docs/api/v1virtualboxuuidreload.rst deleted file mode 100644 index 3ad6c3b7..00000000 --- a/docs/api/v1virtualboxuuidreload.rst +++ /dev/null @@ -1,19 +0,0 @@ -/v1/virtualbox/{uuid}/reload ------------------------------------------------------------ - -.. contents:: - -POST /v1/virtualbox/**{uuid}**/reload -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Reload a VirtualBox VM instance - -Parameters -********** -- **uuid**: Instance UUID - -Response status codes -********************** -- **400**: Invalid instance UUID -- **404**: Instance doesn't exist -- **204**: Instance reloaded - diff --git a/docs/api/v1virtualboxuuidresume.rst b/docs/api/v1virtualboxuuidresume.rst deleted file mode 100644 index 97bfdbf8..00000000 --- a/docs/api/v1virtualboxuuidresume.rst +++ /dev/null @@ -1,19 +0,0 @@ -/v1/virtualbox/{uuid}/resume ------------------------------------------------------------ - -.. contents:: - -POST /v1/virtualbox/**{uuid}**/resume -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Resume a suspended VirtualBox VM instance - -Parameters -********** -- **uuid**: Instance UUID - -Response status codes -********************** -- **400**: Invalid instance UUID -- **404**: Instance doesn't exist -- **204**: Instance resumed - diff --git a/docs/api/v1virtualboxuuidstart.rst b/docs/api/v1virtualboxuuidstart.rst deleted file mode 100644 index 178ed7d4..00000000 --- a/docs/api/v1virtualboxuuidstart.rst +++ /dev/null @@ -1,19 +0,0 @@ -/v1/virtualbox/{uuid}/start ------------------------------------------------------------ - -.. contents:: - -POST /v1/virtualbox/**{uuid}**/start -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Start a VirtualBox VM instance - -Parameters -********** -- **uuid**: Instance UUID - -Response status codes -********************** -- **400**: Invalid instance UUID -- **404**: Instance doesn't exist -- **204**: Instance started - diff --git a/docs/api/v1virtualboxuuidstop.rst b/docs/api/v1virtualboxuuidstop.rst deleted file mode 100644 index 8cbe9441..00000000 --- a/docs/api/v1virtualboxuuidstop.rst +++ /dev/null @@ -1,19 +0,0 @@ -/v1/virtualbox/{uuid}/stop ------------------------------------------------------------ - -.. contents:: - -POST /v1/virtualbox/**{uuid}**/stop -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Stop a VirtualBox VM instance - -Parameters -********** -- **uuid**: Instance UUID - -Response status codes -********************** -- **400**: Invalid instance UUID -- **404**: Instance doesn't exist -- **204**: Instance stopped - diff --git a/docs/api/v1virtualboxuuidsuspend.rst b/docs/api/v1virtualboxuuidsuspend.rst deleted file mode 100644 index 3c14bc16..00000000 --- a/docs/api/v1virtualboxuuidsuspend.rst +++ /dev/null @@ -1,19 +0,0 @@ -/v1/virtualbox/{uuid}/suspend ------------------------------------------------------------ - -.. contents:: - -POST /v1/virtualbox/**{uuid}**/suspend -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Suspend a VirtualBox VM instance - -Parameters -********** -- **uuid**: Instance UUID - -Response status codes -********************** -- **400**: Invalid instance UUID -- **404**: Instance doesn't exist -- **204**: Instance suspended - diff --git a/docs/api/v1vpcs.rst b/docs/api/v1vpcs.rst deleted file mode 100644 index 3e26b9ce..00000000 --- a/docs/api/v1vpcs.rst +++ /dev/null @@ -1,43 +0,0 @@ -/v1/vpcs ------------------------------------------------------------ - -.. contents:: - -POST /v1/vpcs -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Create a new VPCS instance - -Response status codes -********************** -- **400**: Invalid project UUID -- **201**: Instance created -- **409**: Conflict - -Input -******* -.. raw:: html - - - - - - - - - -
Name Mandatory Type Description
console ['integer', 'null'] console TCP port
name string VPCS device name
project_id string Project UUID
startup_script ['string', 'null'] Content of the VPCS startup script
uuid string VPCS device UUID
vpcs_id integer VPCS device instance ID (for project created before GNS3 1.3)
- -Output -******* -.. raw:: html - - - - - - - - - -
Name Mandatory Type Description
console integer console TCP port
name string VPCS device name
project_id string Project UUID
script_file ['string', 'null'] VPCS startup script
startup_script ['string', 'null'] Content of the VPCS startup script
uuid string VPCS device UUID
- diff --git a/docs/api/v1vpcsuuid.rst b/docs/api/v1vpcsuuid.rst deleted file mode 100644 index 0bc9e4e5..00000000 --- a/docs/api/v1vpcsuuid.rst +++ /dev/null @@ -1,86 +0,0 @@ -/v1/vpcs/{uuid} ------------------------------------------------------------ - -.. contents:: - -GET /v1/vpcs/**{uuid}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Get a VPCS instance - -Parameters -********** -- **uuid**: Instance UUID - -Response status codes -********************** -- **200**: Success -- **404**: Instance doesn't exist - -Output -******* -.. raw:: html - - - - - - - - - -
Name Mandatory Type Description
console integer console TCP port
name string VPCS device name
project_id string Project UUID
script_file ['string', 'null'] VPCS startup script
startup_script ['string', 'null'] Content of the VPCS startup script
uuid string VPCS device UUID
- - -PUT /v1/vpcs/**{uuid}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Update a VPCS instance - -Parameters -********** -- **uuid**: Instance UUID - -Response status codes -********************** -- **200**: Instance updated -- **409**: Conflict -- **404**: Instance doesn't exist - -Input -******* -.. raw:: html - - - - - - -
Name Mandatory Type Description
console ['integer', 'null'] console TCP port
name ['string', 'null'] VPCS device name
startup_script ['string', 'null'] Content of the VPCS startup script
- -Output -******* -.. raw:: html - - - - - - - - - -
Name Mandatory Type Description
console integer console TCP port
name string VPCS device name
project_id string Project UUID
script_file ['string', 'null'] VPCS startup script
startup_script ['string', 'null'] Content of the VPCS startup script
uuid string VPCS device UUID
- - -DELETE /v1/vpcs/**{uuid}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Delete a VPCS instance - -Parameters -********** -- **uuid**: Instance UUID - -Response status codes -********************** -- **404**: Instance doesn't exist -- **204**: Instance deleted - diff --git a/docs/api/v1vpcsuuidportsportnumberdnio.rst b/docs/api/v1vpcsuuidportsportnumberdnio.rst deleted file mode 100644 index 72a5b999..00000000 --- a/docs/api/v1vpcsuuidportsportnumberdnio.rst +++ /dev/null @@ -1,36 +0,0 @@ -/v1/vpcs/{uuid}/ports/{port_number:\d+}/nio ------------------------------------------------------------ - -.. contents:: - -POST /v1/vpcs/**{uuid}**/ports/**{port_number:\d+}**/nio -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Add a NIO to a VPCS instance - -Parameters -********** -- **uuid**: Instance UUID -- **port_number**: Port where the nio should be added - -Response status codes -********************** -- **400**: Invalid instance UUID -- **201**: NIO created -- **404**: Instance doesn't exist - - -DELETE /v1/vpcs/**{uuid}**/ports/**{port_number:\d+}**/nio -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Remove a NIO from a VPCS instance - -Parameters -********** -- **uuid**: Instance UUID -- **port_number**: Port from where the nio should be removed - -Response status codes -********************** -- **400**: Invalid instance UUID -- **404**: Instance doesn't exist -- **204**: NIO deleted - diff --git a/docs/api/v1vpcsuuidreload.rst b/docs/api/v1vpcsuuidreload.rst deleted file mode 100644 index 5495cc6e..00000000 --- a/docs/api/v1vpcsuuidreload.rst +++ /dev/null @@ -1,19 +0,0 @@ -/v1/vpcs/{uuid}/reload ------------------------------------------------------------ - -.. contents:: - -POST /v1/vpcs/**{uuid}**/reload -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Reload a VPCS instance - -Parameters -********** -- **uuid**: Instance UUID - -Response status codes -********************** -- **400**: Invalid instance UUID -- **404**: Instance doesn't exist -- **204**: Instance reloaded - diff --git a/docs/api/v1vpcsuuidstart.rst b/docs/api/v1vpcsuuidstart.rst deleted file mode 100644 index 60b9af17..00000000 --- a/docs/api/v1vpcsuuidstart.rst +++ /dev/null @@ -1,19 +0,0 @@ -/v1/vpcs/{uuid}/start ------------------------------------------------------------ - -.. contents:: - -POST /v1/vpcs/**{uuid}**/start -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Start a VPCS instance - -Parameters -********** -- **uuid**: Instance UUID - -Response status codes -********************** -- **400**: Invalid VPCS instance UUID -- **404**: Instance doesn't exist -- **204**: Instance started - diff --git a/docs/api/v1vpcsuuidstop.rst b/docs/api/v1vpcsuuidstop.rst deleted file mode 100644 index 90f96f06..00000000 --- a/docs/api/v1vpcsuuidstop.rst +++ /dev/null @@ -1,19 +0,0 @@ -/v1/vpcs/{uuid}/stop ------------------------------------------------------------ - -.. contents:: - -POST /v1/vpcs/**{uuid}**/stop -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Stop a VPCS instance - -Parameters -********** -- **uuid**: Instance UUID - -Response status codes -********************** -- **400**: Invalid VPCS instance UUID -- **404**: Instance doesn't exist -- **204**: Instance stopped - diff --git a/gns3server/handlers/virtualbox_handler.py b/gns3server/handlers/virtualbox_handler.py index 00518700..61ad2556 100644 --- a/gns3server/handlers/virtualbox_handler.py +++ b/gns3server/handlers/virtualbox_handler.py @@ -23,6 +23,7 @@ from ..schemas.virtualbox import VBOX_NIO_SCHEMA from ..schemas.virtualbox import VBOX_CAPTURE_SCHEMA from ..schemas.virtualbox import VBOX_OBJECT_SCHEMA from ..modules.virtualbox import VirtualBox +from ..modules.project_manager import ProjectManager class VirtualBoxHandler: @@ -33,7 +34,7 @@ class VirtualBoxHandler: @classmethod @Route.get( - r"/virtualbox/vms_tmp", + r"/virtualbox/vms", status_codes={ 200: "Success", }, @@ -46,10 +47,13 @@ class VirtualBoxHandler: @classmethod @Route.post( - r"/virtualbox/vms", + r"/{project_id}/virtualbox/vms", + parameters={ + "project_id": "UUID for the project" + }, status_codes={ 201: "Instance created", - 400: "Invalid project ID", + 400: "Invalid request", 409: "Conflict" }, description="Create a new VirtualBox VM instance", @@ -59,7 +63,7 @@ class VirtualBoxHandler: vbox_manager = VirtualBox.instance() vm = yield from vbox_manager.create_vm(request.json.pop("name"), - request.json.pop("project_id"), + request.match_info["project_id"], request.json.get("vm_id"), request.json.pop("vmname"), request.json.pop("linked_clone"), @@ -74,12 +78,14 @@ class VirtualBoxHandler: @classmethod @Route.get( - r"/virtualbox/vms/{vm_id}", + r"/{project_id}/virtualbox/vms/{vm_id}", parameters={ + "project_id": "UUID for the project", "vm_id": "UUID for the instance" }, status_codes={ 200: "Success", + 400: "Invalid request", 404: "Instance doesn't exist" }, description="Get a VirtualBox VM instance", @@ -87,17 +93,19 @@ class VirtualBoxHandler: def show(request, response): vbox_manager = VirtualBox.instance() - vm = vbox_manager.get_vm(request.match_info["vm_id"]) + vm = vbox_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) response.json(vm) @classmethod @Route.put( - r"/virtualbox/vms/{vm_id}", + r"/{project_id}/virtualbox/vms/{vm_id}", parameters={ + "project_id": "UUID for the project", "vm_id": "UUID for the instance" }, status_codes={ 200: "Instance updated", + 400: "Invalid request", 404: "Instance doesn't exist", 409: "Conflict" }, @@ -107,7 +115,7 @@ class VirtualBoxHandler: def update(request, response): vbox_manager = VirtualBox.instance() - vm = vbox_manager.get_vm(request.match_info["vm_id"]) + vm = vbox_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) for name, value in request.json.items(): if hasattr(vm, name) and getattr(vm, name) != value: @@ -118,124 +126,135 @@ class VirtualBoxHandler: @classmethod @Route.delete( - r"/virtualbox/vms/{vm_id}", + r"/{project_id}/virtualbox/vms/{vm_id}", parameters={ + "project_id": "UUID for the project", "vm_id": "UUID for the instance" }, status_codes={ 204: "Instance deleted", + 400: "Invalid request", 404: "Instance doesn't exist" }, description="Delete a VirtualBox VM instance") def delete(request, response): + # check the project_id exists + ProjectManager.instance().get_project(request.match_info["project_id"]) + yield from VirtualBox.instance().delete_vm(request.match_info["vm_id"]) response.set_status(204) @classmethod @Route.post( - r"/virtualbox/vms/{vm_id}/start", + r"/{project_id}/virtualbox/vms/{vm_id}/start", parameters={ + "project_id": "UUID for the project", "vm_id": "UUID for the instance" }, status_codes={ 204: "Instance started", - 400: "Invalid instance UUID", + 400: "Invalid request", 404: "Instance doesn't exist" }, description="Start a VirtualBox VM instance") def start(request, response): vbox_manager = VirtualBox.instance() - vm = vbox_manager.get_vm(request.match_info["vm_id"]) + vm = vbox_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) yield from vm.start() response.set_status(204) @classmethod @Route.post( - r"/virtualbox/vms/{vm_id}/stop", + r"/{project_id}/virtualbox/vms/{vm_id}/stop", parameters={ + "project_id": "UUID for the project", "vm_id": "UUID for the instance" }, status_codes={ 204: "Instance stopped", - 400: "Invalid instance UUID", + 400: "Invalid request", 404: "Instance doesn't exist" }, description="Stop a VirtualBox VM instance") def stop(request, response): vbox_manager = VirtualBox.instance() - vm = vbox_manager.get_vm(request.match_info["vm_id"]) + vm = vbox_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) yield from vm.stop() response.set_status(204) @classmethod @Route.post( - r"/virtualbox/vms/{vm_id}/suspend", + r"/{project_id}/virtualbox/vms/{vm_id}/suspend", parameters={ + "project_id": "UUID for the project", "vm_id": "UUID for the instance" }, status_codes={ 204: "Instance suspended", - 400: "Invalid instance UUID", + 400: "Invalid request", 404: "Instance doesn't exist" }, description="Suspend a VirtualBox VM instance") def suspend(request, response): vbox_manager = VirtualBox.instance() - vm = vbox_manager.get_vm(request.match_info["vm_id"]) + vm = vbox_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) yield from vm.suspend() response.set_status(204) @classmethod @Route.post( - r"/virtualbox/vms/{vm_id}/resume", + r"/{project_id}/virtualbox/vms/{vm_id}/resume", parameters={ + "project_id": "UUID for the project", "vm_id": "UUID for the instance" }, status_codes={ 204: "Instance resumed", - 400: "Invalid instance UUID", + 400: "Invalid request", 404: "Instance doesn't exist" }, description="Resume a suspended VirtualBox VM instance") def suspend(request, response): vbox_manager = VirtualBox.instance() - vm = vbox_manager.get_vm(request.match_info["vm_id"]) + vm = vbox_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) yield from vm.resume() response.set_status(204) @classmethod @Route.post( - r"/virtualbox/vms/{vm_id}/reload", + r"/{project_id}/virtualbox/vms/{vm_id}/reload", parameters={ + "project_id": "UUID for the project", "vm_id": "UUID for the instance" }, status_codes={ 204: "Instance reloaded", - 400: "Invalid instance UUID", + 400: "Invalid request", 404: "Instance doesn't exist" }, description="Reload a VirtualBox VM instance") - def suspend(request, response): + def reload(request, response): vbox_manager = VirtualBox.instance() - vm = vbox_manager.get_vm(request.match_info["vm_id"]) + vm = vbox_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) yield from vm.reload() response.set_status(204) @Route.post( - r"/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/nio", + r"/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/nio", parameters={ + "project_id": "UUID for the project", "vm_id": "UUID for the instance", "adapter_id": "Adapter where the nio should be added" }, status_codes={ 201: "NIO created", - 400: "Invalid instance UUID", + 400: "Invalid request", 404: "Instance doesn't exist" }, description="Add a NIO to a VirtualBox VM instance", @@ -244,7 +263,7 @@ class VirtualBoxHandler: def create_nio(request, response): vbox_manager = VirtualBox.instance() - vm = vbox_manager.get_vm(request.match_info["vm_id"]) + vm = vbox_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) nio = vbox_manager.create_nio(vbox_manager.vboxmanage_path, request.json) vm.port_add_nio_binding(int(request.match_info["adapter_id"]), nio) response.set_status(201) @@ -252,33 +271,35 @@ class VirtualBoxHandler: @classmethod @Route.delete( - r"/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/nio", + r"/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/nio", parameters={ + "project_id": "UUID for the project", "vm_id": "UUID for the instance", "adapter_id": "Adapter from where the nio should be removed" }, status_codes={ 204: "NIO deleted", - 400: "Invalid instance UUID", + 400: "Invalid request", 404: "Instance doesn't exist" }, description="Remove a NIO from a VirtualBox VM instance") def delete_nio(request, response): vbox_manager = VirtualBox.instance() - vm = vbox_manager.get_vm(request.match_info["vm_id"]) + vm = vbox_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) vm.port_remove_nio_binding(int(request.match_info["adapter_id"])) response.set_status(204) @Route.post( - r"/virtualbox/{vm_id}/capture/{adapter_id:\d+}/start", + r"/{project_id}/virtualbox/{vm_id}/capture/{adapter_id:\d+}/start", parameters={ + "project_id": "UUID for the project", "vm_id": "UUID for the instance", "adapter_id": "Adapter to start a packet capture" }, status_codes={ 200: "Capture started", - 400: "Invalid instance UUID", + 400: "Invalid request", 404: "Instance doesn't exist" }, description="Start a packet capture on a VirtualBox VM instance", @@ -286,27 +307,28 @@ class VirtualBoxHandler: def start_capture(request, response): vbox_manager = VirtualBox.instance() - vm = vbox_manager.get_vm(request.match_info["vm_id"]) + vm = vbox_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) adapter_id = int(request.match_info["adapter_id"]) pcap_file_path = os.path.join(vm.project.capture_working_directory(), request.json["filename"]) vm.start_capture(adapter_id, pcap_file_path) response.json({"pcap_file_path": pcap_file_path}) @Route.post( - r"/virtualbox/{vm_id}/capture/{adapter_id:\d+}/stop", + r"/{project_id}/virtualbox/{vm_id}/capture/{adapter_id:\d+}/stop", parameters={ + "project_id": "UUID for the project", "vm_id": "UUID for the instance", "adapter_id": "Adapter to stop a packet capture" }, status_codes={ 204: "Capture stopped", - 400: "Invalid instance UUID", + 400: "Invalid request", 404: "Instance doesn't exist" }, description="Stop a packet capture on a VirtualBox VM instance") def start_capture(request, response): vbox_manager = VirtualBox.instance() - vm = vbox_manager.get_vm(request.match_info["vm_id"]) + vm = vbox_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) vm.stop_capture(int(request.match_info["adapter_id"])) response.set_status(204) diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index 539ebb68..871f2713 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -31,10 +31,13 @@ class VPCSHandler: @classmethod @Route.post( - r"/vpcs/vms", + r"/{project_id}/vpcs/vms", + parameters={ + "project_id": "UUID for the project" + }, status_codes={ 201: "Instance created", - 400: "Invalid project UUID", + 400: "Invalid request", 409: "Conflict" }, description="Create a new VPCS instance", @@ -44,7 +47,7 @@ class VPCSHandler: vpcs = VPCS.instance() vm = yield from vpcs.create_vm(request.json["name"], - request.json["project_id"], + request.match_info["project_id"], request.json.get("vm_id"), console=request.json.get("console"), startup_script=request.json.get("startup_script")) @@ -53,12 +56,14 @@ class VPCSHandler: @classmethod @Route.get( - r"/vpcs/vms/{vm_id}", + r"/{project_id}/vpcs/vms/{vm_id}", parameters={ + "project_id": "UUID for the project", "vm_id": "UUID for the instance" }, status_codes={ 200: "Success", + 400: "Invalid request", 404: "Instance doesn't exist" }, description="Get a VPCS instance", @@ -66,17 +71,19 @@ class VPCSHandler: def show(request, response): vpcs_manager = VPCS.instance() - vm = vpcs_manager.get_vm(request.match_info["vm_id"]) + vm = vpcs_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) response.json(vm) @classmethod @Route.put( - r"/vpcs/vms/{vm_id}", + r"/{project_id}/vpcs/vms/{vm_id}", parameters={ + "project_id": "UUID for the project", "vm_id": "UUID for the instance" }, status_codes={ 200: "Instance updated", + 400: "Invalid request", 404: "Instance doesn't exist", 409: "Conflict" }, @@ -86,7 +93,7 @@ class VPCSHandler: def update(request, response): vpcs_manager = VPCS.instance() - vm = vpcs_manager.get_vm(request.match_info["vm_id"]) + vm = vpcs_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) vm.name = request.json.get("name", vm.name) vm.console = request.json.get("console", vm.console) vm.startup_script = request.json.get("startup_script", vm.startup_script) @@ -94,12 +101,14 @@ class VPCSHandler: @classmethod @Route.delete( - r"/vpcs/vms/{vm_id}", + r"/{project_id}/vpcs/vms/{vm_id}", parameters={ + "project_id": "UUID for the project", "vm_id": "UUID for the instance" }, status_codes={ 204: "Instance deleted", + 400: "Invalid request", 404: "Instance doesn't exist" }, description="Delete a VPCS instance") @@ -110,70 +119,74 @@ class VPCSHandler: @classmethod @Route.post( - r"/vpcs/vms/{vm_id}/start", + r"/{project_id}/vpcs/vms/{vm_id}/start", parameters={ + "project_id": "UUID for the project", "vm_id": "UUID for the instance" }, status_codes={ 204: "Instance started", - 400: "Invalid VPCS instance UUID", + 400: "Invalid request", 404: "Instance doesn't exist" }, description="Start a VPCS instance") def start(request, response): vpcs_manager = VPCS.instance() - vm = vpcs_manager.get_vm(request.match_info["vm_id"]) + vm = vpcs_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) yield from vm.start() response.set_status(204) @classmethod @Route.post( - r"/vpcs/vms/{vm_id}/stop", + r"/{project_id}/vpcs/vms/{vm_id}/stop", parameters={ + "project_id": "UUID for the project", "vm_id": "UUID for the instance" }, status_codes={ 204: "Instance stopped", - 400: "Invalid VPCS instance UUID", + 400: "Invalid request", 404: "Instance doesn't exist" }, description="Stop a VPCS instance") def stop(request, response): vpcs_manager = VPCS.instance() - vm = vpcs_manager.get_vm(request.match_info["vm_id"]) + vm = vpcs_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) yield from vm.stop() response.set_status(204) @classmethod @Route.post( - r"/vpcs/vms/{vm_id}/reload", + r"/{project_id}/vpcs/vms/{vm_id}/reload", parameters={ + "project_id": "UUID for the project", "vm_id": "UUID for the instance", }, status_codes={ 204: "Instance reloaded", - 400: "Invalid instance UUID", + 400: "Invalid request", 404: "Instance doesn't exist" }, description="Reload a VPCS instance") def reload(request, response): vpcs_manager = VPCS.instance() - vm = vpcs_manager.get_vm(request.match_info["vm_id"]) + vm = vpcs_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) yield from vm.reload() response.set_status(204) @Route.post( - r"/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio", + r"/{project_id}/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio", parameters={ + "project_id": "UUID for the project", "vm_id": "UUID for the instance", "port_number": "Port where the nio should be added" }, status_codes={ 201: "NIO created", - 400: "Invalid instance UUID", + 400: "Invalid request", 404: "Instance doesn't exist" }, description="Add a NIO to a VPCS instance", @@ -182,7 +195,7 @@ class VPCSHandler: def create_nio(request, response): vpcs_manager = VPCS.instance() - vm = vpcs_manager.get_vm(request.match_info["vm_id"]) + vm = vpcs_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) nio = vpcs_manager.create_nio(vm.vpcs_path, request.json) vm.port_add_nio_binding(int(request.match_info["port_number"]), nio) response.set_status(201) @@ -190,20 +203,21 @@ class VPCSHandler: @classmethod @Route.delete( - r"/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio", + r"/{project_id}/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio", parameters={ + "project_id": "UUID for the project", "vm_id": "UUID for the instance", "port_number": "Port from where the nio should be removed" }, status_codes={ 204: "NIO deleted", - 400: "Invalid instance UUID", + 400: "Invalid request", 404: "Instance doesn't exist" }, description="Remove a NIO from a VPCS instance") def delete_nio(request, response): vpcs_manager = VPCS.instance() - vm = vpcs_manager.get_vm(request.match_info["vm_id"]) + vm = vpcs_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) vm.port_remove_nio_binding(int(request.match_info["port_number"])) response.set_status(204) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index e1cecf22..52a224c9 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -108,22 +108,34 @@ class BaseManager: BaseManager._instance = None log.debug("Module {} unloaded".format(self.module_name)) - def get_vm(self, vm_id): + def get_vm(self, vm_id, project_id=None): """ Returns a VM instance. :param vm_id: VM identifier + :param project_id: Project identifier :returns: VM instance """ + if project_id: + # check the project_id exists + project = ProjectManager.instance().get_project(project_id) + try: UUID(vm_id, version=4) except ValueError: - raise aiohttp.web.HTTPBadRequest(text="{} is not a valid UUID".format(vm_id)) + raise aiohttp.web.HTTPBadRequest(text="VM ID {} is not a valid UUID".format(vm_id)) if vm_id not in self._vms: - raise aiohttp.web.HTTPNotFound(text="ID {} doesn't exist".format(vm_id)) + raise aiohttp.web.HTTPNotFound(text="VM ID {} doesn't exist".format(vm_id)) + + vm = self._vms[vm_id] + + if project_id: + if vm.project.id != project.id: + raise aiohttp.web.HTTPNotFound(text="Project ID {} doesn't belong to VM {}".format(project_id, vm.name)) + return self._vms[vm_id] @asyncio.coroutine diff --git a/gns3server/modules/project_manager.py b/gns3server/modules/project_manager.py index 55cae7c4..87ed9288 100644 --- a/gns3server/modules/project_manager.py +++ b/gns3server/modules/project_manager.py @@ -54,7 +54,7 @@ class ProjectManager: try: UUID(project_id, version=4) except ValueError: - raise aiohttp.web.HTTPBadRequest(text="{} is not a valid UUID".format(project_id)) + raise aiohttp.web.HTTPBadRequest(text="Project ID {} is not a valid UUID".format(project_id)) if project_id not in self._projects: raise aiohttp.web.HTTPNotFound(text="Project ID {} doesn't exist".format(project_id)) diff --git a/gns3server/schemas/virtualbox.py b/gns3server/schemas/virtualbox.py index e2fe70aa..03785691 100644 --- a/gns3server/schemas/virtualbox.py +++ b/gns3server/schemas/virtualbox.py @@ -28,13 +28,6 @@ VBOX_CREATE_SCHEMA = { "maxLength": 36, "pattern": "(^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}|\d+)$" }, - "project_id": { - "description": "Project UUID", - "type": "string", - "minLength": 36, - "maxLength": 36, - "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" - }, "linked_clone": { "description": "either the VM is a linked clone or not", "type": "boolean" @@ -82,7 +75,7 @@ VBOX_CREATE_SCHEMA = { }, }, "additionalProperties": False, - "required": ["name", "vmname", "linked_clone", "project_id"], + "required": ["name", "vmname", "linked_clone"], } VBOX_UPDATE_SCHEMA = { diff --git a/gns3server/schemas/vpcs.py b/gns3server/schemas/vpcs.py index 30238901..db446237 100644 --- a/gns3server/schemas/vpcs.py +++ b/gns3server/schemas/vpcs.py @@ -33,13 +33,6 @@ VPCS_CREATE_SCHEMA = { "maxLength": 36, "pattern": "^([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}|\d+)$" }, - "project_id": { - "description": "Project UUID", - "type": "string", - "minLength": 36, - "maxLength": 36, - "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" - }, "console": { "description": "console TCP port", "minimum": 1, @@ -52,7 +45,7 @@ VPCS_CREATE_SCHEMA = { }, }, "additionalProperties": False, - "required": ["name", "project_id"] + "required": ["name"] } VPCS_UPDATE_SCHEMA = { diff --git a/tests/api/test_virtualbox.py b/tests/api/test_virtualbox.py index 0cf2cda9..86179ff8 100644 --- a/tests/api/test_virtualbox.py +++ b/tests/api/test_virtualbox.py @@ -22,10 +22,9 @@ from tests.utils import asyncio_patch @pytest.fixture(scope="module") def vm(server, project): with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.create", return_value=True) as mock: - response = server.post("/virtualbox/vms", {"name": "VMTEST", - "vmname": "VMTEST", - "linked_clone": False, - "project_id": project.id}) + response = server.post("/{project_id}/virtualbox/vms".format(project_id=project.id), {"name": "VMTEST", + "vmname": "VMTEST", + "linked_clone": False}) assert mock.called assert response.status == 201 return response.json @@ -34,10 +33,9 @@ def vm(server, project): def test_vbox_create(server, project): with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.create", return_value=True): - response = server.post("/virtualbox/vms", {"name": "VM1", - "vmname": "VM1", - "linked_clone": False, - "project_id": project.id}, + response = server.post("/{project_id}/virtualbox/vms".format(project_id=project.id), {"name": "VM1", + "vmname": "VM1", + "linked_clone": False}, example=True) assert response.status == 201 assert response.json["name"] == "VM1" @@ -45,72 +43,74 @@ def test_vbox_create(server, project): def test_vbox_get(server, project, vm): - response = server.get("/virtualbox/vms/{}".format(vm["vm_id"]), example=True) + response = server.get("/{project_id}/virtualbox/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert response.status == 200 - assert response.route == "/virtualbox/vms/{vm_id}" + assert response.route == "/{project_id}/virtualbox/vms/{vm_id}" assert response.json["name"] == "VMTEST" assert response.json["project_id"] == project.id def test_vbox_start(server, vm): with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.start", return_value=True) as mock: - response = server.post("/virtualbox/vms/{}/start".format(vm["vm_id"])) + response = server.post("/{project_id}/virtualbox/vms/{vm_id}/start".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) assert mock.called assert response.status == 204 def test_vbox_stop(server, vm): with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.stop", return_value=True) as mock: - response = server.post("/virtualbox/vms/{}/stop".format(vm["vm_id"])) + response = server.post("/{project_id}/virtualbox/vms/{vm_id}/stop".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) assert mock.called assert response.status == 204 def test_vbox_suspend(server, vm): with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.suspend", return_value=True) as mock: - response = server.post("/virtualbox/vms/{}/suspend".format(vm["vm_id"])) + response = server.post("/{project_id}/virtualbox/vms/{vm_id}/suspend".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) assert mock.called assert response.status == 204 def test_vbox_resume(server, vm): with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.resume", return_value=True) as mock: - response = server.post("/virtualbox/vms/{}/resume".format(vm["vm_id"])) + response = server.post("/{project_id}/virtualbox/vms/{vm_id}/resume".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) assert mock.called assert response.status == 204 def test_vbox_reload(server, vm): with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.reload", return_value=True) as mock: - response = server.post("/virtualbox/vms/{}/reload".format(vm["vm_id"])) + response = server.post("/{project_id}/virtualbox/vms/{vm_id}/reload".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) assert mock.called assert response.status == 204 def test_vbox_nio_create_udp(server, vm): - response = server.post("/virtualbox/vms/{}/adapters/0/nio".format(vm["vm_id"]), {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"}, + response = server.post("/{project_id}/virtualbox/vms/{vm_id}/adapters/0/nio".format(project_id=vm["project_id"], + vm_id=vm["vm_id"]), {"type": "nio_udp", + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}, example=True) assert response.status == 201 - assert response.route == "/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/nio" + assert response.route == "/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/nio" assert response.json["type"] == "nio_udp" def test_vbox_delete_nio(server, vm): - server.post("/virtualbox/vms/{}/adapters/0/nio".format(vm["vm_id"]), {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"}) - response = server.delete("/virtualbox/vms/{}/adapters/0/nio".format(vm["vm_id"]), example=True) + server.post("/{project_id}/virtualbox/vms/{vm_id}/adapters/0/nio".format(project_id=vm["project_id"], + vm_id=vm["vm_id"]), {"type": "nio_udp", + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}) + response = server.delete("/{project_id}/virtualbox/vms/{vm_id}/adapters/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert response.status == 204 - assert response.route == "/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/nio" + assert response.route == "/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/nio" -def test_vpcs_update(server, vm, free_console_port): - response = server.put("/virtualbox/vms/{}".format(vm["vm_id"]), {"name": "test", - "console": free_console_port}) +def test_vbox_update(server, vm, free_console_port): + response = server.put("/{project_id}/virtualbox/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"name": "test", + "console": free_console_port}) assert response.status == 200 assert response.json["name"] == "test" assert response.json["console"] == free_console_port diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index afdb578e..fb614693 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -23,108 +23,108 @@ from unittest.mock import patch @pytest.fixture(scope="module") def vm(server, project): - response = server.post("/vpcs/vms", {"name": "PC TEST 1", "project_id": project.id}) + response = server.post("/{project_id}/vpcs/vms".format(project_id=project.id), {"name": "PC TEST 1"}) assert response.status == 201 return response.json def test_vpcs_create(server, project): - response = server.post("/vpcs/vms", {"name": "PC TEST 1", "project_id": project.id}, example=True) + response = server.post("/{project_id}/vpcs/vms".format(project_id=project.id), {"name": "PC TEST 1"}, example=True) assert response.status == 201 - assert response.route == "/vpcs/vms" + assert response.route == "/{project_id}/vpcs/vms" assert response.json["name"] == "PC TEST 1" assert response.json["project_id"] == project.id assert response.json["script_file"] is None def test_vpcs_get(server, project, vm): - response = server.get("/vpcs/vms/{}".format(vm["vm_id"]), example=True) + response = server.get("/{project_id}/vpcs/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert response.status == 200 - assert response.route == "/vpcs/vms/{vm_id}" + assert response.route == "/{project_id}/vpcs/vms/{vm_id}" assert response.json["name"] == "PC TEST 1" assert response.json["project_id"] == project.id def test_vpcs_create_startup_script(server, project): - response = server.post("/vpcs/vms", {"name": "PC TEST 1", "project_id": project.id, "startup_script": "ip 192.168.1.2\necho TEST"}) + response = server.post("/{project_id}/vpcs/vms".format(project_id=project.id), {"name": "PC TEST 1", "startup_script": "ip 192.168.1.2\necho TEST"}) assert response.status == 201 - assert response.route == "/vpcs/vms" + assert response.route == "/{project_id}/vpcs/vms" assert response.json["name"] == "PC TEST 1" assert response.json["project_id"] == project.id assert response.json["startup_script"] == "ip 192.168.1.2\necho TEST" def test_vpcs_create_port(server, project, free_console_port): - response = server.post("/vpcs/vms", {"name": "PC TEST 1", "project_id": project.id, "console": free_console_port}) + response = server.post("/{project_id}/vpcs/vms".format(project_id=project.id), {"name": "PC TEST 1", "console": free_console_port}) assert response.status == 201 - assert response.route == "/vpcs/vms" + assert response.route == "/{project_id}/vpcs/vms" assert response.json["name"] == "PC TEST 1" assert response.json["project_id"] == project.id assert response.json["console"] == free_console_port def test_vpcs_nio_create_udp(server, vm): - response = server.post("/vpcs/vms/{}/ports/0/nio".format(vm["vm_id"]), {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"}, + response = server.post("/{project_id}/vpcs/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}, example=True) assert response.status == 201 - assert response.route == "/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio" + assert response.route == "/{project_id}/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio" assert response.json["type"] == "nio_udp" def test_vpcs_nio_create_tap(server, vm): with patch("gns3server.modules.base_manager.BaseManager._has_privileged_access", return_value=True): - response = server.post("/vpcs/vms/{}/ports/0/nio".format(vm["vm_id"]), {"type": "nio_tap", - "tap_device": "test"}) + response = server.post("/{project_id}/vpcs/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_tap", + "tap_device": "test"}) assert response.status == 201 - assert response.route == "/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio" + assert response.route == "/{project_id}/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio" assert response.json["type"] == "nio_tap" def test_vpcs_delete_nio(server, vm): - server.post("/vpcs/vms/{}/ports/0/nio".format(vm["vm_id"]), {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"}) - response = server.delete("/vpcs/vms/{}/ports/0/nio".format(vm["vm_id"]), example=True) + server.post("/{project_id}/vpcs/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}) + response = server.delete("/{project_id}/vpcs/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert response.status == 204 - assert response.route == "/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio" + assert response.route == "/{project_id}/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio" def test_vpcs_start(server, vm): with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM.start", return_value=True) as mock: - response = server.post("/vpcs/vms/{}/start".format(vm["vm_id"])) + response = server.post("/{project_id}/vpcs/vms/{vm_id}/start".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) assert mock.called assert response.status == 204 def test_vpcs_stop(server, vm): with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM.stop", return_value=True) as mock: - response = server.post("/vpcs/vms/{}/stop".format(vm["vm_id"])) + response = server.post("/{project_id}/vpcs/vms/{vm_id}/stop".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) assert mock.called assert response.status == 204 def test_vpcs_reload(server, vm): with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM.reload", return_value=True) as mock: - response = server.post("/vpcs/vms/{}/reload".format(vm["vm_id"])) + response = server.post("/{project_id}/vpcs/vms/{vm_id}/reload".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) assert mock.called assert response.status == 204 def test_vpcs_delete(server, vm): with asyncio_patch("gns3server.modules.vpcs.VPCS.delete_vm", return_value=True) as mock: - response = server.delete("/vpcs/vms/{}".format(vm["vm_id"])) + response = server.delete("/{project_id}/vpcs/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) assert mock.called assert response.status == 204 def test_vpcs_update(server, vm, tmpdir, free_console_port): - response = server.put("/vpcs/vms/{}".format(vm["vm_id"]), {"name": "test", - "console": free_console_port, - "startup_script": "ip 192.168.1.1"}) + response = server.put("/{project_id}/vpcs/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"name": "test", + "console": free_console_port, + "startup_script": "ip 192.168.1.1"}) assert response.status == 200 assert response.json["name"] == "test" assert response.json["console"] == free_console_port From c12d3ff739b8758a6712264eb834f26db30ab9f4 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 4 Feb 2015 17:48:33 -0700 Subject: [PATCH 182/485] Update documentation. --- docs/api/examples/put_projectsprojectid.txt | 4 +- ...idvirtualboxvmidcaptureadapteriddstart.rst | 30 +++++ ...tidvirtualboxvmidcaptureadapteriddstop.rst | 21 ++++ docs/api/v1projectidvirtualboxvms.rst | 55 +++++++++ docs/api/v1projectidvirtualboxvmsvmid.rst | 105 ++++++++++++++++++ ...virtualboxvmsvmidadaptersadapteriddnio.rst | 38 +++++++ .../v1projectidvirtualboxvmsvmidreload.rst | 20 ++++ .../v1projectidvirtualboxvmsvmidresume.rst | 20 ++++ .../api/v1projectidvirtualboxvmsvmidstart.rst | 20 ++++ docs/api/v1projectidvirtualboxvmsvmidstop.rst | 20 ++++ .../v1projectidvirtualboxvmsvmidsuspend.rst | 20 ++++ docs/api/v1projectidvpcsvms.rst | 45 ++++++++ docs/api/v1projectidvpcsvmsvmid.rst | 92 +++++++++++++++ ...rojectidvpcsvmsvmidportsportnumberdnio.rst | 38 +++++++ docs/api/v1projectidvpcsvmsvmidreload.rst | 20 ++++ docs/api/v1projectidvpcsvmsvmidstart.rst | 20 ++++ docs/api/v1projectidvpcsvmsvmidstop.rst | 20 ++++ 17 files changed, 586 insertions(+), 2 deletions(-) create mode 100644 docs/api/v1projectidvirtualboxvmidcaptureadapteriddstart.rst create mode 100644 docs/api/v1projectidvirtualboxvmidcaptureadapteriddstop.rst create mode 100644 docs/api/v1projectidvirtualboxvms.rst create mode 100644 docs/api/v1projectidvirtualboxvmsvmid.rst create mode 100644 docs/api/v1projectidvirtualboxvmsvmidadaptersadapteriddnio.rst create mode 100644 docs/api/v1projectidvirtualboxvmsvmidreload.rst create mode 100644 docs/api/v1projectidvirtualboxvmsvmidresume.rst create mode 100644 docs/api/v1projectidvirtualboxvmsvmidstart.rst create mode 100644 docs/api/v1projectidvirtualboxvmsvmidstop.rst create mode 100644 docs/api/v1projectidvirtualboxvmsvmidsuspend.rst create mode 100644 docs/api/v1projectidvpcsvms.rst create mode 100644 docs/api/v1projectidvpcsvmsvmid.rst create mode 100644 docs/api/v1projectidvpcsvmsvmidportsportnumberdnio.rst create mode 100644 docs/api/v1projectidvpcsvmsvmidreload.rst create mode 100644 docs/api/v1projectidvpcsvmsvmidstart.rst create mode 100644 docs/api/v1projectidvpcsvmsvmidstop.rst diff --git a/docs/api/examples/put_projectsprojectid.txt b/docs/api/examples/put_projectsprojectid.txt index b4043f7e..43cf8961 100644 --- a/docs/api/examples/put_projectsprojectid.txt +++ b/docs/api/examples/put_projectsprojectid.txt @@ -1,8 +1,8 @@ -curl -i -X PUT 'http://localhost:8000/projects/{project_id}' -d '{"path": "/tmp/pytest-42/test_update_path_project_non_l0"}' +curl -i -X PUT 'http://localhost:8000/projects/{project_id}' -d '{"path": "/tmp/pytest-48/test_update_path_project_non_l0"}' PUT /projects/{project_id} HTTP/1.1 { - "path": "/tmp/pytest-42/test_update_path_project_non_l0" + "path": "/tmp/pytest-48/test_update_path_project_non_l0" } diff --git a/docs/api/v1projectidvirtualboxvmidcaptureadapteriddstart.rst b/docs/api/v1projectidvirtualboxvmidcaptureadapteriddstart.rst new file mode 100644 index 00000000..2faa7709 --- /dev/null +++ b/docs/api/v1projectidvirtualboxvmidcaptureadapteriddstart.rst @@ -0,0 +1,30 @@ +/v1/{project_id}/virtualbox/{vm_id}/capture/{adapter_id:\d+}/start +----------------------------------------------------------- + +.. contents:: + +POST /v1/**{project_id}**/virtualbox/**{vm_id}**/capture/**{adapter_id:\d+}**/start +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Start a packet capture on a VirtualBox VM instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project +- **adapter_id**: Adapter to start a packet capture + +Response status codes +********************** +- **200**: Capture started +- **400**: Invalid request +- **404**: Instance doesn't exist + +Input +******* +.. raw:: html + + + + +
Name Mandatory Type Description
capture_filename string Capture file name
+ diff --git a/docs/api/v1projectidvirtualboxvmidcaptureadapteriddstop.rst b/docs/api/v1projectidvirtualboxvmidcaptureadapteriddstop.rst new file mode 100644 index 00000000..ddacddfd --- /dev/null +++ b/docs/api/v1projectidvirtualboxvmidcaptureadapteriddstop.rst @@ -0,0 +1,21 @@ +/v1/{project_id}/virtualbox/{vm_id}/capture/{adapter_id:\d+}/stop +----------------------------------------------------------- + +.. contents:: + +POST /v1/**{project_id}**/virtualbox/**{vm_id}**/capture/**{adapter_id:\d+}**/stop +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Stop a packet capture on a VirtualBox VM instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project +- **adapter_id**: Adapter to stop a packet capture + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Capture stopped + diff --git a/docs/api/v1projectidvirtualboxvms.rst b/docs/api/v1projectidvirtualboxvms.rst new file mode 100644 index 00000000..4b47e6f0 --- /dev/null +++ b/docs/api/v1projectidvirtualboxvms.rst @@ -0,0 +1,55 @@ +/v1/{project_id}/virtualbox/vms +----------------------------------------------------------- + +.. contents:: + +POST /v1/**{project_id}**/virtualbox/vms +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Create a new VirtualBox VM instance + +Parameters +********** +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **201**: Instance created +- **409**: Conflict + +Input +******* +.. raw:: html + + + + + + + + + + + + + +
Name Mandatory Type Description
adapter_start_index integer adapter index from which to start using adapters
adapter_type string VirtualBox adapter type
adapters integer number of adapters
console integer console TCP port
enable_remote_console boolean enable the remote console
headless boolean headless mode
linked_clone boolean either the VM is a linked clone or not
name string VirtualBox VM instance name
vm_id string VirtualBox VM instance identifier
vmname string VirtualBox VM name (in VirtualBox itself)
+ +Output +******* +.. raw:: html + + + + + + + + + + + + + +
Name Mandatory Type Description
adapter_start_index integer adapter index from which to start using adapters
adapter_type string VirtualBox adapter type
adapters integer number of adapters
console integer console TCP port
enable_remote_console boolean enable the remote console
headless boolean headless mode
name string VirtualBox VM instance name
project_id string Project UUID
vm_id string VirtualBox VM instance UUID
vmname string VirtualBox VM name (in VirtualBox itself)
+ diff --git a/docs/api/v1projectidvirtualboxvmsvmid.rst b/docs/api/v1projectidvirtualboxvmsvmid.rst new file mode 100644 index 00000000..18c50005 --- /dev/null +++ b/docs/api/v1projectidvirtualboxvmsvmid.rst @@ -0,0 +1,105 @@ +/v1/{project_id}/virtualbox/vms/{vm_id} +----------------------------------------------------------- + +.. contents:: + +GET /v1/**{project_id}**/virtualbox/vms/**{vm_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Get a VirtualBox VM instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **200**: Success +- **400**: Invalid request +- **404**: Instance doesn't exist + +Output +******* +.. raw:: html + + + + + + + + + + + + + +
Name Mandatory Type Description
adapter_start_index integer adapter index from which to start using adapters
adapter_type string VirtualBox adapter type
adapters integer number of adapters
console integer console TCP port
enable_remote_console boolean enable the remote console
headless boolean headless mode
name string VirtualBox VM instance name
project_id string Project UUID
vm_id string VirtualBox VM instance UUID
vmname string VirtualBox VM name (in VirtualBox itself)
+ + +PUT /v1/**{project_id}**/virtualbox/vms/**{vm_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Update a VirtualBox VM instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **200**: Instance updated +- **400**: Invalid request +- **404**: Instance doesn't exist +- **409**: Conflict + +Input +******* +.. raw:: html + + + + + + + + + + + +
Name Mandatory Type Description
adapter_start_index integer adapter index from which to start using adapters
adapter_type string VirtualBox adapter type
adapters integer number of adapters
console integer console TCP port
enable_remote_console boolean enable the remote console
headless boolean headless mode
name string VirtualBox VM instance name
vmname string VirtualBox VM name (in VirtualBox itself)
+ +Output +******* +.. raw:: html + + + + + + + + + + + + + +
Name Mandatory Type Description
adapter_start_index integer adapter index from which to start using adapters
adapter_type string VirtualBox adapter type
adapters integer number of adapters
console integer console TCP port
enable_remote_console boolean enable the remote console
headless boolean headless mode
name string VirtualBox VM instance name
project_id string Project UUID
vm_id string VirtualBox VM instance UUID
vmname string VirtualBox VM name (in VirtualBox itself)
+ + +DELETE /v1/**{project_id}**/virtualbox/vms/**{vm_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Delete a VirtualBox VM instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance deleted + diff --git a/docs/api/v1projectidvirtualboxvmsvmidadaptersadapteriddnio.rst b/docs/api/v1projectidvirtualboxvmsvmidadaptersadapteriddnio.rst new file mode 100644 index 00000000..880222ad --- /dev/null +++ b/docs/api/v1projectidvirtualboxvmsvmidadaptersadapteriddnio.rst @@ -0,0 +1,38 @@ +/v1/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/nio +----------------------------------------------------------- + +.. contents:: + +POST /v1/**{project_id}**/virtualbox/vms/**{vm_id}**/adapters/**{adapter_id:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Add a NIO to a VirtualBox VM instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project +- **adapter_id**: Adapter where the nio should be added + +Response status codes +********************** +- **400**: Invalid request +- **201**: NIO created +- **404**: Instance doesn't exist + + +DELETE /v1/**{project_id}**/virtualbox/vms/**{vm_id}**/adapters/**{adapter_id:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Remove a NIO from a VirtualBox VM instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project +- **adapter_id**: Adapter from where the nio should be removed + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: NIO deleted + diff --git a/docs/api/v1projectidvirtualboxvmsvmidreload.rst b/docs/api/v1projectidvirtualboxvmsvmidreload.rst new file mode 100644 index 00000000..48387994 --- /dev/null +++ b/docs/api/v1projectidvirtualboxvmsvmidreload.rst @@ -0,0 +1,20 @@ +/v1/{project_id}/virtualbox/vms/{vm_id}/reload +----------------------------------------------------------- + +.. contents:: + +POST /v1/**{project_id}**/virtualbox/vms/**{vm_id}**/reload +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Reload a VirtualBox VM instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance reloaded + diff --git a/docs/api/v1projectidvirtualboxvmsvmidresume.rst b/docs/api/v1projectidvirtualboxvmsvmidresume.rst new file mode 100644 index 00000000..87a3aa85 --- /dev/null +++ b/docs/api/v1projectidvirtualboxvmsvmidresume.rst @@ -0,0 +1,20 @@ +/v1/{project_id}/virtualbox/vms/{vm_id}/resume +----------------------------------------------------------- + +.. contents:: + +POST /v1/**{project_id}**/virtualbox/vms/**{vm_id}**/resume +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Resume a suspended VirtualBox VM instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance resumed + diff --git a/docs/api/v1projectidvirtualboxvmsvmidstart.rst b/docs/api/v1projectidvirtualboxvmsvmidstart.rst new file mode 100644 index 00000000..9a4672a3 --- /dev/null +++ b/docs/api/v1projectidvirtualboxvmsvmidstart.rst @@ -0,0 +1,20 @@ +/v1/{project_id}/virtualbox/vms/{vm_id}/start +----------------------------------------------------------- + +.. contents:: + +POST /v1/**{project_id}**/virtualbox/vms/**{vm_id}**/start +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Start a VirtualBox VM instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance started + diff --git a/docs/api/v1projectidvirtualboxvmsvmidstop.rst b/docs/api/v1projectidvirtualboxvmsvmidstop.rst new file mode 100644 index 00000000..70acc079 --- /dev/null +++ b/docs/api/v1projectidvirtualboxvmsvmidstop.rst @@ -0,0 +1,20 @@ +/v1/{project_id}/virtualbox/vms/{vm_id}/stop +----------------------------------------------------------- + +.. contents:: + +POST /v1/**{project_id}**/virtualbox/vms/**{vm_id}**/stop +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Stop a VirtualBox VM instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance stopped + diff --git a/docs/api/v1projectidvirtualboxvmsvmidsuspend.rst b/docs/api/v1projectidvirtualboxvmsvmidsuspend.rst new file mode 100644 index 00000000..e9caa7d9 --- /dev/null +++ b/docs/api/v1projectidvirtualboxvmsvmidsuspend.rst @@ -0,0 +1,20 @@ +/v1/{project_id}/virtualbox/vms/{vm_id}/suspend +----------------------------------------------------------- + +.. contents:: + +POST /v1/**{project_id}**/virtualbox/vms/**{vm_id}**/suspend +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Suspend a VirtualBox VM instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance suspended + diff --git a/docs/api/v1projectidvpcsvms.rst b/docs/api/v1projectidvpcsvms.rst new file mode 100644 index 00000000..c6a3a9e4 --- /dev/null +++ b/docs/api/v1projectidvpcsvms.rst @@ -0,0 +1,45 @@ +/v1/{project_id}/vpcs/vms +----------------------------------------------------------- + +.. contents:: + +POST /v1/**{project_id}**/vpcs/vms +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Create a new VPCS instance + +Parameters +********** +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **201**: Instance created +- **409**: Conflict + +Input +******* +.. raw:: html + + + + + + + +
Name Mandatory Type Description
console ['integer', 'null'] console TCP port
name string VPCS VM name
startup_script ['string', 'null'] Content of the VPCS startup script
vm_id string VPCS VM identifier
+ +Output +******* +.. raw:: html + + + + + + + + + +
Name Mandatory Type Description
console integer console TCP port
name string VPCS VM name
project_id string Project UUID
script_file ['string', 'null'] VPCS startup script
startup_script ['string', 'null'] Content of the VPCS startup script
vm_id string VPCS VM UUID
+ diff --git a/docs/api/v1projectidvpcsvmsvmid.rst b/docs/api/v1projectidvpcsvmsvmid.rst new file mode 100644 index 00000000..ca80efaa --- /dev/null +++ b/docs/api/v1projectidvpcsvmsvmid.rst @@ -0,0 +1,92 @@ +/v1/{project_id}/vpcs/vms/{vm_id} +----------------------------------------------------------- + +.. contents:: + +GET /v1/**{project_id}**/vpcs/vms/**{vm_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Get a VPCS instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **200**: Success +- **400**: Invalid request +- **404**: Instance doesn't exist + +Output +******* +.. raw:: html + + + + + + + + + +
Name Mandatory Type Description
console integer console TCP port
name string VPCS VM name
project_id string Project UUID
script_file ['string', 'null'] VPCS startup script
startup_script ['string', 'null'] Content of the VPCS startup script
vm_id string VPCS VM UUID
+ + +PUT /v1/**{project_id}**/vpcs/vms/**{vm_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Update a VPCS instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **200**: Instance updated +- **400**: Invalid request +- **404**: Instance doesn't exist +- **409**: Conflict + +Input +******* +.. raw:: html + + + + + + +
Name Mandatory Type Description
console ['integer', 'null'] console TCP port
name ['string', 'null'] VPCS VM name
startup_script ['string', 'null'] Content of the VPCS startup script
+ +Output +******* +.. raw:: html + + + + + + + + + +
Name Mandatory Type Description
console integer console TCP port
name string VPCS VM name
project_id string Project UUID
script_file ['string', 'null'] VPCS startup script
startup_script ['string', 'null'] Content of the VPCS startup script
vm_id string VPCS VM UUID
+ + +DELETE /v1/**{project_id}**/vpcs/vms/**{vm_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Delete a VPCS instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance deleted + diff --git a/docs/api/v1projectidvpcsvmsvmidportsportnumberdnio.rst b/docs/api/v1projectidvpcsvmsvmidportsportnumberdnio.rst new file mode 100644 index 00000000..0abccb0b --- /dev/null +++ b/docs/api/v1projectidvpcsvmsvmidportsportnumberdnio.rst @@ -0,0 +1,38 @@ +/v1/{project_id}/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio +----------------------------------------------------------- + +.. contents:: + +POST /v1/**{project_id}**/vpcs/vms/**{vm_id}**/ports/**{port_number:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Add a NIO to a VPCS instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project +- **port_number**: Port where the nio should be added + +Response status codes +********************** +- **400**: Invalid request +- **201**: NIO created +- **404**: Instance doesn't exist + + +DELETE /v1/**{project_id}**/vpcs/vms/**{vm_id}**/ports/**{port_number:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Remove a NIO from a VPCS instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project +- **port_number**: Port from where the nio should be removed + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: NIO deleted + diff --git a/docs/api/v1projectidvpcsvmsvmidreload.rst b/docs/api/v1projectidvpcsvmsvmidreload.rst new file mode 100644 index 00000000..b5357561 --- /dev/null +++ b/docs/api/v1projectidvpcsvmsvmidreload.rst @@ -0,0 +1,20 @@ +/v1/{project_id}/vpcs/vms/{vm_id}/reload +----------------------------------------------------------- + +.. contents:: + +POST /v1/**{project_id}**/vpcs/vms/**{vm_id}**/reload +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Reload a VPCS instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance reloaded + diff --git a/docs/api/v1projectidvpcsvmsvmidstart.rst b/docs/api/v1projectidvpcsvmsvmidstart.rst new file mode 100644 index 00000000..ae8a095f --- /dev/null +++ b/docs/api/v1projectidvpcsvmsvmidstart.rst @@ -0,0 +1,20 @@ +/v1/{project_id}/vpcs/vms/{vm_id}/start +----------------------------------------------------------- + +.. contents:: + +POST /v1/**{project_id}**/vpcs/vms/**{vm_id}**/start +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Start a VPCS instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance started + diff --git a/docs/api/v1projectidvpcsvmsvmidstop.rst b/docs/api/v1projectidvpcsvmsvmidstop.rst new file mode 100644 index 00000000..16f3135b --- /dev/null +++ b/docs/api/v1projectidvpcsvmsvmidstop.rst @@ -0,0 +1,20 @@ +/v1/{project_id}/vpcs/vms/{vm_id}/stop +----------------------------------------------------------- + +.. contents:: + +POST /v1/**{project_id}**/vpcs/vms/**{vm_id}**/stop +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Stop a VPCS instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance stopped + From 4e690a6d062770c150b1308db9d347178dc53596 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 4 Feb 2015 17:59:16 -0700 Subject: [PATCH 183/485] Update required aiohttp version to 0.14.4 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 586d3f32..2a06203d 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ class PyTest(TestCommand): sys.exit(errcode) -dependencies = ["aiohttp==0.13.1", +dependencies = ["aiohttp==0.14.4", "jsonschema==2.4.0", "apache-libcloud==0.16.0", "requests==2.5.0"] From 132c06a2d45fad49942ca44aff9e2d47481c8677 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 5 Feb 2015 10:36:39 +0100 Subject: [PATCH 184/485] Add travis icon on README --- README.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.rst b/README.rst index 5cea41ce..967583a1 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,9 @@ GNS3-server =========== +.. image:: https://travis-ci.org/GNS3/gns3-server.svg?branch=master + :target: https://travis-ci.org/GNS3/gns3-server + This is the GNS3 server repository. The GNS3 server manages emulators such as Dynamips, VirtualBox or Qemu/KVM. From 2df3525ffe737b5a6970b5ad48dc6ee455e5b0f6 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 5 Feb 2015 10:46:27 +0100 Subject: [PATCH 185/485] Add code coverage --- .travis.yml | 4 +++- dev-requirements.txt | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1649f84b..dadbe57c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ install: - pip install -rdev-requirements.txt script: - - py.test -v -s tests + - py.test -v -s tests --cov gns3server --cov-report term-missing #branches: # only: @@ -28,3 +28,5 @@ notifications: # on_success: change # on_failure: always +after_success: + - coveralls diff --git a/dev-requirements.txt b/dev-requirements.txt index 791cf88b..af76ab92 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -5,3 +5,5 @@ pytest==2.6.4 pep8==1.5.7 pytest-timeout pytest-capturelog +pytest-cov +python-coveralls From ed41384e222e238eba4a3ea03bd340c77a403bfd Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 5 Feb 2015 10:49:27 +0100 Subject: [PATCH 186/485] Add PyPi badge --- README.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.rst b/README.rst index 967583a1..f6817743 100644 --- a/README.rst +++ b/README.rst @@ -4,6 +4,9 @@ GNS3-server .. image:: https://travis-ci.org/GNS3/gns3-server.svg?branch=master :target: https://travis-ci.org/GNS3/gns3-server +.. image:: https://img.shields.io/pypi/v/gns3-server.svg + :target: https://pypi.python.org/pypi/gns3-server + This is the GNS3 server repository. The GNS3 server manages emulators such as Dynamips, VirtualBox or Qemu/KVM. From 8367a9eb308a7fe994d724e6be7e636c54d47910 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 5 Feb 2015 11:00:42 +0100 Subject: [PATCH 187/485] Remove unused files (we can restore them later via git history) --- gns3server/handlers/auth_handler.py | 97 -------- gns3server/handlers/file_upload_handler.py | 95 -------- gns3server/start_server.py | 254 --------------------- 3 files changed, 446 deletions(-) delete mode 100644 gns3server/handlers/auth_handler.py delete mode 100644 gns3server/handlers/file_upload_handler.py delete mode 100644 gns3server/start_server.py diff --git a/gns3server/handlers/auth_handler.py b/gns3server/handlers/auth_handler.py deleted file mode 100644 index b479e533..00000000 --- a/gns3server/handlers/auth_handler.py +++ /dev/null @@ -1,97 +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 . - -""" -Simple file upload & listing handler. -""" - - -import os -import tornado.web -import tornado.websocket - -import logging -log = logging.getLogger(__name__) - - -class GNS3BaseHandler(tornado.web.RequestHandler): - - def get_current_user(self): - if 'required_user' not in self.settings: - return "FakeUser" - - user = self.get_secure_cookie("user") - if not user: - return None - - if self.settings['required_user'] == user.decode("utf-8"): - return user - - -class GNS3WebSocketBaseHandler(tornado.websocket.WebSocketHandler): - - def get_current_user(self): - if 'required_user' not in self.settings: - return "FakeUser" - - user = self.get_secure_cookie("user") - if not user: - return None - - if self.settings['required_user'] == user.decode("utf-8"): - return user - - -class LoginHandler(tornado.web.RequestHandler): - - def get(self): - self.write('
' - 'Name: ' - 'Password: ' - '' - '
') - - try: - redirect_to = self.get_argument("next") - self.set_secure_cookie("login_success_redirect_to", redirect_to) - except tornado.web.MissingArgumentError: - pass - - def post(self): - - user = self.get_argument("name") - password = self.get_argument("password") - - if self.settings['required_user'] == user and self.settings['required_pass'] == password: - self.set_secure_cookie("user", user) - auth_status = "successful" - else: - self.set_secure_cookie("user", "None") - auth_status = "failure" - - log.info("Authentication attempt {}: {}, {}".format(auth_status, user, password)) - - try: - redirect_to = self.get_secure_cookie("login_success_redirect_to") - except tornado.web.MissingArgumentError: - redirect_to = "/" - - if redirect_to is None: - self.write({'result': auth_status}) - else: - log.info('Redirecting to {}'.format(redirect_to)) - self.redirect(redirect_to) diff --git a/gns3server/handlers/file_upload_handler.py b/gns3server/handlers/file_upload_handler.py deleted file mode 100644 index 7c8fd862..00000000 --- a/gns3server/handlers/file_upload_handler.py +++ /dev/null @@ -1,95 +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 . - -""" -Simple file upload & listing handler. -""" - -# TODO: file upload with aiohttp - - -import os -import stat -import tornado.web -from .auth_handler import GNS3BaseHandler -from ..version import __version__ -from ..config import Config - -import logging -log = logging.getLogger(__name__) - - -class FileUploadHandler(GNS3BaseHandler): - - """ - File upload handler. - - :param application: Tornado Application instance - :param request: Tornado Request instance - """ - - def __init__(self, application, request, **kwargs): - - super().__init__(application, request, **kwargs) - config = Config.instance() - server_config = config.get_default_section() - self._upload_dir = os.path.expandvars( - os.path.expanduser(server_config.get("upload_directory", "~/GNS3/images"))) - self._host = request.host - try: - os.makedirs(self._upload_dir) - log.info("upload directory '{}' created".format(self._upload_dir)) - except FileExistsError: - pass - except OSError as e: - log.error("could not create the upload directory {}: {}".format(self._upload_dir, e)) - - @tornado.web.authenticated - def get(self): - """ - Invoked on GET request. - """ - - items = [] - path = self._upload_dir - for filename in os.listdir(path): - items.append(filename) - - self.render("upload.html", - version=__version__, - host=self._host, - path=path, - items=items) - - @tornado.web.authenticated - def post(self): - """ - Invoked on POST request. - """ - - if "file" in self.request.files: - fileinfo = self.request.files["file"][0] - destination_path = os.path.join(self._upload_dir, fileinfo['filename']) - try: - with open(destination_path, 'wb') as f: - f.write(fileinfo['body']) - except OSError as e: - self.write("Could not upload {}: {}".format(fileinfo['filename'], e)) - return - st = os.stat(destination_path) - os.chmod(destination_path, st.st_mode | stat.S_IXUSR) - self.redirect("/upload") diff --git a/gns3server/start_server.py b/gns3server/start_server.py deleted file mode 100644 index c603a2b9..00000000 --- a/gns3server/start_server.py +++ /dev/null @@ -1,254 +0,0 @@ -# -*- 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 . - -# __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) - -""" -Startup script for a GNS3 Server Cloud Instance. It generates certificates, -config files and usernames before finally starting the gns3server process -on the instance. -""" - -import os -import sys -import configparser -import getopt -import datetime -import signal -from logging.handlers import * -from os.path import expanduser -from gns3server.config import Config -import ast -import subprocess -import uuid - -SCRIPT_NAME = os.path.basename(__file__) - -# This is the full path when used as an import -SCRIPT_PATH = os.path.dirname(__file__) - -if not SCRIPT_PATH: - SCRIPT_PATH = os.path.join(os.path.dirname(os.path.abspath( - sys.argv[0]))) - - -LOG_NAME = "gns3-startup" -log = None - -usage = """ -USAGE: %s - -Options: - - -d, --debug Enable debugging - -i --ip The ip address of the server, for cert generation - -v, --verbose Enable verbose logging - -h, --help Display this menu :) - - --data Python dict of data to be written to the config file: - " { 'gns3' : 'Is AWESOME' } " - -""" % (SCRIPT_NAME) - - -def parse_cmd_line(argv): - """ - Parse command line arguments - - argv: Passed in sys.argv - """ - - short_args = "dvh" - long_args = ("debug", - "ip=", - "verbose", - "help", - "data=", - ) - try: - opts, extra_opts = getopt.getopt(argv[1:], short_args, long_args) - except getopt.GetoptError as e: - print("Unrecognized command line option or missing required argument: %s" % (e)) - print(usage) - sys.exit(2) - - cmd_line_option_list = {'debug': False, 'verbose': True, 'data': None} - - if sys.platform == "linux": - cmd_line_option_list['syslog'] = "/dev/log" - elif sys.platform == "osx": - cmd_line_option_list['syslog'] = "/var/run/syslog" - else: - cmd_line_option_list['syslog'] = ('localhost', 514) - - for opt, val in opts: - if opt in ("-h", "--help"): - print(usage) - sys.exit(0) - elif opt in ("-d", "--debug"): - cmd_line_option_list["debug"] = True - elif opt in ("--ip",): - cmd_line_option_list["ip"] = val - elif opt in ("-v", "--verbose"): - cmd_line_option_list["verbose"] = True - elif opt in ("--data",): - cmd_line_option_list["data"] = ast.literal_eval(val) - - return cmd_line_option_list - - -def set_logging(cmd_options): - """ - Setup logging and format output for console and syslog - - Syslog is using the KERN facility - """ - log = logging.getLogger("%s" % (LOG_NAME)) - log_level = logging.INFO - log_level_console = logging.WARNING - - if cmd_options['verbose'] is True: - log_level_console = logging.INFO - - if cmd_options['debug'] is True: - log_level_console = logging.DEBUG - log_level = logging.DEBUG - - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - sys_formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s') - - console_log = logging.StreamHandler() - console_log.setLevel(log_level_console) - console_log.setFormatter(formatter) - - syslog_handler = SysLogHandler( - address=cmd_options['syslog'], - facility=SysLogHandler.LOG_KERN - ) - - syslog_handler.setFormatter(sys_formatter) - - log.setLevel(log_level) - log.addHandler(console_log) - log.addHandler(syslog_handler) - - return log - - -def _generate_certs(options): - """ - Generate a self-signed certificate for SSL-enabling the WebSocket - connection. The certificate is sent back to the client so it can - verify the authenticity of the server. - - :return: A 2-tuple of strings containing (server_key, server_cert) - """ - cmd = ["{}/cert_utils/create_cert.sh".format(SCRIPT_PATH), options['ip']] - log.debug("Generating certs with cmd: {}".format(' '.join(cmd))) - output_raw = subprocess.check_output(cmd, shell=False, - stderr=subprocess.STDOUT) - - output_str = output_raw.decode("utf-8") - output = output_str.strip().split("\n") - log.debug(output) - return (output[-2], output[-1]) - - -def _start_gns3server(): - """ - Start up the gns3 server. - - :return: None - """ - cmd = 'gns3server --quiet > /tmp/gns3.log 2>&1 &' - log.info("Starting gns3server with cmd {}".format(cmd)) - os.system(cmd) - - -def main(): - - global log - options = parse_cmd_line(sys.argv) - log = set_logging(options) - - def _shutdown(signalnum=None, frame=None): - """ - Handles the SIGINT and SIGTERM event, inside of main so it has access to - the log vars. - """ - - log.info("Received shutdown signal") - sys.exit(0) - - # Setup signal to catch Control-C / SIGINT and SIGTERM - signal.signal(signal.SIGINT, _shutdown) - signal.signal(signal.SIGTERM, _shutdown) - - client_data = {} - - config = Config.instance() - cfg = config.list_cloud_config_file() - cfg_path = os.path.dirname(cfg) - - try: - os.makedirs(cfg_path) - except FileExistsError: - pass - - (server_key, server_crt) = _generate_certs(options) - - cloud_config = configparser.ConfigParser() - cloud_config['CLOUD_SERVER'] = {} - - if options['data']: - cloud_config['CLOUD_SERVER'] = options['data'] - - cloud_config['CLOUD_SERVER']['SSL_KEY'] = server_key - cloud_config['CLOUD_SERVER']['SSL_CRT'] = server_crt - cloud_config['CLOUD_SERVER']['SSL_ENABLED'] = 'no' - cloud_config['CLOUD_SERVER']['WEB_USERNAME'] = str(uuid.uuid4()).upper()[0:8] - cloud_config['CLOUD_SERVER']['WEB_PASSWORD'] = str(uuid.uuid4()).upper()[0:8] - - with open(cfg, 'w') as cloud_config_file: - cloud_config.write(cloud_config_file) - - _start_gns3server() - - with open(server_crt, 'r') as cert_file: - cert_data = cert_file.readlines() - - cert_file.close() - - # Return a stringified dictionary on stdout. The gui captures this to get - # things like the server cert. - client_data['SSL_CRT_FILE'] = server_crt - client_data['SSL_CRT'] = cert_data - client_data['WEB_USERNAME'] = cloud_config['CLOUD_SERVER']['WEB_USERNAME'] - client_data['WEB_PASSWORD'] = cloud_config['CLOUD_SERVER']['WEB_PASSWORD'] - print(client_data) - return 0 - - -if __name__ == "__main__": - result = main() - sys.exit(result) From dae48b2de4791b270c4e7a4e2bb127289b1f4887 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 5 Feb 2015 11:39:32 +0100 Subject: [PATCH 188/485] Update temporary status if project change location This avoid race condition during file move. --- gns3server/modules/project.py | 8 ++++++++ tests/modules/test_project.py | 24 ++++++++++++++++++++---- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index db4f0cfe..ac74fd58 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -114,6 +114,7 @@ class Project: raise aiohttp.web.HTTPForbidden(text="You are not allowed to modifiy the project directory location") self._path = path + self._update_temporary_file() @property def vms(self): @@ -132,6 +133,13 @@ class Project: return self._temporary = temporary + self._update_temporary_file() + + def _update_temporary_file(self): + """ + Update the .gns3_temporary file in order to reflect current + project status. + """ if self._temporary: try: diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py index c7ce93e5..45254a1f 100644 --- a/tests/modules/test_project.py +++ b/tests/modules/test_project.py @@ -53,21 +53,37 @@ def test_path(tmpdir): p = Project(location=str(tmpdir)) assert p.path == os.path.join(str(tmpdir), p.id) assert os.path.exists(os.path.join(str(tmpdir), p.id)) - assert not os.path.exists(os.path.join(p.path, '.gns3_temporary')) + assert not os.path.exists(os.path.join(p.path, ".gns3_temporary")) + + +def test_changing_path_temporary_flag(tmpdir): + + with patch("gns3server.config.Config.get_section_config", return_value={"local": True}): + p = Project(temporary=True) + assert os.path.exists(p.path) + assert os.path.exists(os.path.join(p.path, ".gns3_temporary")) + p.temporary = False + assert not os.path.exists(os.path.join(p.path, ".gns3_temporary")) + + with open(str(tmpdir / ".gns3_temporary"), "w+") as f: + f.write("1") + + p.path = str(tmpdir) + assert not os.path.exists(os.path.join(str(tmpdir), ".gns3_temporary")) def test_temporary_path(): p = Project(temporary=True) assert os.path.exists(p.path) - assert os.path.exists(os.path.join(p.path, '.gns3_temporary')) + assert os.path.exists(os.path.join(p.path, ".gns3_temporary")) def test_remove_temporary_flag(): p = Project(temporary=True) assert os.path.exists(p.path) - assert os.path.exists(os.path.join(p.path, '.gns3_temporary')) + assert os.path.exists(os.path.join(p.path, ".gns3_temporary")) p.temporary = False - assert not os.path.exists(os.path.join(p.path, '.gns3_temporary')) + assert not os.path.exists(os.path.join(p.path, ".gns3_temporary")) def test_changing_location_not_allowed(tmpdir): From b92e065add93bd14d17890d382078777cb8b97fd Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 5 Feb 2015 12:00:34 +0100 Subject: [PATCH 189/485] Fix binary location change for VPCS --- gns3server/modules/vpcs/vpcs_vm.py | 31 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index 1f1fa796..9c887aee 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -57,7 +57,6 @@ class VPCSVM(BaseVM): super().__init__(name, vm_id, project, manager) - self._path = manager.config.get_section_config("VPCS").get("vpcs_path", "vpcs") self._console = console self._command = [] self._process = None @@ -87,18 +86,15 @@ class VPCSVM(BaseVM): """ Check if VPCS is available with the correct version """ - - if self._path == "vpcs": - self._path = shutil.which("vpcs") - - if not self._path: + path = self.vpcs_path + if not path: raise VPCSError("No path to a VPCS executable has been set") - if not os.path.isfile(self._path): - raise VPCSError("VPCS program '{}' is not accessible".format(self._path)) + if not os.path.isfile(path): + raise VPCSError("VPCS program '{}' is not accessible".format(path)) - if not os.access(self._path, os.X_OK): - raise VPCSError("VPCS program '{}' is not executable".format(self._path)) + if not os.access(path, os.X_OK): + raise VPCSError("VPCS program '{}' is not executable".format(path)) yield from self._check_vpcs_version() @@ -119,7 +115,10 @@ class VPCSVM(BaseVM): :returns: path to VPCS """ - return self._path + path = self._manager.config.get_section_config("VPCS").get("vpcs_path", "vpcs") + if path == "vpcs": + path = shutil.which("vpcs") + return path @property def console(self): @@ -211,13 +210,13 @@ class VPCSVM(BaseVM): if parse_version(version) < parse_version("0.5b1"): raise VPCSError("VPCS executable version must be >= 0.5b1") else: - raise VPCSError("Could not determine the VPCS version for {}".format(self._path)) + raise VPCSError("Could not determine the VPCS version for {}".format(self.vpcs_path)) except (OSError, subprocess.SubprocessError) as e: raise VPCSError("Error while looking for the VPCS version: {}".format(e)) @asyncio.coroutine def _get_vpcs_welcome(self): - proc = yield from asyncio.create_subprocess_exec(self._path, "-v", stdout=asyncio.subprocess.PIPE, cwd=self.working_dir) + proc = yield from asyncio.create_subprocess_exec(self.vpcs_path, "-v", stdout=asyncio.subprocess.PIPE, cwd=self.working_dir) out = yield from proc.stdout.read() return out.decode("utf-8") @@ -251,8 +250,8 @@ class VPCSVM(BaseVM): self._started = True except (OSError, subprocess.SubprocessError) as e: vpcs_stdout = self.read_vpcs_stdout() - log.error("Could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout)) - raise VPCSError("Could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout)) + log.error("Could not start VPCS {}: {}\n{}".format(self.vpcs_path, e, vpcs_stdout)) + raise VPCSError("Could not start VPCS {}: {}\n{}".format(self.vpcs_path, e, vpcs_stdout)) @asyncio.coroutine def stop(self): @@ -395,7 +394,7 @@ class VPCSVM(BaseVM): """ - command = [self._path] + command = [self.vpcs_path] command.extend(["-p", str(self._console)]) # listen to console port nio = self._ethernet_adapter.get_nio(0) From 869405738e79910282b03ec35a7a5720ffa08ca2 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 5 Feb 2015 13:55:53 +0100 Subject: [PATCH 190/485] Code cleanup --- gns3server/web/documentation.py | 4 ++-- gns3server/web/response.py | 14 ++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/gns3server/web/documentation.py b/gns3server/web/documentation.py index 04b5cfd3..6b15b7e0 100644 --- a/gns3server/web/documentation.py +++ b/gns3server/web/documentation.py @@ -85,7 +85,7 @@ class Documentation(object): self._write_json_schema(f, schema['definitions'][definition]) f.write("Body\n+++++++++\n") - def _write_json_schema_object(self, f, obj, schema): + def _write_json_schema_object(self, f, obj): """ obj is current object in JSON schema schema is the whole schema including definitions @@ -126,7 +126,7 @@ class Documentation(object): Type \ Description \ \n") - self._write_json_schema_object(f, schema, schema) + self._write_json_schema_object(f, schema) f.write(" \n\n") diff --git a/gns3server/web/response.py b/gns3server/web/response.py index e252b0b6..eff5b165 100644 --- a/gns3server/web/response.py +++ b/gns3server/web/response.py @@ -44,15 +44,13 @@ class Response(aiohttp.web.Response): log.debug(data) return super().write(data) - """ - Set the response content type to application/json and serialize - the content. - - :param anwser The response as a Python object - """ - def json(self, answer): - """Pass a Python object and return a JSON as answer""" + """ + Set the response content type to application/json and serialize + the content. + + :param anwser The response as a Python object + """ self.content_type = "application/json" if hasattr(answer, '__json__'): From 41a887281956a1fb77e095631abc79e1b58f45d1 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 5 Feb 2015 14:20:01 +0100 Subject: [PATCH 191/485] Refactor VPCS script file loading This allow to support moving the project on disk --- gns3server/modules/vpcs/vpcs_vm.py | 40 ++++++++++++++---------------- gns3server/schemas/vpcs.py | 4 --- tests/api/test_vpcs.py | 1 - tests/modules/vpcs/test_vpcs_vm.py | 7 +++--- 4 files changed, 22 insertions(+), 30 deletions(-) diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index 9c887aee..ff87cde9 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -49,11 +49,10 @@ class VPCSVM(BaseVM): :param project: Project instance :param manager: parent VM Manager :param console: TCP console port - :param script_file: A VPCS startup script :param startup_script: Content of vpcs startup script file """ - def __init__(self, name, vm_id, project, manager, console=None, script_file=None, startup_script=None): + def __init__(self, name, vm_id, project, manager, console=None, startup_script=None): super().__init__(name, vm_id, project, manager) @@ -64,7 +63,6 @@ class VPCSVM(BaseVM): self._started = False # VPCS settings - self._script_file = script_file if startup_script is not None: self.startup_script = startup_script self._ethernet_adapter = EthernetAdapter() # one adapter with 1 Ethernet interface @@ -104,7 +102,6 @@ class VPCSVM(BaseVM): "vm_id": self.id, "console": self._console, "project_id": self.project.id, - "script_file": self.script_file, "startup_script": self.startup_script} @property @@ -152,7 +149,7 @@ class VPCSVM(BaseVM): :param new_name: name """ - if self._script_file: + if self.script_file: content = self.startup_script content = content.replace(self._name, new_name) self.startup_script = content @@ -163,19 +160,15 @@ class VPCSVM(BaseVM): def startup_script(self): """Return the content of the current startup script""" - if self._script_file is None: - # If the default VPCS file exist we use it - path = os.path.join(self.working_dir, 'startup.vpc') - if os.path.exists(path): - self._script_file = path - else: - return None + script_file = self.script_file + if script_file is None: + return None try: - with open(self._script_file) as f: + with open(script_file) as f: return f.read() except OSError as e: - raise VPCSError("Can't read VPCS startup file '{}'".format(self._script_file)) + raise VPCSError("Can't read VPCS startup file '{}'".format(script_file)) @startup_script.setter def startup_script(self, startup_script): @@ -185,17 +178,16 @@ class VPCSVM(BaseVM): :param startup_script The content of the vpcs startup script """ - if self._script_file is None: - self._script_file = os.path.join(self.working_dir, 'startup.vpcs') try: - with open(self._script_file, 'w+') as f: + script_file = os.path.join(self.working_dir, 'startup.vpc') + with open(script_file, 'w+') as f: if startup_script is None: f.write('') else: startup_script = startup_script.replace("%h", self._name) f.write(startup_script) except OSError as e: - raise VPCSError("Can't write VPCS startup file '{}'".format(self._script_file)) + raise VPCSError("Can't write VPCS startup file '{}'".format(self.script_file)) @asyncio.coroutine def _check_vpcs_version(self): @@ -413,8 +405,9 @@ class VPCSVM(BaseVM): command.extend(["-m", str(self._manager.get_mac_id(self.id))]) # the unique ID is used to set the MAC address offset command.extend(["-i", "1"]) # option to start only one VPC instance command.extend(["-F"]) # option to avoid the daemonization of VPCS - if self._script_file: - command.extend([self._script_file]) + + if self.script_file: + command.extend([self.script_file]) return command @property @@ -425,4 +418,9 @@ class VPCSVM(BaseVM): :returns: path to script-file """ - return self._script_file + # If the default VPCS file exist we use it + path = os.path.join(self.working_dir, 'startup.vpc') + if os.path.exists(path): + return path + else: + return None diff --git a/gns3server/schemas/vpcs.py b/gns3server/schemas/vpcs.py index db446237..fe9cc8e8 100644 --- a/gns3server/schemas/vpcs.py +++ b/gns3server/schemas/vpcs.py @@ -158,10 +158,6 @@ VPCS_OBJECT_SCHEMA = { "maxLength": 36, "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" }, - "script_file": { - "description": "VPCS startup script", - "type": ["string", "null"] - }, "startup_script": { "description": "Content of the VPCS startup script", "type": ["string", "null"] diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index fb614693..de8c5b76 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -34,7 +34,6 @@ def test_vpcs_create(server, project): assert response.route == "/{project_id}/vpcs/vms" assert response.json["name"] == "PC TEST 1" assert response.json["project_id"] == project.id - assert response.json["script_file"] is None def test_vpcs_get(server, project, vm): diff --git a/tests/modules/vpcs/test_vpcs_vm.py b/tests/modules/vpcs/test_vpcs_vm.py index 9017d5d8..b69d5bef 100644 --- a/tests/modules/vpcs/test_vpcs_vm.py +++ b/tests/modules/vpcs/test_vpcs_vm.py @@ -134,7 +134,7 @@ def test_port_remove_nio_binding(vm): def test_update_startup_script(vm): content = "echo GNS3 VPCS\nip 192.168.1.2\n" vm.startup_script = content - filepath = os.path.join(vm.working_dir, 'startup.vpcs') + filepath = os.path.join(vm.working_dir, 'startup.vpc') assert os.path.exists(filepath) with open(filepath) as f: assert f.read() == content @@ -143,7 +143,7 @@ def test_update_startup_script(vm): def test_update_startup_script(vm): content = "echo GNS3 VPCS\nip 192.168.1.2\n" vm.startup_script = content - filepath = os.path.join(vm.working_dir, 'startup.vpcs') + filepath = os.path.join(vm.working_dir, 'startup.vpc') assert os.path.exists(filepath) with open(filepath) as f: assert f.read() == content @@ -192,11 +192,10 @@ def test_change_console_port(vm, port_manager): def test_change_name(vm, tmpdir): - path = os.path.join(str(tmpdir), 'startup.vpcs') + path = os.path.join(vm.working_dir, 'startup.vpc') vm.name = "world" with open(path, 'w+') as f: f.write("name world") - vm._script_file = path vm.name = "hello" assert vm.name == "hello" with open(path) as f: From 2786d0f044fddef2534e2b4c9e90fde8d12e5d2a Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 5 Feb 2015 15:04:59 +0100 Subject: [PATCH 192/485] Update aiohttp 0.14.4 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b7c04a29..701f290b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,4 @@ pycurl==7.19.5 python-dateutil==2.3 apache-libcloud==0.16.0 requests==2.5.0 -aiohttp==0.14.2 +aiohttp==0.14.4 From 0abf2e82d6dcf46a18b5734bc8ff9d088b8f828c Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 5 Feb 2015 15:35:52 +0100 Subject: [PATCH 193/485] Improve server debug logging --- gns3server/server.py | 3 ++- gns3server/web/request_handler.py | 28 ++++++++++++++++++++++++++++ gns3server/web/response.py | 15 +++++++++------ 3 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 gns3server/web/request_handler.py diff --git a/gns3server/server.py b/gns3server/server.py index 3e19cdcf..349880e5 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -29,6 +29,7 @@ import types import time from .web.route import Route +from .web.request_handler import RequestHandler from .config import Config from .modules import MODULES from .modules.port_manager import PortManager @@ -54,7 +55,7 @@ class Server: def _run_application(self, app, ssl_context=None): try: - server = yield from self._loop.create_server(app.make_handler(), self._host, self._port, ssl=ssl_context) + server = yield from self._loop.create_server(app.make_handler(handler=RequestHandler), self._host, self._port, ssl=ssl_context) except OSError as e: log.critical("Could not start the server: {}".format(e)) self._loop.stop() diff --git a/gns3server/web/request_handler.py b/gns3server/web/request_handler.py new file mode 100644 index 00000000..b7fbb121 --- /dev/null +++ b/gns3server/web/request_handler.py @@ -0,0 +1,28 @@ +# -*- 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 aiohttp.web +import logging + + +class RequestHandler(aiohttp.web.RequestHandler): + + def log_access(self, message, environ, response, time): + # In debug mode we don't use the standard request log but a more complete in response.py + print(self.logger.getEffectiveLevel()) + if self.logger.getEffectiveLevel() != logging.DEBUG: + super().log_access(message, environ, response, time) diff --git a/gns3server/web/response.py b/gns3server/web/response.py index eff5b165..366b8a6c 100644 --- a/gns3server/web/response.py +++ b/gns3server/web/response.py @@ -36,14 +36,17 @@ class Response(aiohttp.web.Response): super().__init__(headers=headers, **kwargs) def start(self, request): - log.debug("{} {}".format(self.status, self.reason)) - log.debug(dict(self.headers)) + if log.getEffectiveLevel() == logging.DEBUG: + log.info("%s %s", request.method, request.path_qs) + log.debug("%s", dict(request.headers)) + if isinstance(request.json, dict): + log.debug("%s", request.json) + log.info("Response: %d %s", self.status, self.reason) + log.debug(dict(self.headers)) + if hasattr(self, 'body'): + log.debug(json.loads(self.body.decode('utf-8'))) return super().start(request) - def write(self, data): - log.debug(data) - return super().write(data) - def json(self, answer): """ Set the response content type to application/json and serialize From 30f10a559e5ef64988a2ac89cbc120d71d05e325 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 5 Feb 2015 16:34:18 +0100 Subject: [PATCH 194/485] Fix crash in debug log --- gns3server/web/response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/web/response.py b/gns3server/web/response.py index 366b8a6c..9ae3c1a6 100644 --- a/gns3server/web/response.py +++ b/gns3server/web/response.py @@ -43,7 +43,7 @@ class Response(aiohttp.web.Response): log.debug("%s", request.json) log.info("Response: %d %s", self.status, self.reason) log.debug(dict(self.headers)) - if hasattr(self, 'body'): + if hasattr(self, 'body') and self.body is not None: log.debug(json.loads(self.body.decode('utf-8'))) return super().start(request) From 9f7b8574c83bcafdf0e9887ed3f3e2caa83d98c7 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 5 Feb 2015 17:15:40 +0100 Subject: [PATCH 195/485] Useless print --- gns3server/web/request_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/web/request_handler.py b/gns3server/web/request_handler.py index b7fbb121..17e0ab90 100644 --- a/gns3server/web/request_handler.py +++ b/gns3server/web/request_handler.py @@ -22,7 +22,7 @@ import logging class RequestHandler(aiohttp.web.RequestHandler): def log_access(self, message, environ, response, time): + # In debug mode we don't use the standard request log but a more complete in response.py - print(self.logger.getEffectiveLevel()) if self.logger.getEffectiveLevel() != logging.DEBUG: super().log_access(message, environ, response, time) From 5a0c224292d6ffb6d3feef5874338893d4bcc2fb Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 5 Feb 2015 17:52:37 +0100 Subject: [PATCH 196/485] Allow user to change project path on local server --- gns3server/handlers/project_handler.py | 2 +- gns3server/modules/project.py | 22 ++++++++++++++++------ gns3server/schemas/project.py | 4 ++-- tests/api/test_project.py | 15 +++++++++------ tests/modules/test_project.py | 7 +++++++ 5 files changed, 35 insertions(+), 15 deletions(-) diff --git a/gns3server/handlers/project_handler.py b/gns3server/handlers/project_handler.py index 382206d0..0ab4d681 100644 --- a/gns3server/handlers/project_handler.py +++ b/gns3server/handlers/project_handler.py @@ -32,7 +32,7 @@ class ProjectHandler: pm = ProjectManager.instance() p = pm.create_project( - location=request.json.get("location"), + path=request.json.get("path"), project_id=request.json.get("project_id"), temporary=request.json.get("temporary", False) ) diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index ac74fd58..cfddbda4 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -35,11 +35,12 @@ class Project: In theory VM are isolated project/project. :param project_id: Force project identifier (None by default auto generate an UUID) + :param path: Path of the project. (None use the standard directory) :param location: Parent path of the project. (None should create a tmp directory) :param temporary: Boolean the project is a temporary project (destroy when closed) """ - def __init__(self, project_id=None, location=None, temporary=False): + def __init__(self, project_id=None, path=None, location=None, temporary=False): if project_id is None: self._id = str(uuid4()) @@ -58,12 +59,17 @@ class Project: self._vms = set() self._vms_to_destroy = set() - self._path = os.path.join(self._location, self._id) + + self.temporary = temporary + + if path is None: + path = os.path.join(self._location, self._id) try: - os.makedirs(self._path, exist_ok=True) + os.makedirs(path, exist_ok=True) except OSError as e: raise aiohttp.web.HTTPInternalServerError(text="Could not create project directory: {}".format(e)) - self.temporary = temporary + self.path = path + log.debug("Create project {id} in directory {path}".format(path=self._path, id=self._id)) def _config(self): @@ -110,8 +116,9 @@ class Project: @path.setter def path(self, path): - if path != self._path and self._config().get("local", False) is False: - raise aiohttp.web.HTTPForbidden(text="You are not allowed to modifiy the project directory location") + if hasattr(self, "_path"): + if path != self._path and self._config().get("local", False) is False: + raise aiohttp.web.HTTPForbidden(text="You are not allowed to modifiy the project directory location") self._path = path self._update_temporary_file() @@ -141,6 +148,9 @@ class Project: project status. """ + if not hasattr(self, "_path"): + return + if self._temporary: try: with open(os.path.join(self._path, ".gns3_temporary"), 'w+') as f: diff --git a/gns3server/schemas/project.py b/gns3server/schemas/project.py index 942b154b..9ebcfba0 100644 --- a/gns3server/schemas/project.py +++ b/gns3server/schemas/project.py @@ -21,8 +21,8 @@ PROJECT_CREATE_SCHEMA = { "description": "Request validation to create a new Project instance", "type": "object", "properties": { - "location": { - "description": "Base directory where the project should be created on remote server", + "path": { + "description": "Project directory", "type": "string", "minLength": 1 }, diff --git a/tests/api/test_project.py b/tests/api/test_project.py index c7383bf7..df684018 100644 --- a/tests/api/test_project.py +++ b/tests/api/test_project.py @@ -24,11 +24,11 @@ from unittest.mock import patch from tests.utils import asyncio_patch -def test_create_project_with_dir(server, tmpdir): +def test_create_project_with_path(server, tmpdir): with patch("gns3server.config.Config.get_section_config", return_value={"local": True}): - response = server.post("/projects", {"location": str(tmpdir)}) + response = server.post("/projects", {"path": str(tmpdir)}) assert response.status == 200 - assert response.json["location"] == str(tmpdir) + assert response.json["path"] == str(tmpdir) def test_create_project_without_dir(server): @@ -55,13 +55,16 @@ def test_create_project_with_uuid(server): def test_show_project(server): - query = {"project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f", "location": "/tmp", "temporary": False} + query = {"project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f", "path": "/tmp", "temporary": False} with patch("gns3server.config.Config.get_section_config", return_value={"local": True}): response = server.post("/projects", query) assert response.status == 200 response = server.get("/projects/00010203-0405-0607-0809-0a0b0c0d0e0f", example=True) - query["path"] = "/tmp/00010203-0405-0607-0809-0a0b0c0d0e0f" - assert response.json == query + assert len(response.json.keys()) == 4 + assert len(response.json["location"]) > 0 + assert response.json["project_id"] == "00010203-0405-0607-0809-0a0b0c0d0e0f" + assert response.json["path"] == "/tmp" + assert response.json["temporary"] is False def test_show_project_invalid_uuid(server): diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py index 45254a1f..fa0df924 100644 --- a/tests/modules/test_project.py +++ b/tests/modules/test_project.py @@ -56,6 +56,13 @@ def test_path(tmpdir): assert not os.path.exists(os.path.join(p.path, ".gns3_temporary")) +def test_init_path(tmpdir): + + with patch("gns3server.config.Config.get_section_config", return_value={"local": True}): + p = Project(path=str(tmpdir)) + assert p.path == str(tmpdir) + + def test_changing_path_temporary_flag(tmpdir): with patch("gns3server.config.Config.get_section_config", return_value={"local": True}): From ab122d969ed611e32c9633d1ec63570dc59222f3 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 5 Feb 2015 17:57:51 +0100 Subject: [PATCH 197/485] Allow empty project directory --- gns3server/schemas/project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/schemas/project.py b/gns3server/schemas/project.py index 9ebcfba0..7bd8b723 100644 --- a/gns3server/schemas/project.py +++ b/gns3server/schemas/project.py @@ -23,7 +23,7 @@ PROJECT_CREATE_SCHEMA = { "properties": { "path": { "description": "Project directory", - "type": "string", + "type": ["string", "null"], "minLength": 1 }, "project_id": { From f2ff933b203b27f988a613dcb6aae8e3836cb1d6 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 5 Feb 2015 11:58:10 -0700 Subject: [PATCH 198/485] Fixes console and close in VirtualBox VM. --- gns3server/modules/virtualbox/virtualbox_vm.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index bc59ab6d..924dbfb2 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -327,7 +327,7 @@ class VirtualBoxVM(BaseVM): # VM is already closed return - self.stop() + yield from self.stop() if self._console: self._manager.port_manager.release_console_port(self._console) @@ -450,6 +450,11 @@ class VirtualBoxVM(BaseVM): # yield from self._modify_vm('--name "{}"'.format(vmname)) self._vmname = vmname + @asyncio.coroutine + def rename(self): + + pass + @asyncio.coroutine def set_adapters(self, adapters): """ @@ -494,7 +499,8 @@ class VirtualBoxVM(BaseVM): """ self._adapter_start_index = adapter_start_index - self.adapters = self.adapters # this forces to recreate the adapter list with the correct index + # TODO: get rid of adapter start index + #self.adapters = self.adapters # this forces to recreate the adapter list with the correct index log.info("VirtualBox VM '{name}' [{id}]: adapter start index changed to {index}".format(name=self.name, id=self.id, index=adapter_start_index)) @@ -518,7 +524,6 @@ class VirtualBoxVM(BaseVM): """ self._adapter_type = adapter_type - log.info("VirtualBox VM '{name}' [{id}]: adapter type changed to {adapter_type}".format(name=self.name, id=self.id, adapter_type=adapter_type)) @@ -723,7 +728,7 @@ class VirtualBoxVM(BaseVM): self._serial_pipe = open(pipe_name, "a+b") except OSError as e: raise VirtualBoxError("Could not open the pipe {}: {}".format(pipe_name, e)) - self._telnet_server_thread = TelnetServer(self._vmname, msvcrt.get_osfhandle(self._serial_pipe.fileno()), self._console_host, self._console) + self._telnet_server_thread = TelnetServer(self._vmname, msvcrt.get_osfhandle(self._serial_pipe.fileno()), self._manager.port_manager.console_host, self._console) self._telnet_server_thread.start() else: try: @@ -731,7 +736,7 @@ class VirtualBoxVM(BaseVM): self._serial_pipe.connect(pipe_name) except OSError as e: raise VirtualBoxError("Could not connect to the pipe {}: {}".format(pipe_name, e)) - self._telnet_server_thread = TelnetServer(self._vmname, self._serial_pipe, self._console_host, self._console) + self._telnet_server_thread = TelnetServer(self._vmname, self._serial_pipe, self._manager.port_manager.console_host, self._console) self._telnet_server_thread.start() def _stop_remote_console(self): From 8118d7762f87867bdaa72befbc63a634c3355f9d Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 5 Feb 2015 14:24:06 -0700 Subject: [PATCH 199/485] Parallel execution when closing VMs. --- gns3server/modules/base_manager.py | 16 +++++++++++----- gns3server/modules/project.py | 12 +++++++++++- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 52a224c9..999c2bc3 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -97,12 +97,18 @@ class BaseManager: @asyncio.coroutine def unload(self): + tasks = [] for vm_id in self._vms.keys(): - try: - yield from self.close_vm(vm_id) - except Exception as e: - log.error("Could not delete VM {}: {}".format(vm_id, e), exc_info=1) - continue + tasks.append(asyncio.async(self.close_vm(vm_id))) + + if tasks: + done, _ = yield from asyncio.wait(tasks) + for future in done: + try: + future.result() + except Exception as e: + log.error("Could not close VM {}".format(e), exc_info=1) + continue if hasattr(BaseManager, "_instance"): BaseManager._instance = None diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index cfddbda4..64627805 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -243,11 +243,21 @@ class Project: :param cleanup: If True drop the project directory """ + tasks = [] for vm in self._vms: if asyncio.iscoroutinefunction(vm.close): - yield from vm.close() + tasks.append(asyncio.async(vm.close())) else: vm.close() + + if tasks: + done, _ = yield from asyncio.wait(tasks) + for future in done: + try: + future.result() + except Exception as e: + log.error("Could not close VM {}".format(e), exc_info=1) + if cleanup and os.path.exists(self.path): try: yield from wait_run_in_executor(shutil.rmtree, self.path) From 5c3969ae79e6ec5356433b28d438c4b4ce5e25e0 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 6 Feb 2015 11:14:56 +0100 Subject: [PATCH 200/485] Fix tests creating garbage project in ~/GNS3/project --- tests/conftest.py | 8 +++++++- tests/modules/test_project.py | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 113022ee..0ab02c19 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -107,7 +107,11 @@ def free_console_port(request, port_manager): @pytest.yield_fixture(autouse=True) -def run_around_tests(): +def run_around_tests(monkeypatch): + """ + This setup a temporay project file environnement around tests + """ + tmppath = tempfile.mkdtemp() config = Config.instance() @@ -115,6 +119,8 @@ def run_around_tests(): server_section["project_directory"] = tmppath config.set_section_config("Server", server_section) + monkeypatch.setattr("gns3server.modules.project.Project._get_default_project_directory", lambda *args: tmppath) + yield # An helper should not raise Exception diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py index fa0df924..f1d5775f 100644 --- a/tests/modules/test_project.py +++ b/tests/modules/test_project.py @@ -198,8 +198,9 @@ def test_project_close_temporary_project(loop, manager): assert os.path.exists(directory) is False -def test_get_default_project_directory(): +def test_get_default_project_directory(monkeypatch): + monkeypatch.undo() project = Project() path = os.path.normpath(os.path.expanduser("~/GNS3/projects")) assert project._get_default_project_directory() == path From 571044b3e8ffffbfbaded6716eaf560ec7ce6806 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 6 Feb 2015 11:31:54 +0100 Subject: [PATCH 201/485] Fix server close tests --- gns3server/modules/virtualbox/virtualbox_vm.py | 2 +- tests/modules/test_project.py | 3 ++- tests/utils.py | 5 ++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 924dbfb2..49ad1cc2 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -500,7 +500,7 @@ class VirtualBoxVM(BaseVM): self._adapter_start_index = adapter_start_index # TODO: get rid of adapter start index - #self.adapters = self.adapters # this forces to recreate the adapter list with the correct index + # self.adapters = self.adapters # this forces to recreate the adapter list with the correct index log.info("VirtualBox VM '{name}' [{id}]: adapter start index changed to {index}".format(name=self.name, id=self.id, index=adapter_start_index)) diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py index f1d5775f..b2ce3466 100644 --- a/tests/modules/test_project.py +++ b/tests/modules/test_project.py @@ -24,6 +24,7 @@ import shutil from uuid import uuid4 from unittest.mock import patch +from tests.utils import asyncio_patch from gns3server.modules.project import Project from gns3server.modules.vpcs import VPCS, VPCSVM @@ -183,7 +184,7 @@ def test_project_close(loop, manager): project = Project() vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) project.add_vm(vm) - with patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM.close") as mock: + with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM.close") as mock: loop.run_until_complete(asyncio.async(project.close())) assert mock.called diff --git a/tests/utils.py b/tests/utils.py index bc8dc5dc..b8994f66 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -48,7 +48,10 @@ class _asyncio_patch: def _fake_anwser(self): future = asyncio.Future() - future.set_result(self.kwargs["return_value"]) + if "return_value" in self.kwargs: + future.set_result(self.kwargs["return_value"]) + else: + future.set_result(True) return future From 27cbfbbdc6586cf51cf8f4fe14855960822f2cb5 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 6 Feb 2015 12:35:31 +0100 Subject: [PATCH 202/485] Useless requirement --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 701f290b..1c9c8dbc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ netifaces==0.10.4 jsonschema==2.4.0 -pycurl==7.19.5 python-dateutil==2.3 apache-libcloud==0.16.0 requests==2.5.0 From e81dcd4bbacd266fbb66f4511fae208d5b9bc3f4 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 6 Feb 2015 17:42:25 +0100 Subject: [PATCH 203/485] Add /projects before /project --- gns3server/config.py | 3 +- gns3server/handlers/virtualbox_handler.py | 26 +++++++------- gns3server/handlers/vpcs_handler.py | 18 +++++----- gns3server/main.py | 1 + tests/api/test_virtualbox.py | 30 ++++++++-------- tests/api/test_vpcs.py | 42 +++++++++++------------ 6 files changed, 61 insertions(+), 59 deletions(-) diff --git a/gns3server/config.py b/gns3server/config.py index 07761bab..848ab15d 100644 --- a/gns3server/config.py +++ b/gns3server/config.py @@ -182,10 +182,11 @@ class Config(object): self._override_config[section] = content @staticmethod - def instance(): + def instance(files=[]): """ Singleton to return only on instance of Config. + :params files: Array of configuration files (optionnal) :returns: instance of Config """ diff --git a/gns3server/handlers/virtualbox_handler.py b/gns3server/handlers/virtualbox_handler.py index 61ad2556..0b62b253 100644 --- a/gns3server/handlers/virtualbox_handler.py +++ b/gns3server/handlers/virtualbox_handler.py @@ -47,7 +47,7 @@ class VirtualBoxHandler: @classmethod @Route.post( - r"/{project_id}/virtualbox/vms", + r"/projects/{project_id}/virtualbox/vms", parameters={ "project_id": "UUID for the project" }, @@ -78,7 +78,7 @@ class VirtualBoxHandler: @classmethod @Route.get( - r"/{project_id}/virtualbox/vms/{vm_id}", + r"/projects/{project_id}/virtualbox/vms/{vm_id}", parameters={ "project_id": "UUID for the project", "vm_id": "UUID for the instance" @@ -98,7 +98,7 @@ class VirtualBoxHandler: @classmethod @Route.put( - r"/{project_id}/virtualbox/vms/{vm_id}", + r"/projects/{project_id}/virtualbox/vms/{vm_id}", parameters={ "project_id": "UUID for the project", "vm_id": "UUID for the instance" @@ -126,7 +126,7 @@ class VirtualBoxHandler: @classmethod @Route.delete( - r"/{project_id}/virtualbox/vms/{vm_id}", + r"/projects/{project_id}/virtualbox/vms/{vm_id}", parameters={ "project_id": "UUID for the project", "vm_id": "UUID for the instance" @@ -147,7 +147,7 @@ class VirtualBoxHandler: @classmethod @Route.post( - r"/{project_id}/virtualbox/vms/{vm_id}/start", + r"/projects/{project_id}/virtualbox/vms/{vm_id}/start", parameters={ "project_id": "UUID for the project", "vm_id": "UUID for the instance" @@ -167,7 +167,7 @@ class VirtualBoxHandler: @classmethod @Route.post( - r"/{project_id}/virtualbox/vms/{vm_id}/stop", + r"/projects/{project_id}/virtualbox/vms/{vm_id}/stop", parameters={ "project_id": "UUID for the project", "vm_id": "UUID for the instance" @@ -187,7 +187,7 @@ class VirtualBoxHandler: @classmethod @Route.post( - r"/{project_id}/virtualbox/vms/{vm_id}/suspend", + r"/projects/{project_id}/virtualbox/vms/{vm_id}/suspend", parameters={ "project_id": "UUID for the project", "vm_id": "UUID for the instance" @@ -207,7 +207,7 @@ class VirtualBoxHandler: @classmethod @Route.post( - r"/{project_id}/virtualbox/vms/{vm_id}/resume", + r"/projects/{project_id}/virtualbox/vms/{vm_id}/resume", parameters={ "project_id": "UUID for the project", "vm_id": "UUID for the instance" @@ -227,7 +227,7 @@ class VirtualBoxHandler: @classmethod @Route.post( - r"/{project_id}/virtualbox/vms/{vm_id}/reload", + r"/projects/{project_id}/virtualbox/vms/{vm_id}/reload", parameters={ "project_id": "UUID for the project", "vm_id": "UUID for the instance" @@ -246,7 +246,7 @@ class VirtualBoxHandler: response.set_status(204) @Route.post( - r"/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/nio", + r"/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/nio", parameters={ "project_id": "UUID for the project", "vm_id": "UUID for the instance", @@ -271,7 +271,7 @@ class VirtualBoxHandler: @classmethod @Route.delete( - r"/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/nio", + r"/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/nio", parameters={ "project_id": "UUID for the project", "vm_id": "UUID for the instance", @@ -291,7 +291,7 @@ class VirtualBoxHandler: response.set_status(204) @Route.post( - r"/{project_id}/virtualbox/{vm_id}/capture/{adapter_id:\d+}/start", + r"/projects/{project_id}/virtualbox/{vm_id}/capture/{adapter_id:\d+}/start", parameters={ "project_id": "UUID for the project", "vm_id": "UUID for the instance", @@ -314,7 +314,7 @@ class VirtualBoxHandler: response.json({"pcap_file_path": pcap_file_path}) @Route.post( - r"/{project_id}/virtualbox/{vm_id}/capture/{adapter_id:\d+}/stop", + r"/projects/{project_id}/virtualbox/{vm_id}/capture/{adapter_id:\d+}/stop", parameters={ "project_id": "UUID for the project", "vm_id": "UUID for the instance", diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index 871f2713..0ecb6c08 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -31,7 +31,7 @@ class VPCSHandler: @classmethod @Route.post( - r"/{project_id}/vpcs/vms", + r"/projects/{project_id}/vpcs/vms", parameters={ "project_id": "UUID for the project" }, @@ -56,7 +56,7 @@ class VPCSHandler: @classmethod @Route.get( - r"/{project_id}/vpcs/vms/{vm_id}", + r"/projects/{project_id}/vpcs/vms/{vm_id}", parameters={ "project_id": "UUID for the project", "vm_id": "UUID for the instance" @@ -76,7 +76,7 @@ class VPCSHandler: @classmethod @Route.put( - r"/{project_id}/vpcs/vms/{vm_id}", + r"/projects/{project_id}/vpcs/vms/{vm_id}", parameters={ "project_id": "UUID for the project", "vm_id": "UUID for the instance" @@ -101,7 +101,7 @@ class VPCSHandler: @classmethod @Route.delete( - r"/{project_id}/vpcs/vms/{vm_id}", + r"/projects/{project_id}/vpcs/vms/{vm_id}", parameters={ "project_id": "UUID for the project", "vm_id": "UUID for the instance" @@ -119,7 +119,7 @@ class VPCSHandler: @classmethod @Route.post( - r"/{project_id}/vpcs/vms/{vm_id}/start", + r"/projects/{project_id}/vpcs/vms/{vm_id}/start", parameters={ "project_id": "UUID for the project", "vm_id": "UUID for the instance" @@ -139,7 +139,7 @@ class VPCSHandler: @classmethod @Route.post( - r"/{project_id}/vpcs/vms/{vm_id}/stop", + r"/projects/{project_id}/vpcs/vms/{vm_id}/stop", parameters={ "project_id": "UUID for the project", "vm_id": "UUID for the instance" @@ -159,7 +159,7 @@ class VPCSHandler: @classmethod @Route.post( - r"/{project_id}/vpcs/vms/{vm_id}/reload", + r"/projects/{project_id}/vpcs/vms/{vm_id}/reload", parameters={ "project_id": "UUID for the project", "vm_id": "UUID for the instance", @@ -178,7 +178,7 @@ class VPCSHandler: response.set_status(204) @Route.post( - r"/{project_id}/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio", + r"/projects/{project_id}/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio", parameters={ "project_id": "UUID for the project", "vm_id": "UUID for the instance", @@ -203,7 +203,7 @@ class VPCSHandler: @classmethod @Route.delete( - r"/{project_id}/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio", + r"/projects/{project_id}/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio", parameters={ "project_id": "UUID for the project", "vm_id": "UUID for the instance", diff --git a/gns3server/main.py b/gns3server/main.py index 7b0bd3a7..ce249dad 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -78,6 +78,7 @@ def parse_arguments(): parser.add_argument("-v", "--version", help="show the version", action="version", version=__version__) parser.add_argument("--host", help="run on the given host/IP address", default="127.0.0.1") parser.add_argument("--port", help="run on the given port", type=int, default=8000) + parser.add_argument("--config", help="use this config file", type=str, default=None) parser.add_argument("--ssl", action="store_true", help="run in SSL mode") parser.add_argument("--certfile", help="SSL cert file", default="") parser.add_argument("--certkey", help="SSL key file", default="") diff --git a/tests/api/test_virtualbox.py b/tests/api/test_virtualbox.py index 86179ff8..c1eb556c 100644 --- a/tests/api/test_virtualbox.py +++ b/tests/api/test_virtualbox.py @@ -22,7 +22,7 @@ from tests.utils import asyncio_patch @pytest.fixture(scope="module") def vm(server, project): with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.create", return_value=True) as mock: - response = server.post("/{project_id}/virtualbox/vms".format(project_id=project.id), {"name": "VMTEST", + response = server.post("/projects/{project_id}/virtualbox/vms".format(project_id=project.id), {"name": "VMTEST", "vmname": "VMTEST", "linked_clone": False}) assert mock.called @@ -33,7 +33,7 @@ def vm(server, project): def test_vbox_create(server, project): with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.create", return_value=True): - response = server.post("/{project_id}/virtualbox/vms".format(project_id=project.id), {"name": "VM1", + response = server.post("/projects/{project_id}/virtualbox/vms".format(project_id=project.id), {"name": "VM1", "vmname": "VM1", "linked_clone": False}, example=True) @@ -43,73 +43,73 @@ def test_vbox_create(server, project): def test_vbox_get(server, project, vm): - response = server.get("/{project_id}/virtualbox/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) + response = server.get("/projects/{project_id}/virtualbox/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert response.status == 200 - assert response.route == "/{project_id}/virtualbox/vms/{vm_id}" + assert response.route == "/projects/{project_id}/virtualbox/vms/{vm_id}" assert response.json["name"] == "VMTEST" assert response.json["project_id"] == project.id def test_vbox_start(server, vm): with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.start", return_value=True) as mock: - response = server.post("/{project_id}/virtualbox/vms/{vm_id}/start".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + response = server.post("/projects/{project_id}/virtualbox/vms/{vm_id}/start".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) assert mock.called assert response.status == 204 def test_vbox_stop(server, vm): with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.stop", return_value=True) as mock: - response = server.post("/{project_id}/virtualbox/vms/{vm_id}/stop".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + response = server.post("/projects/{project_id}/virtualbox/vms/{vm_id}/stop".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) assert mock.called assert response.status == 204 def test_vbox_suspend(server, vm): with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.suspend", return_value=True) as mock: - response = server.post("/{project_id}/virtualbox/vms/{vm_id}/suspend".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + response = server.post("/projects/{project_id}/virtualbox/vms/{vm_id}/suspend".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) assert mock.called assert response.status == 204 def test_vbox_resume(server, vm): with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.resume", return_value=True) as mock: - response = server.post("/{project_id}/virtualbox/vms/{vm_id}/resume".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + response = server.post("/projects/{project_id}/virtualbox/vms/{vm_id}/resume".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) assert mock.called assert response.status == 204 def test_vbox_reload(server, vm): with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.reload", return_value=True) as mock: - response = server.post("/{project_id}/virtualbox/vms/{vm_id}/reload".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + response = server.post("/projects/{project_id}/virtualbox/vms/{vm_id}/reload".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) assert mock.called assert response.status == 204 def test_vbox_nio_create_udp(server, vm): - response = server.post("/{project_id}/virtualbox/vms/{vm_id}/adapters/0/nio".format(project_id=vm["project_id"], + response = server.post("/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"}, example=True) assert response.status == 201 - assert response.route == "/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/nio" + assert response.route == "/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/nio" assert response.json["type"] == "nio_udp" def test_vbox_delete_nio(server, vm): - server.post("/{project_id}/virtualbox/vms/{vm_id}/adapters/0/nio".format(project_id=vm["project_id"], + server.post("/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"}) - response = server.delete("/{project_id}/virtualbox/vms/{vm_id}/adapters/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) + response = server.delete("/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert response.status == 204 - assert response.route == "/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/nio" + assert response.route == "/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/nio" def test_vbox_update(server, vm, free_console_port): - response = server.put("/{project_id}/virtualbox/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"name": "test", + response = server.put("/projects/{project_id}/virtualbox/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"name": "test", "console": free_console_port}) assert response.status == 200 assert response.json["name"] == "test" diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index de8c5b76..9c4dcfe0 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -23,105 +23,105 @@ from unittest.mock import patch @pytest.fixture(scope="module") def vm(server, project): - response = server.post("/{project_id}/vpcs/vms".format(project_id=project.id), {"name": "PC TEST 1"}) + response = server.post("/projects/{project_id}/vpcs/vms".format(project_id=project.id), {"name": "PC TEST 1"}) assert response.status == 201 return response.json def test_vpcs_create(server, project): - response = server.post("/{project_id}/vpcs/vms".format(project_id=project.id), {"name": "PC TEST 1"}, example=True) + response = server.post("/projects/{project_id}/vpcs/vms".format(project_id=project.id), {"name": "PC TEST 1"}, example=True) assert response.status == 201 - assert response.route == "/{project_id}/vpcs/vms" + assert response.route == "/projects/{project_id}/vpcs/vms" assert response.json["name"] == "PC TEST 1" assert response.json["project_id"] == project.id def test_vpcs_get(server, project, vm): - response = server.get("/{project_id}/vpcs/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) + response = server.get("/projects/{project_id}/vpcs/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert response.status == 200 - assert response.route == "/{project_id}/vpcs/vms/{vm_id}" + assert response.route == "/projects/{project_id}/vpcs/vms/{vm_id}" assert response.json["name"] == "PC TEST 1" assert response.json["project_id"] == project.id def test_vpcs_create_startup_script(server, project): - response = server.post("/{project_id}/vpcs/vms".format(project_id=project.id), {"name": "PC TEST 1", "startup_script": "ip 192.168.1.2\necho TEST"}) + response = server.post("/projects/{project_id}/vpcs/vms".format(project_id=project.id), {"name": "PC TEST 1", "startup_script": "ip 192.168.1.2\necho TEST"}) assert response.status == 201 - assert response.route == "/{project_id}/vpcs/vms" + assert response.route == "/projects/{project_id}/vpcs/vms" assert response.json["name"] == "PC TEST 1" assert response.json["project_id"] == project.id assert response.json["startup_script"] == "ip 192.168.1.2\necho TEST" def test_vpcs_create_port(server, project, free_console_port): - response = server.post("/{project_id}/vpcs/vms".format(project_id=project.id), {"name": "PC TEST 1", "console": free_console_port}) + response = server.post("/projects/{project_id}/vpcs/vms".format(project_id=project.id), {"name": "PC TEST 1", "console": free_console_port}) assert response.status == 201 - assert response.route == "/{project_id}/vpcs/vms" + assert response.route == "/projects/{project_id}/vpcs/vms" assert response.json["name"] == "PC TEST 1" assert response.json["project_id"] == project.id assert response.json["console"] == free_console_port def test_vpcs_nio_create_udp(server, vm): - response = server.post("/{project_id}/vpcs/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", + response = server.post("/projects/{project_id}/vpcs/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"}, example=True) assert response.status == 201 - assert response.route == "/{project_id}/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio" + assert response.route == "/projects/{project_id}/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio" assert response.json["type"] == "nio_udp" def test_vpcs_nio_create_tap(server, vm): with patch("gns3server.modules.base_manager.BaseManager._has_privileged_access", return_value=True): - response = server.post("/{project_id}/vpcs/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_tap", + response = server.post("/projects/{project_id}/vpcs/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_tap", "tap_device": "test"}) assert response.status == 201 - assert response.route == "/{project_id}/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio" + assert response.route == "/projects/{project_id}/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio" assert response.json["type"] == "nio_tap" def test_vpcs_delete_nio(server, vm): - server.post("/{project_id}/vpcs/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", + server.post("/projects/{project_id}/vpcs/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"}) - response = server.delete("/{project_id}/vpcs/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) + response = server.delete("/projects/{project_id}/vpcs/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert response.status == 204 - assert response.route == "/{project_id}/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio" + assert response.route == "/projects/{project_id}/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio" def test_vpcs_start(server, vm): with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM.start", return_value=True) as mock: - response = server.post("/{project_id}/vpcs/vms/{vm_id}/start".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + response = server.post("/projects/{project_id}/vpcs/vms/{vm_id}/start".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) assert mock.called assert response.status == 204 def test_vpcs_stop(server, vm): with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM.stop", return_value=True) as mock: - response = server.post("/{project_id}/vpcs/vms/{vm_id}/stop".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + response = server.post("/projects/{project_id}/vpcs/vms/{vm_id}/stop".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) assert mock.called assert response.status == 204 def test_vpcs_reload(server, vm): with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM.reload", return_value=True) as mock: - response = server.post("/{project_id}/vpcs/vms/{vm_id}/reload".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + response = server.post("/projects/{project_id}/vpcs/vms/{vm_id}/reload".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) assert mock.called assert response.status == 204 def test_vpcs_delete(server, vm): with asyncio_patch("gns3server.modules.vpcs.VPCS.delete_vm", return_value=True) as mock: - response = server.delete("/{project_id}/vpcs/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + response = server.delete("/projects/{project_id}/vpcs/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) assert mock.called assert response.status == 204 def test_vpcs_update(server, vm, tmpdir, free_console_port): - response = server.put("/{project_id}/vpcs/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"name": "test", + response = server.put("/projects/{project_id}/vpcs/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"name": "test", "console": free_console_port, "startup_script": "ip 192.168.1.1"}) assert response.status == 200 From d499402491a076659f4714f2218089302774e6e7 Mon Sep 17 00:00:00 2001 From: grossmj Date: Fri, 6 Feb 2015 17:31:13 -0700 Subject: [PATCH 204/485] VirtualBox implementation complete. --- gns3server/handlers/virtualbox_handler.py | 12 +- .../modules/virtualbox/virtualbox_vm.py | 136 +++++++++--------- gns3server/schemas/virtualbox.py | 24 ++-- 3 files changed, 87 insertions(+), 85 deletions(-) diff --git a/gns3server/handlers/virtualbox_handler.py b/gns3server/handlers/virtualbox_handler.py index 0b62b253..bbe5661d 100644 --- a/gns3server/handlers/virtualbox_handler.py +++ b/gns3server/handlers/virtualbox_handler.py @@ -120,8 +120,14 @@ class VirtualBoxHandler: for name, value in request.json.items(): if hasattr(vm, name) and getattr(vm, name) != value: setattr(vm, name, value) + if name == "vmname": + yield from vm.rename_in_virtualbox() + + if "adapters" in request.json: + adapters = int(request.json["adapters"]) + if adapters != vm.adapters: + yield from vm.set_adapters(adapters) - # TODO: FINISH UPDATE (adapters). response.json(vm) @classmethod @@ -265,7 +271,7 @@ class VirtualBoxHandler: vbox_manager = VirtualBox.instance() vm = vbox_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) nio = vbox_manager.create_nio(vbox_manager.vboxmanage_path, request.json) - vm.port_add_nio_binding(int(request.match_info["adapter_id"]), nio) + yield from vm.adapter_add_nio_binding(int(request.match_info["adapter_id"]), nio) response.set_status(201) response.json(nio) @@ -287,7 +293,7 @@ class VirtualBoxHandler: vbox_manager = VirtualBox.instance() vm = vbox_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) - vm.port_remove_nio_binding(int(request.match_info["adapter_id"])) + yield from vm.adapter_remove_nio_binding(int(request.match_info["adapter_id"])) response.set_status(204) @Route.post( diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 49ad1cc2..202b59f2 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -66,7 +66,7 @@ class VirtualBoxVM(BaseVM): self._headless = False self._enable_remote_console = False self._vmname = vmname - self._adapter_start_index = 0 + self._use_any_adapter = False self._adapter_type = "Intel PRO/1000 MT Desktop (82540EM)" if self._console is not None: @@ -85,7 +85,7 @@ class VirtualBoxVM(BaseVM): "enable_remote_console": self.enable_remote_console, "adapters": self._adapters, "adapter_type": self.adapter_type, - "adapter_start_index": self.adapter_start_index} + "use_any_adapter": self.use_any_adapter} @asyncio.coroutine def _get_system_properties(self): @@ -213,11 +213,11 @@ class VirtualBoxVM(BaseVM): log.warn("Could not deactivate the first serial port: {}".format(e)) for adapter_id in range(0, len(self._ethernet_adapters)): - if self._ethernet_adapters[adapter_id] is None: - continue - yield from self._modify_vm("--nictrace{} off".format(adapter_id + 1)) - yield from self._modify_vm("--cableconnected{} off".format(adapter_id + 1)) - yield from self._modify_vm("--nic{} null".format(adapter_id + 1)) + nio = self._ethernet_adapters[adapter_id].get_nio(0) + if nio: + yield from self._modify_vm("--nictrace{} off".format(adapter_id + 1)) + yield from self._modify_vm("--cableconnected{} off".format(adapter_id + 1)) + yield from self._modify_vm("--nic{} null".format(adapter_id + 1)) @asyncio.coroutine def suspend(self): @@ -312,6 +312,7 @@ class VirtualBoxVM(BaseVM): port=hdd_info["port"], device=hdd_info["device"], medium=hdd_file)) + yield from self._storage_attach('--storagectl "{}" --port {} --device {} --type hdd --medium "{}"'.format(hdd_info["controller"], hdd_info["port"], hdd_info["device"], @@ -350,7 +351,9 @@ class VirtualBoxVM(BaseVM): controller=controller, port=port, device=device)) - yield from self._storage_attach('--storagectl "{}" --port {} --device {} --type hdd --medium none'.format(controller, port, device)) + yield from self._storage_attach('--storagectl "{}" --port {} --device {} --type hdd --medium none'.format(controller, + port, + device)) hdd_table.append( { "hdd": os.path.basename(value), @@ -420,10 +423,10 @@ class VirtualBoxVM(BaseVM): if enable_remote_console: log.info("VirtualBox VM '{name}' [{id}] has enabled the console".format(name=self.name, id=self.id)) - # self._start_remote_console() + self._start_remote_console() else: log.info("VirtualBox VM '{name}' [{id}] has disabled the console".format(name=self.name, id=self.id)) - # self._stop_remote_console() + self._stop_remote_console() self._enable_remote_console = enable_remote_console @property @@ -445,15 +448,26 @@ class VirtualBoxVM(BaseVM): """ log.info("VirtualBox VM '{name}' [{id}] has set the VM name to '{vmname}'".format(name=self.name, id=self.id, vmname=vmname)) - # TODO: test linked clone - # if self._linked_clone: - # yield from self._modify_vm('--name "{}"'.format(vmname)) self._vmname = vmname @asyncio.coroutine - def rename(self): + def rename_in_virtualbox(self): + """ + Renames the VirtualBox VM. + """ + + if self._linked_clone: + yield from self._modify_vm('--name "{}"'.format(self._vmname)) + + @property + def adapters(self): + """ + Returns the number of adapters configured for this VirtualBox VM. + + :returns: number of adapters + """ - pass + return self._adapters @asyncio.coroutine def set_adapters(self, adapters): @@ -469,10 +483,7 @@ class VirtualBoxVM(BaseVM): raise VirtualBoxError("Number of adapters above the maximum supported of {}".format(self._maximum_adapters)) self._ethernet_adapters.clear() - for adapter_id in range(0, self._adapter_start_index + adapters): - if adapter_id < self._adapter_start_index: - self._ethernet_adapters.append(None) - continue + for adapter_id in range(0, adapters): self._ethernet_adapters.append(EthernetAdapter()) self._adapters = len(self._ethernet_adapters) @@ -481,29 +492,28 @@ class VirtualBoxVM(BaseVM): adapters=adapters)) @property - def adapter_start_index(self): + def use_any_adapter(self): """ - Returns the adapter start index for this VirtualBox VM instance. + Returns either GNS3 can use any VirtualBox adapter on this instance. :returns: index """ - return self._adapter_start_index + return self._use_any_adapter - @adapter_start_index.setter - def adapter_start_index(self, adapter_start_index): + @use_any_adapter.setter + def use_any_adapter(self, use_any_adapter): """ - Sets the adapter start index for this VirtualBox VM instance. + Allows GNS3 to use any VirtualBox adapter on this instance. - :param adapter_start_index: index + :param use_any_adapter: boolean """ - self._adapter_start_index = adapter_start_index - # TODO: get rid of adapter start index - # self.adapters = self.adapters # this forces to recreate the adapter list with the correct index - log.info("VirtualBox VM '{name}' [{id}]: adapter start index changed to {index}".format(name=self.name, - id=self.id, - index=adapter_start_index)) + if use_any_adapter: + log.info("VirtualBox VM '{name}' [{id}] is allowed to use any adapter".format(name=self.name, id=self.id)) + else: + log.info("VirtualBox VM '{name}' [{id}] is not allowd to use any adapter".format(name=self.name, id=self.id)) + self._use_any_adapter = use_any_adapter @property def adapter_type(self): @@ -628,37 +638,33 @@ class VirtualBoxVM(BaseVM): Configures network options. """ - nic_attachements = yield from self._get_nic_attachements(self._maximum_adapters) + nic_attachments = yield from self._get_nic_attachements(self._maximum_adapters) for adapter_id in range(0, len(self._ethernet_adapters)): - if self._ethernet_adapters[adapter_id] is None: - # force enable to avoid any discrepancy in the interface numbering inside the VM - # e.g. Ethernet2 in GNS3 becoming eth0 inside the VM when using a start index of 2. - attachement = nic_attachements[adapter_id] - if attachement: - # attachement can be none, null, nat, bridged, intnet, hostonly or generic - yield from self._modify_vm("--nic{} {}".format(adapter_id + 1, attachement)) - continue + nio = self._ethernet_adapters[adapter_id].get_nio(0) + if nio: + attachment = nic_attachments[adapter_id] + if not self._use_any_adapter and attachment not in ("none", "null"): + raise VirtualBoxError("Attachment ({}) already configured on adapter {}. " + "Please set it to 'Not attached' to allow GNS3 to use it.".format(attachment, + adapter_id + 1)) + yield from self._modify_vm("--nictrace{} off".format(adapter_id + 1)) - vbox_adapter_type = "82540EM" - if self._adapter_type == "PCnet-PCI II (Am79C970A)": - vbox_adapter_type = "Am79C970A" - if self._adapter_type == "PCNet-FAST III (Am79C973)": - vbox_adapter_type = "Am79C973" - if self._adapter_type == "Intel PRO/1000 MT Desktop (82540EM)": vbox_adapter_type = "82540EM" - if self._adapter_type == "Intel PRO/1000 T Server (82543GC)": - vbox_adapter_type = "82543GC" - if self._adapter_type == "Intel PRO/1000 MT Server (82545EM)": - vbox_adapter_type = "82545EM" - if self._adapter_type == "Paravirtualized Network (virtio-net)": - vbox_adapter_type = "virtio" - - args = [self._vmname, "--nictype{}".format(adapter_id + 1), vbox_adapter_type] - yield from self.manager.execute("modifyvm", args) + if self._adapter_type == "PCnet-PCI II (Am79C970A)": + vbox_adapter_type = "Am79C970A" + if self._adapter_type == "PCNet-FAST III (Am79C973)": + vbox_adapter_type = "Am79C973" + if self._adapter_type == "Intel PRO/1000 MT Desktop (82540EM)": + vbox_adapter_type = "82540EM" + if self._adapter_type == "Intel PRO/1000 T Server (82543GC)": + vbox_adapter_type = "82543GC" + if self._adapter_type == "Intel PRO/1000 MT Server (82545EM)": + vbox_adapter_type = "82545EM" + if self._adapter_type == "Paravirtualized Network (virtio-net)": + vbox_adapter_type = "virtio" + args = [self._vmname, "--nictype{}".format(adapter_id + 1), vbox_adapter_type] + yield from self.manager.execute("modifyvm", args) - yield from self._modify_vm("--nictrace{} off".format(adapter_id + 1)) - nio = self._ethernet_adapters[adapter_id].get_nio(0) - if nio: log.debug("setting UDP params on adapter {}".format(adapter_id)) yield from self._modify_vm("--nic{} generic".format(adapter_id + 1)) yield from self._modify_vm("--nicgenericdrv{} UDPTunnel".format(adapter_id + 1)) @@ -670,10 +676,6 @@ class VirtualBoxVM(BaseVM): if nio.capturing: yield from self._modify_vm("--nictrace{} on".format(adapter_id + 1)) yield from self._modify_vm("--nictracefile{} {}".format(adapter_id + 1, nio.pcap_output_file)) - else: - # shutting down unused adapters... - yield from self._modify_vm("--cableconnected{} off".format(adapter_id + 1)) - yield from self._modify_vm("--nic{} null".format(adapter_id + 1)) for adapter_id in range(len(self._ethernet_adapters), self._maximum_adapters): log.debug("disabling remaining adapter {}".format(adapter_id)) @@ -759,9 +761,9 @@ class VirtualBoxVM(BaseVM): self._serial_pipe = None @asyncio.coroutine - def port_add_nio_binding(self, adapter_id, nio): + def adapter_add_nio_binding(self, adapter_id, nio): """ - Adds a port NIO binding. + Adds an adapter NIO binding. :param adapter_id: adapter ID :param nio: NIO instance to add to the slot/port @@ -789,9 +791,9 @@ class VirtualBoxVM(BaseVM): adapter_id=adapter_id)) @asyncio.coroutine - def port_remove_nio_binding(self, adapter_id): + def adapter_remove_nio_binding(self, adapter_id): """ - Removes a port NIO binding. + Removes an adapter NIO binding. :param adapter_id: adapter ID diff --git a/gns3server/schemas/virtualbox.py b/gns3server/schemas/virtualbox.py index 03785691..05c0b5e0 100644 --- a/gns3server/schemas/virtualbox.py +++ b/gns3server/schemas/virtualbox.py @@ -48,11 +48,9 @@ VBOX_CREATE_SCHEMA = { "minimum": 0, "maximum": 36, # maximum given by the ICH9 chipset in VirtualBox }, - "adapter_start_index": { - "description": "adapter index from which to start using adapters", - "type": "integer", - "minimum": 0, - "maximum": 35, # maximum given by the ICH9 chipset in VirtualBox + "use_any_adapter": { + "description": "allow GNS3 to use any VirtualBox adapter", + "type": "boolean", }, "adapter_type": { "description": "VirtualBox adapter type", @@ -99,11 +97,9 @@ VBOX_UPDATE_SCHEMA = { "minimum": 0, "maximum": 36, # maximum given by the ICH9 chipset in VirtualBox }, - "adapter_start_index": { - "description": "adapter index from which to start using adapters", - "type": "integer", - "minimum": 0, - "maximum": 35, # maximum given by the ICH9 chipset in VirtualBox + "use_any_adapter": { + "description": "allow GNS3 to use any VirtualBox adapter", + "type": "boolean", }, "adapter_type": { "description": "VirtualBox adapter type", @@ -226,11 +222,9 @@ VBOX_OBJECT_SCHEMA = { "minimum": 0, "maximum": 36, # maximum given by the ICH9 chipset in VirtualBox }, - "adapter_start_index": { - "description": "adapter index from which to start using adapters", - "type": "integer", - "minimum": 0, - "maximum": 35, # maximum given by the ICH9 chipset in VirtualBox + "use_any_adapter": { + "description": "allow GNS3 to use any VirtualBox adapter", + "type": "boolean", }, "adapter_type": { "description": "VirtualBox adapter type", From 2a3b37a3bd4d28e7ef669bf68ea397d56252b808 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 8 Feb 2015 14:44:56 -0700 Subject: [PATCH 205/485] VirtualBox packet capture. --- gns3server/config.py | 4 ++-- gns3server/handlers/virtualbox_handler.py | 6 +++--- gns3server/modules/base_manager.py | 7 ++++++- gns3server/modules/project.py | 4 ++-- gns3server/schemas/virtualbox.py | 4 ++-- 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/gns3server/config.py b/gns3server/config.py index 848ab15d..21e1e5b9 100644 --- a/gns3server/config.py +++ b/gns3server/config.py @@ -35,7 +35,7 @@ class Config(object): """ Configuration file management using configparser. - :params files: Array of configuration files (optionnal) + :params files: Array of configuration files (optional) """ def __init__(self, files=None): @@ -45,7 +45,7 @@ class Config(object): # Monitor configuration files for changes self._watched_files = {} - # Override config from commande even if modify the config file and live reload it. + # Override config from command line even if we modify the config file and live reload it. self._override_config = {} if sys.platform.startswith("win"): diff --git a/gns3server/handlers/virtualbox_handler.py b/gns3server/handlers/virtualbox_handler.py index bbe5661d..97997d3e 100644 --- a/gns3server/handlers/virtualbox_handler.py +++ b/gns3server/handlers/virtualbox_handler.py @@ -297,7 +297,7 @@ class VirtualBoxHandler: response.set_status(204) @Route.post( - r"/projects/{project_id}/virtualbox/{vm_id}/capture/{adapter_id:\d+}/start", + r"/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/start_capture", parameters={ "project_id": "UUID for the project", "vm_id": "UUID for the instance", @@ -315,12 +315,12 @@ class VirtualBoxHandler: vbox_manager = VirtualBox.instance() vm = vbox_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) adapter_id = int(request.match_info["adapter_id"]) - pcap_file_path = os.path.join(vm.project.capture_working_directory(), request.json["filename"]) + pcap_file_path = os.path.join(vm.project.capture_working_directory(), request.json["capture_file_name"]) vm.start_capture(adapter_id, pcap_file_path) response.json({"pcap_file_path": pcap_file_path}) @Route.post( - r"/projects/{project_id}/virtualbox/{vm_id}/capture/{adapter_id:\d+}/stop", + r"/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/stop_capture", parameters={ "project_id": "UUID for the project", "vm_id": "UUID for the instance", diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 999c2bc3..1a5836bc 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -156,7 +156,12 @@ class BaseManager: project = ProjectManager.instance().get_project(project_id) - # TODO: support for old projects VM with normal IDs. + try: + if vm_id: + legacy_id = int(vm_id) + # TODO: support for old projects VM with normal IDs. + except ValueError: + pass if not vm_id: vm_id = str(uuid4()) diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index 64627805..b3ba2c99 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -104,7 +104,7 @@ class Project: def location(self, location): if location != self._location and self._config().get("local", False) is False: - raise aiohttp.web.HTTPForbidden(text="You are not allowed to modifiy the project directory location") + raise aiohttp.web.HTTPForbidden(text="You are not allowed to modify the project directory location") self._location = location @@ -118,7 +118,7 @@ class Project: if hasattr(self, "_path"): if path != self._path and self._config().get("local", False) is False: - raise aiohttp.web.HTTPForbidden(text="You are not allowed to modifiy the project directory location") + raise aiohttp.web.HTTPForbidden(text="You are not allowed to modify the project directory location") self._path = path self._update_temporary_file() diff --git a/gns3server/schemas/virtualbox.py b/gns3server/schemas/virtualbox.py index 05c0b5e0..adcfd6eb 100644 --- a/gns3server/schemas/virtualbox.py +++ b/gns3server/schemas/virtualbox.py @@ -169,14 +169,14 @@ VBOX_CAPTURE_SCHEMA = { "description": "Request validation to start a packet capture on a VirtualBox VM instance port", "type": "object", "properties": { - "capture_filename": { + "capture_file_name": { "description": "Capture file name", "type": "string", "minLength": 1, }, }, "additionalProperties": False, - "required": ["capture_filename"] + "required": ["capture_file_name"] } VBOX_OBJECT_SCHEMA = { From 0d7d0a05c36663629ceab9febddee42a1e169cf1 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 8 Feb 2015 18:10:04 -0700 Subject: [PATCH 206/485] Handle old projects. --- gns3server/modules/base_manager.py | 17 +++++++++++++++-- gns3server/modules/virtualbox/__init__.py | 11 +++++++++++ gns3server/modules/vpcs/__init__.py | 11 +++++++++++ gns3server/schemas/virtualbox.py | 11 +++++++---- gns3server/schemas/vpcs.py | 11 +++++++---- gns3server/utils/asyncio.py | 2 +- 6 files changed, 52 insertions(+), 11 deletions(-) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 1a5836bc..ec51f658 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -22,12 +22,14 @@ import stat import asyncio import aiohttp import socket +import shutil import logging log = logging.getLogger(__name__) from uuid import UUID, uuid4 from ..config import Config +from ..utils.asyncio import wait_run_in_executor from .project_manager import ProjectManager from .nios.nio_udp import NIOUDP @@ -157,9 +159,20 @@ class BaseManager: project = ProjectManager.instance().get_project(project_id) try: - if vm_id: + if vm_id and hasattr(self, "get_legacy_vm_workdir_name"): + # move old project VM files to a new location legacy_id = int(vm_id) - # TODO: support for old projects VM with normal IDs. + project_dir = os.path.dirname(project.path) + project_name = os.path.basename(project_dir) + project_files_dir = os.path.join(project_dir, "{}-files".format(project_name)) + module_path = os.path.join(project_files_dir, self.module_name.lower()) + vm_working_dir = os.path.join(module_path, self.get_legacy_vm_workdir_name(legacy_id)) + vm_id = str(uuid4()) + new_vm_working_dir = os.path.join(project.path, self.module_name.lower(), vm_id) + try: + yield from wait_run_in_executor(shutil.move, vm_working_dir, new_vm_working_dir) + except OSError as e: + raise aiohttp.web.HTTPInternalServerError(text="Could not move VM working directory: {}".format(e)) except ValueError: pass diff --git a/gns3server/modules/virtualbox/__init__.py b/gns3server/modules/virtualbox/__init__.py index 245d8976..346b237c 100644 --- a/gns3server/modules/virtualbox/__init__.py +++ b/gns3server/modules/virtualbox/__init__.py @@ -127,3 +127,14 @@ class VirtualBox(BaseManager): if not extra_data[0].strip() == "Value: yes": vms.append(vmname) return vms + + @staticmethod + def get_legacy_vm_workdir_name(legacy_vm_id): + """ + Returns the name of the legacy working directory name for a VM. + + :param legacy_vm_id: legacy VM identifier (integer) + :returns: working directory name + """ + + return "vm-{}".format(legacy_vm_id) diff --git a/gns3server/modules/vpcs/__init__.py b/gns3server/modules/vpcs/__init__.py index 0741c0ab..826e6fd8 100644 --- a/gns3server/modules/vpcs/__init__.py +++ b/gns3server/modules/vpcs/__init__.py @@ -63,3 +63,14 @@ class VPCS(BaseManager): """ return self._used_mac_ids.get(vm_id, 1) + + @staticmethod + def get_legacy_vm_workdir_name(legacy_vm_id): + """ + Returns the name of the legacy working directory name for a VM. + + :param legacy_vm_id: legacy VM identifier (integer) + :returns: working directory name + """ + + return "pc-{}".format(legacy_vm_id) diff --git a/gns3server/schemas/virtualbox.py b/gns3server/schemas/virtualbox.py index adcfd6eb..b527f6f6 100644 --- a/gns3server/schemas/virtualbox.py +++ b/gns3server/schemas/virtualbox.py @@ -23,10 +23,13 @@ VBOX_CREATE_SCHEMA = { "properties": { "vm_id": { "description": "VirtualBox VM instance identifier", - "type": "string", - "minLength": 36, - "maxLength": 36, - "pattern": "(^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}|\d+)$" + "oneOf": [ + {"type": "string", + "minLength": 36, + "maxLength": 36, + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$"}, + {"type": "integer"} # for legacy projects + ] }, "linked_clone": { "description": "either the VM is a linked clone or not", diff --git a/gns3server/schemas/vpcs.py b/gns3server/schemas/vpcs.py index fe9cc8e8..8ba53064 100644 --- a/gns3server/schemas/vpcs.py +++ b/gns3server/schemas/vpcs.py @@ -28,10 +28,13 @@ VPCS_CREATE_SCHEMA = { }, "vm_id": { "description": "VPCS VM identifier", - "type": "string", - "minLength": 36, - "maxLength": 36, - "pattern": "^([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}|\d+)$" + "oneOf": [ + {"type": "string", + "minLength": 36, + "maxLength": 36, + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$"}, + {"type": "integer"} # for legacy projects + ] }, "console": { "description": "console TCP port", diff --git a/gns3server/utils/asyncio.py b/gns3server/utils/asyncio.py index 9b94eaf0..0554593a 100644 --- a/gns3server/utils/asyncio.py +++ b/gns3server/utils/asyncio.py @@ -23,7 +23,7 @@ import asyncio def wait_run_in_executor(func, *args): """ Run blocking code in a different thread and wait - the result. + for the result. :param func: Run this function in a different thread :param args: Parameters of the function From 64c197c7190f5a9ca73372d90da5e67302a49a39 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 9 Feb 2015 10:18:37 +0100 Subject: [PATCH 207/485] Fix tests --- tests/api/test_virtualbox.py | 50 ++++++++++++++++++++++-------------- tests/api/test_vpcs.py | 18 ++++++------- tests/conftest.py | 14 ++++++++++ 3 files changed, 54 insertions(+), 28 deletions(-) diff --git a/tests/api/test_virtualbox.py b/tests/api/test_virtualbox.py index c1eb556c..f1f729d6 100644 --- a/tests/api/test_virtualbox.py +++ b/tests/api/test_virtualbox.py @@ -18,24 +18,27 @@ import pytest from tests.utils import asyncio_patch +@pytest.yield_fixture(scope="module") +def vm(server, project, monkeypatch): + + vboxmanage_path = "/fake/VboxManage" -@pytest.fixture(scope="module") -def vm(server, project): with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.create", return_value=True) as mock: response = server.post("/projects/{project_id}/virtualbox/vms".format(project_id=project.id), {"name": "VMTEST", - "vmname": "VMTEST", - "linked_clone": False}) + "vmname": "VMTEST", + "linked_clone": False}) assert mock.called assert response.status == 201 - return response.json + with asyncio_patch("gns3server.modules.virtualbox.VirtualBox.find_vboxmanage", return_value=vboxmanage_path): + yield response.json def test_vbox_create(server, project): with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.create", return_value=True): response = server.post("/projects/{project_id}/virtualbox/vms".format(project_id=project.id), {"name": "VM1", - "vmname": "VM1", - "linked_clone": False}, + "vmname": "VM1", + "linked_clone": False}, example=True) assert response.status == 201 assert response.json["name"] == "VM1" @@ -86,31 +89,40 @@ def test_vbox_reload(server, vm): def test_vbox_nio_create_udp(server, vm): - response = server.post("/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/0/nio".format(project_id=vm["project_id"], - vm_id=vm["vm_id"]), {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"}, + + with asyncio_patch('gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.adapter_add_nio_binding') as mock: + response = server.post("/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/0/nio".format(project_id=vm["project_id"], + vm_id=vm["vm_id"]), {"type": "nio_udp", + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}, example=True) + + assert mock.called + args, kwgars = mock.call_args + assert args[0] == 0 + assert response.status == 201 assert response.route == "/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/nio" assert response.json["type"] == "nio_udp" def test_vbox_delete_nio(server, vm): - server.post("/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/0/nio".format(project_id=vm["project_id"], - vm_id=vm["vm_id"]), {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"}) - response = server.delete("/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) + + with asyncio_patch('gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.adapter_remove_nio_binding') as mock: + response = server.delete("/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) + + assert mock.called + args, kwgars = mock.call_args + assert args[0] == 0 + assert response.status == 204 assert response.route == "/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/nio" def test_vbox_update(server, vm, free_console_port): response = server.put("/projects/{project_id}/virtualbox/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"name": "test", - "console": free_console_port}) + "console": free_console_port}) assert response.status == 200 assert response.json["name"] == "test" assert response.json["console"] == free_console_port diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index 9c4dcfe0..ba42c45c 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -64,9 +64,9 @@ def test_vpcs_create_port(server, project, free_console_port): def test_vpcs_nio_create_udp(server, vm): response = server.post("/projects/{project_id}/vpcs/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"}, + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}, example=True) assert response.status == 201 assert response.route == "/projects/{project_id}/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio" @@ -76,7 +76,7 @@ def test_vpcs_nio_create_udp(server, vm): def test_vpcs_nio_create_tap(server, vm): with patch("gns3server.modules.base_manager.BaseManager._has_privileged_access", return_value=True): response = server.post("/projects/{project_id}/vpcs/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_tap", - "tap_device": "test"}) + "tap_device": "test"}) assert response.status == 201 assert response.route == "/projects/{project_id}/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio" assert response.json["type"] == "nio_tap" @@ -84,9 +84,9 @@ def test_vpcs_nio_create_tap(server, vm): def test_vpcs_delete_nio(server, vm): server.post("/projects/{project_id}/vpcs/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"}) + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}) response = server.delete("/projects/{project_id}/vpcs/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert response.status == 204 assert response.route == "/projects/{project_id}/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio" @@ -122,8 +122,8 @@ def test_vpcs_delete(server, vm): def test_vpcs_update(server, vm, tmpdir, free_console_port): response = server.put("/projects/{project_id}/vpcs/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"name": "test", - "console": free_console_port, - "startup_script": "ip 192.168.1.1"}) + "console": free_console_port, + "startup_script": "ip 192.168.1.1"}) assert response.status == 200 assert response.json["name"] == "test" assert response.json["console"] == free_console_port diff --git a/tests/conftest.py b/tests/conftest.py index 0ab02c19..490a35cd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,6 +20,7 @@ import socket import asyncio import tempfile import shutil +import os from aiohttp import web from gns3server.config import Config @@ -32,6 +33,10 @@ from gns3server.modules.project_manager import ProjectManager from tests.api.base import Query +# Prevent execution of external binaries +os.environ["PATH"] = tempfile.mkdtemp() + + @pytest.fixture(scope="session") def loop(request): """Return an event loop and destroy it at the end of test""" @@ -119,6 +124,15 @@ def run_around_tests(monkeypatch): server_section["project_directory"] = tmppath config.set_section_config("Server", server_section) + # Prevent exectuions of the VM if we forgot to mock something + vbox_section = config.get_section_config("VirtualBox") + vbox_section["vboxmanage_path"] = tmppath + config.set_section_config("VirtualBox", vbox_section) + + vbox_section = config.get_section_config("VPCS") + vbox_section["vpcs_path"] = tmppath + config.set_section_config("VPCS", vbox_section) + monkeypatch.setattr("gns3server.modules.project.Project._get_default_project_directory", lambda *args: tmppath) yield From bf29e0319e5dc41f11e1bbe75d98856f7d8d3fa9 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 9 Feb 2015 10:38:34 +0100 Subject: [PATCH 208/485] Test logger and PEP8 --- tests/api/test_virtualbox.py | 12 +++++---- tests/web/test_logger.py | 50 ++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 tests/web/test_logger.py diff --git a/tests/api/test_virtualbox.py b/tests/api/test_virtualbox.py index f1f729d6..c37a56f0 100644 --- a/tests/api/test_virtualbox.py +++ b/tests/api/test_virtualbox.py @@ -18,6 +18,7 @@ import pytest from tests.utils import asyncio_patch + @pytest.yield_fixture(scope="module") def vm(server, project, monkeypatch): @@ -33,6 +34,7 @@ def vm(server, project, monkeypatch): with asyncio_patch("gns3server.modules.virtualbox.VirtualBox.find_vboxmanage", return_value=vboxmanage_path): yield response.json + def test_vbox_create(server, project): with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.create", return_value=True): @@ -92,11 +94,11 @@ def test_vbox_nio_create_udp(server, vm): with asyncio_patch('gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.adapter_add_nio_binding') as mock: response = server.post("/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/0/nio".format(project_id=vm["project_id"], - vm_id=vm["vm_id"]), {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"}, - example=True) + vm_id=vm["vm_id"]), {"type": "nio_udp", + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}, + example=True) assert mock.called args, kwgars = mock.call_args diff --git a/tests/web/test_logger.py b/tests/web/test_logger.py new file mode 100644 index 00000000..5867157d --- /dev/null +++ b/tests/web/test_logger.py @@ -0,0 +1,50 @@ +# -*- 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 logging + +from gns3server.web.logger import init_logger + + +def test_init_logger(caplog): + + logger = init_logger(logging.DEBUG) + logger.debug("DEBUG1") + assert "DEBUG1" in caplog.text() + logger.info("INFO1") + assert "INFO1" in caplog.text() + logger.warn("WARN1") + assert "WARN1" in caplog.text() + logger.error("ERROR1") + assert "ERROR1" in caplog.text() + logger.critical("CRITICAL1") + assert "CRITICAL1" in caplog.text() + + +def test_init_logger_quiet(caplog): + + logger = init_logger(logging.DEBUG, quiet=True) + logger.debug("DEBUG1") + assert "DEBUG1" not in caplog.text() + logger.info("INFO1") + assert "INFO1" not in caplog.text() + logger.warn("WARN1") + assert "WARN1" not in caplog.text() + logger.error("ERROR1") + assert "ERROR1" not in caplog.text() + logger.critical("CRITICAL1") + assert "CRITICAL1" not in caplog.text() From e99c0f6ac563bca1e565dd7b0454133155c00ff9 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 9 Feb 2015 10:59:09 +0100 Subject: [PATCH 209/485] I hope it's fix tests on Travis Python 3.3 --- tests/conftest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 490a35cd..d7e4da6c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -61,7 +61,7 @@ def _get_unused_port(): @pytest.fixture(scope="session") -def server(request, loop, port_manager): +def server(request, loop, port_manager, monkeypatch): """A GNS3 server""" port = _get_unused_port() @@ -78,6 +78,7 @@ def server(request, loop, port_manager): def tear_down(): for module in MODULES: instance = module.instance() + monkeypatch.setattr('gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.close', lambda self: True) loop.run_until_complete(instance.unload()) srv.close() srv.wait_closed() From b31af0abcdd492a2ad51ed1ad52ba0a779acb9c9 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 9 Feb 2015 11:26:22 +0100 Subject: [PATCH 210/485] Sub directory project-files --- gns3server/modules/project.py | 2 +- tests/modules/test_project.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index b3ba2c99..8fa0dc85 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -170,7 +170,7 @@ class Project: :returns: A string with a VM working directory """ - workdir = os.path.join(self._path, vm.manager.module_name.lower(), vm.id) + workdir = os.path.join(self._path, 'project-files', vm.manager.module_name.lower(), vm.id) try: os.makedirs(workdir, exist_ok=True) except OSError as e: diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py index b2ce3466..d410da31 100644 --- a/tests/modules/test_project.py +++ b/tests/modules/test_project.py @@ -115,8 +115,8 @@ def test_json(tmpdir): def test_vm_working_directory(tmpdir, vm): with patch("gns3server.config.Config.get_section_config", return_value={"local": True}): p = Project(location=str(tmpdir)) + assert p.vm_working_directory(vm) == os.path.join(str(tmpdir), p.id, 'project-files', vm.module_name, vm.id) assert os.path.exists(p.vm_working_directory(vm)) - assert os.path.exists(os.path.join(str(tmpdir), p.id, vm.module_name, vm.id)) def test_mark_vm_for_destruction(vm): From e1a80a9fabd489a1c1673e068c1593b81158c769 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 9 Feb 2015 19:58:23 +0100 Subject: [PATCH 211/485] Remove debug --- tests/modules/vpcs/test_vpcs_vm.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/modules/vpcs/test_vpcs_vm.py b/tests/modules/vpcs/test_vpcs_vm.py index b69d5bef..14a1e84b 100644 --- a/tests/modules/vpcs/test_vpcs_vm.py +++ b/tests/modules/vpcs/test_vpcs_vm.py @@ -183,8 +183,6 @@ def test_change_console_port(vm, port_manager): port2 = port_manager.get_free_console_port() port_manager.release_console_port(port1) port_manager.release_console_port(port2) - print(vm.console) - print(port1) vm.console = port1 vm.console = port2 assert vm.console == port2 From 2f85d71f321abf47abb6043dcc1d89b1bf75e2d7 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 9 Feb 2015 21:30:22 +0100 Subject: [PATCH 212/485] Correctly override the config from command line The tests was long write but allow me to found some typos bugs. --- gns3server/main.py | 52 +++++++++++------ tests/test_main.py | 139 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+), 17 deletions(-) create mode 100644 tests/test_main.py diff --git a/gns3server/main.py b/gns3server/main.py index ce249dad..f8ca55d6 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -21,6 +21,7 @@ import datetime import sys import locale import argparse +import configparser from gns3server.server import Server from gns3server.web.logger import init_logger @@ -72,37 +73,54 @@ def locale_check(): log.info("Current locale is {}.{}".format(language, encoding)) -def parse_arguments(): +def parse_arguments(argv, config): + """ + Parse command line arguments and override local configuration + + :params args: Array of command line arguments + :params config: ConfigParser with default variable from configuration + """ + + defaults = { + "host": config.get("host", "127.0.0.1"), + "port": config.get("port", 8000), + "ssl": config.getboolean("ssl", False), + "certfile": config.get("certfile", ""), + "certkey": config.get("certkey", ""), + "local": config.getboolean("local", False), + "allow": config.getboolean("allow_remote_console", False), + "quiet": config.getboolean("quiet", False), + "debug": config.getboolean("debug", False), + } parser = argparse.ArgumentParser(description="GNS3 server version {}".format(__version__)) + parser.set_defaults(**defaults) parser.add_argument("-v", "--version", help="show the version", action="version", version=__version__) - parser.add_argument("--host", help="run on the given host/IP address", default="127.0.0.1") - parser.add_argument("--port", help="run on the given port", type=int, default=8000) - parser.add_argument("--config", help="use this config file", type=str, default=None) + parser.add_argument("--host", help="run on the given host/IP address") + parser.add_argument("--port", help="run on the given port", type=int) parser.add_argument("--ssl", action="store_true", help="run in SSL mode") - parser.add_argument("--certfile", help="SSL cert file", default="") - parser.add_argument("--certkey", help="SSL key file", default="") + parser.add_argument("--certfile", help="SSL cert file") + parser.add_argument("--certkey", help="SSL key file") parser.add_argument("-L", "--local", action="store_true", help="local mode (allows some insecure operations)") parser.add_argument("-A", "--allow", action="store_true", help="allow remote connections to local console ports") parser.add_argument("-q", "--quiet", action="store_true", help="do not show logs on stdout") parser.add_argument("-d", "--debug", action="store_true", help="show debug logs and enable code live reload") - args = parser.parse_args() - return args + return parser.parse_args(argv) def set_config(args): config = Config.instance() server_config = config.get_section_config("Server") - server_config["local"] = server_config.get("local", "true" if args.local else "false") - server_config["allow_remote_console"] = server_config.get("allow_remote_console", "true" if args.allow else "false") - server_config["host"] = server_config.get("host", args.host) - server_config["port"] = server_config.get("port", str(args.port)) - server_config["ssl"] = server_config.get("ssl", "true" if args.ssl else "false") - server_config["certfile"] = server_config.get("certfile", args.certfile) - server_config["certkey"] = server_config.get("certkey", args.certkey) - server_config["debug"] = server_config.get("debug", "true" if args.debug else "false") + server_config["local"] = str(args.local) + server_config["allow_remote_console"] = str(args.allow) + server_config["host"] = args.host + server_config["port"] = str(args.port) + server_config["ssl"] = str(args.ssl) + server_config["certfile"] = args.certfile + server_config["certkey"] = args.certkey + server_config["debug"] = str(args.debug) config.set_section_config("Server", server_config) @@ -112,7 +130,7 @@ def main(): """ level = logging.INFO - args = parse_arguments() + args = parse_arguments(sys.argv[1:], Config.instance().get_section_config("Server")) if args.debug: level = logging.DEBUG diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 00000000..ed9006b3 --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python +# -*- 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 pytest +import locale + +from gns3server import main +from gns3server.config import Config +from gns3server.version import __version__ + + +def test_locale_check(): + + locale.setlocale(locale.LC_ALL, ("fr_FR")) + main.locale_check() + assert locale.getlocale() == ('fr_FR', 'UTF-8') + + +def test_parse_arguments(capsys): + + config = Config.instance() + server_config = config.get_section_config("Server") + + with pytest.raises(SystemExit): + main.parse_arguments(["--fail"], server_config) + out, err = capsys.readouterr() + assert "usage" in err + assert "fail" in err + assert "unrecognized arguments" in err + + with pytest.raises(SystemExit): + main.parse_arguments(["-v"], server_config) + out, err = capsys.readouterr() + assert __version__ in err + + with pytest.raises(SystemExit): + main.parse_arguments(["--version"], server_config) + out, err = capsys.readouterr() + assert __version__ in err + + with pytest.raises(SystemExit): + main.parse_arguments(["-h"], server_config) + out, err = capsys.readouterr() + assert __version__ in out + assert "optional arguments" in out + + with pytest.raises(SystemExit): + main.parse_arguments(["--help"], server_config) + out, err = capsys.readouterr() + assert __version__ in out + assert "optional arguments" in out + + assert main.parse_arguments(["--host", "192.168.1.1"], server_config).host == "192.168.1.1" + assert main.parse_arguments([], server_config).host == "127.0.0.1" + server_config["host"] = "192.168.1.2" + assert main.parse_arguments(["--host", "192.168.1.1"], server_config).host == "192.168.1.1" + assert main.parse_arguments([], server_config).host == "192.168.1.2" + + assert main.parse_arguments(["--port", "8002"], server_config).port == 8002 + assert main.parse_arguments([], server_config).port == 8000 + server_config["port"] = "8003" + assert main.parse_arguments([], server_config).port == 8003 + + assert main.parse_arguments(["--ssl"], server_config).ssl + assert main.parse_arguments([], server_config).ssl is False + server_config["ssl"] = "True" + assert main.parse_arguments([], server_config).ssl + + assert main.parse_arguments(["--certfile", "bla"], server_config).certfile == "bla" + assert main.parse_arguments([], server_config).certfile == "" + + assert main.parse_arguments(["--certkey", "blu"], server_config).certkey == "blu" + assert main.parse_arguments([], server_config).certkey == "" + + assert main.parse_arguments(["-L"], server_config).local + assert main.parse_arguments(["--local"], server_config).local + assert main.parse_arguments([], server_config).local is False + server_config["local"] = "True" + assert main.parse_arguments([], server_config).local + + assert main.parse_arguments(["-A"], server_config).allow + assert main.parse_arguments(["--allow"], server_config).allow + assert main.parse_arguments([], server_config).allow is False + server_config["allow_remote_console"] = "True" + assert main.parse_arguments([], server_config).allow + + assert main.parse_arguments(["-q"], server_config).quiet + assert main.parse_arguments(["--quiet"], server_config).quiet + assert main.parse_arguments([], server_config).quiet is False + + assert main.parse_arguments(["-d"], server_config).debug + assert main.parse_arguments([], server_config).debug is False + server_config["debug"] = "True" + assert main.parse_arguments([], server_config).debug + + +def test_set_config_with_args(): + + config = Config.instance() + args = main.parse_arguments(["--host", + "192.168.1.1", + "--local", + "--allow", + "--port", + "8001", + "--ssl", + "--certfile", + "bla", + "--certkey", + "blu", + "--debug"], + config.get_section_config("Server")) + main.set_config(args) + server_config = config.get_section_config("Server") + + assert server_config.getboolean("local") + assert server_config.getboolean("allow_remote_console") + assert server_config["host"] == "192.168.1.1" + assert server_config["port"] == "8001" + assert server_config.getboolean("ssl") + assert server_config["certfile"] == "bla" + assert server_config["certkey"] == "blu" + assert server_config.getboolean("debug") From 98586b93ee9d576935be576bdc550aff2253faf5 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 9 Feb 2015 13:41:31 -0700 Subject: [PATCH 213/485] Add timeout on stopping a VPCS just in case. --- gns3server/modules/vpcs/vpcs_vm.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index ff87cde9..036bfedc 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -74,7 +74,7 @@ class VPCSVM(BaseVM): def close(self): - self._kill_process() + self._terminate_process() if self._console: self._manager.port_manager.release_console_port(self._console) self._console = None @@ -251,10 +251,14 @@ class VPCSVM(BaseVM): Stops the VPCS process. """ - # stop the VPCS process if self.is_running(): - self._kill_process() - yield from self._process.wait() + self._terminate_process() + try: + yield from asyncio.wait_for(self._process.wait(), timeout=10) + except asyncio.TimeoutError: + self._process.kill() + if self._process.poll() is None: + log.warn("VPCS process {} is still running".format(self._process.pid)) self._process = None self._started = False @@ -268,8 +272,8 @@ class VPCSVM(BaseVM): yield from self.stop() yield from self.start() - def _kill_process(self): - """Kill the process if running""" + def _terminate_process(self): + """Terminate the process if running""" if self._process: log.info("Stopping VPCS instance {} PID={}".format(self.name, self._process.pid)) From 648850c411f8e12eb1da6c9b109e8d6f7e56f004 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 9 Feb 2015 13:42:50 -0700 Subject: [PATCH 214/485] Server listen to 0.0.0.0 by default. --- gns3server/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/main.py b/gns3server/main.py index f8ca55d6..4810fd68 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -82,7 +82,7 @@ def parse_arguments(argv, config): """ defaults = { - "host": config.get("host", "127.0.0.1"), + "host": config.get("host", "0.0.0.0"), "port": config.get("port", 8000), "ssl": config.getboolean("ssl", False), "certfile": config.get("certfile", ""), From 46cbcd613243d985581e7e762ae5dd50c77e0711 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 9 Feb 2015 18:24:13 -0700 Subject: [PATCH 215/485] New Dynamips integration part 1 --- gns3server/handlers/__init__.py | 7 +- gns3server/handlers/dynamips_handler.py | 58 + gns3server/modules/__init__.py | 4 +- gns3server/modules/dynamips/__init__.py | 611 ++++++ .../dynamips/adapters/__init__.py | 0 .../dynamips/adapters/adapter.py | 1 - .../dynamips/adapters/c1700_mb_1fe.py | 1 - .../dynamips/adapters/c1700_mb_wic1.py | 1 - .../dynamips/adapters/c2600_mb_1e.py | 1 - .../dynamips/adapters/c2600_mb_1fe.py | 1 - .../dynamips/adapters/c2600_mb_2e.py | 1 - .../dynamips/adapters/c2600_mb_2fe.py | 1 - .../dynamips/adapters/c7200_io_2fe.py | 1 - .../dynamips/adapters/c7200_io_fe.py | 1 - .../dynamips/adapters/c7200_io_ge_e.py | 1 - .../dynamips/adapters/gt96100_fe.py | 0 .../dynamips/adapters/leopard_2fe.py | 1 - .../dynamips/adapters/nm_16esw.py | 1 - .../dynamips/adapters/nm_1e.py | 1 - .../dynamips/adapters/nm_1fe_tx.py | 1 - .../dynamips/adapters/nm_4e.py | 1 - .../dynamips/adapters/nm_4t.py | 1 - .../dynamips/adapters/pa_2fe_tx.py | 1 - .../dynamips/adapters/pa_4e.py | 1 - .../dynamips/adapters/pa_4t.py | 1 - .../dynamips/adapters/pa_8e.py | 1 - .../dynamips/adapters/pa_8t.py | 1 - .../dynamips/adapters/pa_a1.py | 1 - .../dynamips/adapters/pa_fe_tx.py | 1 - .../dynamips/adapters/pa_ge.py | 1 - .../dynamips/adapters/pa_pos_oc3.py | 1 - .../dynamips/adapters/wic_1enet.py | 1 - .../dynamips/adapters/wic_1t.py | 1 - .../dynamips/adapters/wic_2t.py | 1 - .../dynamips/dynamips_error.py | 2 +- .../dynamips/dynamips_hypervisor.py | 428 ++--- .../dynamips/hypervisor.py | 139 +- .../dynamips/nios}/__init__.py | 0 .../dynamips/nios/nio.py | 75 +- .../dynamips/nios/nio_fifo.py | 17 +- .../dynamips/nios/nio_generic_ethernet.py | 15 +- .../dynamips/nios/nio_linux_ethernet.py | 17 +- .../dynamips/nios/nio_mcast.py | 28 +- .../dynamips/nios/nio_null.py | 11 +- .../dynamips/nios/nio_tap.py | 14 +- .../dynamips/nios/nio_udp.py | 25 +- .../dynamips/nios/nio_udp_auto.py | 33 +- .../dynamips/nios/nio_unix.py | 21 +- .../dynamips/nios/nio_vde.py | 1 - .../dynamips/nodes}/__init__.py | 0 .../dynamips/nodes/c1700.py | 1 - .../dynamips/nodes/c2600.py | 1 - .../dynamips/nodes/c2691.py | 1 - .../dynamips/nodes/c3600.py | 1 - .../dynamips/nodes/c3725.py | 1 - .../dynamips/nodes/c3745.py | 1 - .../dynamips/nodes/c7200.py | 7 +- gns3server/modules/dynamips/nodes/router.py | 1514 +++++++++++++++ gns3server/modules/port_manager.py | 4 +- gns3server/modules/vpcs/vpcs_vm.py | 4 +- gns3server/old_modules/dynamips/__init__.py | 578 ------ .../old_modules/dynamips/backends/atmsw.py | 395 ---- .../old_modules/dynamips/backends/ethhub.py | 353 ---- .../old_modules/dynamips/backends/ethsw.py | 382 ---- .../old_modules/dynamips/backends/frsw.py | 374 ---- .../old_modules/dynamips/backends/vm.py | 905 --------- .../dynamips/hypervisor_manager.py | 655 ------- .../old_modules/dynamips/nodes/__init__.py | 0 .../old_modules/dynamips/nodes/atm_bridge.py | 172 -- .../old_modules/dynamips/nodes/atm_switch.py | 406 ---- .../old_modules/dynamips/nodes/bridge.py | 122 -- .../dynamips/nodes/ethernet_switch.py | 342 ---- .../dynamips/nodes/frame_relay_switch.py | 328 ---- gns3server/old_modules/dynamips/nodes/hub.py | 189 -- .../old_modules/dynamips/nodes/router.py | 1676 ----------------- .../old_modules/dynamips/schemas/__init__.py | 0 .../old_modules/dynamips/schemas/atmsw.py | 322 ---- .../old_modules/dynamips/schemas/ethhub.py | 319 ---- .../old_modules/dynamips/schemas/ethsw.py | 348 ---- .../old_modules/dynamips/schemas/frsw.py | 322 ---- gns3server/old_modules/dynamips/schemas/vm.py | 719 ------- gns3server/schemas/dynamips.py | 108 ++ 82 files changed, 2677 insertions(+), 9407 deletions(-) create mode 100644 gns3server/handlers/dynamips_handler.py create mode 100644 gns3server/modules/dynamips/__init__.py rename gns3server/{old_modules => modules}/dynamips/adapters/__init__.py (100%) rename gns3server/{old_modules => modules}/dynamips/adapters/adapter.py (99%) rename gns3server/{old_modules => modules}/dynamips/adapters/c1700_mb_1fe.py (99%) rename gns3server/{old_modules => modules}/dynamips/adapters/c1700_mb_wic1.py (99%) rename gns3server/{old_modules => modules}/dynamips/adapters/c2600_mb_1e.py (99%) rename gns3server/{old_modules => modules}/dynamips/adapters/c2600_mb_1fe.py (99%) rename gns3server/{old_modules => modules}/dynamips/adapters/c2600_mb_2e.py (99%) rename gns3server/{old_modules => modules}/dynamips/adapters/c2600_mb_2fe.py (99%) rename gns3server/{old_modules => modules}/dynamips/adapters/c7200_io_2fe.py (99%) rename gns3server/{old_modules => modules}/dynamips/adapters/c7200_io_fe.py (99%) rename gns3server/{old_modules => modules}/dynamips/adapters/c7200_io_ge_e.py (99%) rename gns3server/{old_modules => modules}/dynamips/adapters/gt96100_fe.py (100%) rename gns3server/{old_modules => modules}/dynamips/adapters/leopard_2fe.py (99%) rename gns3server/{old_modules => modules}/dynamips/adapters/nm_16esw.py (99%) rename gns3server/{old_modules => modules}/dynamips/adapters/nm_1e.py (99%) rename gns3server/{old_modules => modules}/dynamips/adapters/nm_1fe_tx.py (99%) rename gns3server/{old_modules => modules}/dynamips/adapters/nm_4e.py (99%) rename gns3server/{old_modules => modules}/dynamips/adapters/nm_4t.py (99%) rename gns3server/{old_modules => modules}/dynamips/adapters/pa_2fe_tx.py (99%) rename gns3server/{old_modules => modules}/dynamips/adapters/pa_4e.py (99%) rename gns3server/{old_modules => modules}/dynamips/adapters/pa_4t.py (99%) rename gns3server/{old_modules => modules}/dynamips/adapters/pa_8e.py (99%) rename gns3server/{old_modules => modules}/dynamips/adapters/pa_8t.py (99%) rename gns3server/{old_modules => modules}/dynamips/adapters/pa_a1.py (99%) rename gns3server/{old_modules => modules}/dynamips/adapters/pa_fe_tx.py (99%) rename gns3server/{old_modules => modules}/dynamips/adapters/pa_ge.py (99%) rename gns3server/{old_modules => modules}/dynamips/adapters/pa_pos_oc3.py (99%) rename gns3server/{old_modules => modules}/dynamips/adapters/wic_1enet.py (99%) rename gns3server/{old_modules => modules}/dynamips/adapters/wic_1t.py (99%) rename gns3server/{old_modules => modules}/dynamips/adapters/wic_2t.py (99%) rename gns3server/{old_modules => modules}/dynamips/dynamips_error.py (96%) rename gns3server/{old_modules => modules}/dynamips/dynamips_hypervisor.py (58%) rename gns3server/{old_modules => modules}/dynamips/hypervisor.py (57%) rename gns3server/{old_modules/dynamips/backends => modules/dynamips/nios}/__init__.py (100%) rename gns3server/{old_modules => modules}/dynamips/nios/nio.py (79%) rename gns3server/{old_modules => modules}/dynamips/nios/nio_fifo.py (85%) rename gns3server/{old_modules => modules}/dynamips/nios/nio_generic_ethernet.py (87%) rename gns3server/{old_modules => modules}/dynamips/nios/nio_linux_ethernet.py (87%) rename gns3server/{old_modules => modules}/dynamips/nios/nio_mcast.py (83%) rename gns3server/{old_modules => modules}/dynamips/nios/nio_null.py (90%) rename gns3server/{old_modules => modules}/dynamips/nios/nio_tap.py (85%) rename gns3server/{old_modules => modules}/dynamips/nios/nio_udp.py (83%) rename gns3server/{old_modules => modules}/dynamips/nios/nio_udp_auto.py (83%) rename gns3server/{old_modules => modules}/dynamips/nios/nio_unix.py (85%) rename gns3server/{old_modules => modules}/dynamips/nios/nio_vde.py (99%) rename gns3server/{old_modules/dynamips/nios => modules/dynamips/nodes}/__init__.py (100%) rename gns3server/{old_modules => modules}/dynamips/nodes/c1700.py (99%) rename gns3server/{old_modules => modules}/dynamips/nodes/c2600.py (99%) rename gns3server/{old_modules => modules}/dynamips/nodes/c2691.py (99%) rename gns3server/{old_modules => modules}/dynamips/nodes/c3600.py (99%) rename gns3server/{old_modules => modules}/dynamips/nodes/c3725.py (99%) rename gns3server/{old_modules => modules}/dynamips/nodes/c3745.py (99%) rename gns3server/{old_modules => modules}/dynamips/nodes/c7200.py (97%) create mode 100644 gns3server/modules/dynamips/nodes/router.py delete mode 100644 gns3server/old_modules/dynamips/__init__.py delete mode 100644 gns3server/old_modules/dynamips/backends/atmsw.py delete mode 100644 gns3server/old_modules/dynamips/backends/ethhub.py delete mode 100644 gns3server/old_modules/dynamips/backends/ethsw.py delete mode 100644 gns3server/old_modules/dynamips/backends/frsw.py delete mode 100644 gns3server/old_modules/dynamips/backends/vm.py delete mode 100644 gns3server/old_modules/dynamips/hypervisor_manager.py delete mode 100644 gns3server/old_modules/dynamips/nodes/__init__.py delete mode 100644 gns3server/old_modules/dynamips/nodes/atm_bridge.py delete mode 100644 gns3server/old_modules/dynamips/nodes/atm_switch.py delete mode 100644 gns3server/old_modules/dynamips/nodes/bridge.py delete mode 100644 gns3server/old_modules/dynamips/nodes/ethernet_switch.py delete mode 100644 gns3server/old_modules/dynamips/nodes/frame_relay_switch.py delete mode 100644 gns3server/old_modules/dynamips/nodes/hub.py delete mode 100644 gns3server/old_modules/dynamips/nodes/router.py delete mode 100644 gns3server/old_modules/dynamips/schemas/__init__.py delete mode 100644 gns3server/old_modules/dynamips/schemas/atmsw.py delete mode 100644 gns3server/old_modules/dynamips/schemas/ethhub.py delete mode 100644 gns3server/old_modules/dynamips/schemas/ethsw.py delete mode 100644 gns3server/old_modules/dynamips/schemas/frsw.py delete mode 100644 gns3server/old_modules/dynamips/schemas/vm.py create mode 100644 gns3server/schemas/dynamips.py diff --git a/gns3server/handlers/__init__.py b/gns3server/handlers/__init__.py index de1a5c0b..727b4053 100644 --- a/gns3server/handlers/__init__.py +++ b/gns3server/handlers/__init__.py @@ -1 +1,6 @@ -__all__ = ['version_handler', 'network_handler', 'vpcs_handler', 'project_handler', 'virtualbox_handler'] +__all__ = ["version_handler", + "network_handler", + "vpcs_handler", + "project_handler", + "virtualbox_handler", + "dynamips_handler"] diff --git a/gns3server/handlers/dynamips_handler.py b/gns3server/handlers/dynamips_handler.py new file mode 100644 index 00000000..f7929708 --- /dev/null +++ b/gns3server/handlers/dynamips_handler.py @@ -0,0 +1,58 @@ +# -*- 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 os +from ..web.route import Route +from ..schemas.dynamips import ROUTER_CREATE_SCHEMA +from ..schemas.dynamips import ROUTER_OBJECT_SCHEMA +from ..modules.dynamips import Dynamips +from ..modules.project_manager import ProjectManager + + +class DynamipsHandler: + + """ + API entry points for Dynamips. + """ + + @classmethod + @Route.post( + r"/projects/{project_id}/dynamips/routers", + parameters={ + "project_id": "UUID for the project" + }, + status_codes={ + 201: "Instance created", + 400: "Invalid request", + 409: "Conflict" + }, + description="Create a new Dynamips router instance", + input=ROUTER_CREATE_SCHEMA) + #output=ROUTER_OBJECT_SCHEMA) + def create(request, response): + + dynamips_manager = Dynamips.instance() + vm = yield from dynamips_manager.create_vm(request.json.pop("name"), + request.match_info["project_id"], + request.json.get("vm_id")) + + #for name, value in request.json.items(): + # if hasattr(vm, name) and getattr(vm, name) != value: + # setattr(vm, name, value) + + response.set_status(201) + response.json(vm) diff --git a/gns3server/modules/__init__.py b/gns3server/modules/__init__.py index 5127dd83..4e2f51bb 100644 --- a/gns3server/modules/__init__.py +++ b/gns3server/modules/__init__.py @@ -15,8 +15,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import sys from .vpcs import VPCS from .virtualbox import VirtualBox +from .dynamips import Dynamips -MODULES = [VPCS, VirtualBox] +MODULES = [VPCS, VirtualBox, Dynamips] diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py new file mode 100644 index 00000000..9e9246bd --- /dev/null +++ b/gns3server/modules/dynamips/__init__.py @@ -0,0 +1,611 @@ +# -*- 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 . + +""" +Dynamips server module. +""" + +import sys +import os +import base64 +import tempfile +import shutil +import glob +import socket +from gns3server.config import Config + +# from .hypervisor import Hypervisor +# from .hypervisor_manager import HypervisorManager +# from .dynamips_error import DynamipsError +# +# # Nodes +# from .nodes.router import Router +# from .nodes.c1700 import C1700 +# from .nodes.c2600 import C2600 +# from .nodes.c2691 import C2691 +# from .nodes.c3600 import C3600 +# from .nodes.c3725 import C3725 +# from .nodes.c3745 import C3745 +# from .nodes.c7200 import C7200 +# from .nodes.bridge import Bridge +# from .nodes.ethernet_switch import EthernetSwitch +# from .nodes.atm_switch import ATMSwitch +# from .nodes.atm_bridge import ATMBridge +# from .nodes.frame_relay_switch import FrameRelaySwitch +# from .nodes.hub import Hub +# +# # Adapters +# from .adapters.c7200_io_2fe import C7200_IO_2FE +# from .adapters.c7200_io_fe import C7200_IO_FE +# from .adapters.c7200_io_ge_e import C7200_IO_GE_E +# from .adapters.nm_16esw import NM_16ESW +# from .adapters.nm_1e import NM_1E +# from .adapters.nm_1fe_tx import NM_1FE_TX +# from .adapters.nm_4e import NM_4E +# from .adapters.nm_4t import NM_4T +# from .adapters.pa_2fe_tx import PA_2FE_TX +# from .adapters.pa_4e import PA_4E +# from .adapters.pa_4t import PA_4T +# from .adapters.pa_8e import PA_8E +# from .adapters.pa_8t import PA_8T +# from .adapters.pa_a1 import PA_A1 +# from .adapters.pa_fe_tx import PA_FE_TX +# from .adapters.pa_ge import PA_GE +# from .adapters.pa_pos_oc3 import PA_POS_OC3 +# from .adapters.wic_1t import WIC_1T +# from .adapters.wic_2t import WIC_2T +# from .adapters.wic_1enet import WIC_1ENET +# +# # NIOs +# from .nios.nio_udp import NIO_UDP +# from .nios.nio_udp_auto import NIO_UDP_auto +# from .nios.nio_unix import NIO_UNIX +# from .nios.nio_vde import NIO_VDE +# from .nios.nio_tap import NIO_TAP +# from .nios.nio_generic_ethernet import NIO_GenericEthernet +# from .nios.nio_linux_ethernet import NIO_LinuxEthernet +# from .nios.nio_fifo import NIO_FIFO +# from .nios.nio_mcast import NIO_Mcast +# from .nios.nio_null import NIO_Null +# +# from .backends import vm +# from .backends import ethsw +# from .backends import ethhub +# from .backends import frsw +# from .backends import atmsw + +import time +import asyncio +import logging + +log = logging.getLogger(__name__) + +from pkg_resources import parse_version +from ..base_manager import BaseManager +from .dynamips_error import DynamipsError +from .hypervisor import Hypervisor +from .nodes.router import Router + + +class Dynamips(BaseManager): + + _VM_CLASS = Router + + def __init__(self): + + super().__init__() + self._dynamips_path = None + + # FIXME: temporary + self._working_dir = "/tmp" + self._dynamips_path = "/usr/local/bin/dynamips" + + def find_dynamips(self): + + # look for Dynamips + dynamips_path = self.config.get_section_config("Dynamips").get("dynamips_path") + if not dynamips_path: + dynamips_path = shutil.which("dynamips") + + if not dynamips_path: + raise DynamipsError("Could not find Dynamips") + if not os.path.isfile(dynamips_path): + raise DynamipsError("Dynamips {} is not accessible".format(dynamips_path)) + if not os.access(dynamips_path, os.X_OK): + raise DynamipsError("Dynamips is not executable") + + self._dynamips_path = dynamips_path + return dynamips_path + + @asyncio.coroutine + def _wait_for_hypervisor(self, host, port, timeout=10.0): + """ + Waits for an hypervisor to be started (accepting a socket connection) + + :param host: host/address to connect to the hypervisor + :param port: port to connect to the hypervisor + """ + + begin = time.time() + connection_success = False + last_exception = None + while time.time() - begin < timeout: + yield from asyncio.sleep(0.01) + try: + _, writer = yield from asyncio.open_connection(host, port) + writer.close() + except OSError as e: + last_exception = e + continue + connection_success = True + break + + if not connection_success: + raise DynamipsError("Couldn't connect to hypervisor on {}:{} :{}".format(host, port, last_exception)) + else: + log.info("Dynamips server ready after {:.4f} seconds".format(time.time() - begin)) + + @asyncio.coroutine + def start_new_hypervisor(self): + """ + Creates a new Dynamips process and start it. + + :returns: the new hypervisor instance + """ + + try: + # let the OS find an unused port for the Dynamips hypervisor + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.bind(("127.0.0.1", 0)) + port = sock.getsockname()[1] + except OSError as e: + raise DynamipsError("Could not find free port for the Dynamips hypervisor: {}".format(e)) + + hypervisor = Hypervisor(self._dynamips_path, self._working_dir, "127.0.0.1", port) + + log.info("Ceating new hypervisor {}:{} with working directory {}".format(hypervisor.host, hypervisor.port, self._working_dir)) + yield from hypervisor.start() + + yield from self._wait_for_hypervisor("127.0.0.1", port) + log.info("Hypervisor {}:{} has successfully started".format(hypervisor.host, hypervisor.port)) + + yield from hypervisor.connect() + if parse_version(hypervisor.version) < parse_version('0.2.11'): + raise DynamipsError("Dynamips version must be >= 0.2.11, detected version is {}".format(hypervisor.version)) + + return hypervisor + + +# class Dynamips(IModule): +# """ +# Dynamips module. +# +# :param name: module name +# :param args: arguments for the module +# :param kwargs: named arguments for the module +# """ +# +# def stop(self, signum=None): +# """ +# Properly stops the module. +# +# :param signum: signal number (if called by the signal handler) +# """ +# +# if not sys.platform.startswith("win32"): +# self._callback.stop() +# +# # automatically save configs for all router instances +# for router_id in self._routers: +# router = self._routers[router_id] +# try: +# router.save_configs() +# except DynamipsError: +# continue +# +# # stop all Dynamips hypervisors +# if self._hypervisor_manager: +# self._hypervisor_manager.stop_all_hypervisors() +# +# self.delete_dynamips_files() +# IModule.stop(self, signum) # this will stop the I/O loop +# +# def get_device_instance(self, device_id, instance_dict): +# """ +# Returns a device instance. +# +# :param device_id: device identifier +# :param instance_dict: dictionary containing the instances +# +# :returns: device instance +# """ +# +# if device_id not in instance_dict: +# log.debug("device ID {} doesn't exist".format(device_id), exc_info=1) +# self.send_custom_error("Device ID {} doesn't exist".format(device_id)) +# return None +# return instance_dict[device_id] +# +# def delete_dynamips_files(self): +# """ +# Deletes useless Dynamips files from the working directory +# """ +# +# files = glob.glob(os.path.join(self._working_dir, "dynamips", "*.ghost")) +# files += glob.glob(os.path.join(self._working_dir, "dynamips", "*_lock")) +# files += glob.glob(os.path.join(self._working_dir, "dynamips", "ilt_*")) +# files += glob.glob(os.path.join(self._working_dir, "dynamips", "c[0-9][0-9][0-9][0-9]_*_rommon_vars")) +# files += glob.glob(os.path.join(self._working_dir, "dynamips", "c[0-9][0-9][0-9][0-9]_*_ssa")) +# for file in files: +# try: +# log.debug("deleting file {}".format(file)) +# os.remove(file) +# except OSError as e: +# log.warn("could not delete file {}: {}".format(file, e)) +# continue +# +# @IModule.route("dynamips.reset") +# def reset(self, request=None): +# """ +# Resets the module (JSON-RPC notification). +# +# :param request: JSON request (not used) +# """ +# +# # automatically save configs for all router instances +# for router_id in self._routers: +# router = self._routers[router_id] +# try: +# router.save_configs() +# except DynamipsError: +# continue +# +# # stop all Dynamips hypervisors +# if self._hypervisor_manager: +# self._hypervisor_manager.stop_all_hypervisors() +# +# # resets the instance counters +# Router.reset() +# EthernetSwitch.reset() +# Hub.reset() +# FrameRelaySwitch.reset() +# ATMSwitch.reset() +# NIO_UDP.reset() +# NIO_UDP_auto.reset() +# NIO_UNIX.reset() +# NIO_VDE.reset() +# NIO_TAP.reset() +# NIO_GenericEthernet.reset() +# NIO_LinuxEthernet.reset() +# NIO_FIFO.reset() +# NIO_Mcast.reset() +# NIO_Null.reset() +# +# self._routers.clear() +# self._ethernet_switches.clear() +# self._frame_relay_switches.clear() +# self._atm_switches.clear() +# +# self.delete_dynamips_files() +# +# self._hypervisor_manager = None +# self._working_dir = self._projects_dir +# log.info("dynamips module has been reset") +# +# def start_hypervisor_manager(self): +# """ +# Starts the hypervisor manager. +# """ +# +# # check if Dynamips path exists +# if not os.path.isfile(self._dynamips): +# raise DynamipsError("Dynamips executable {} doesn't exist".format(self._dynamips)) +# +# # check if Dynamips is executable +# if not os.access(self._dynamips, os.X_OK): +# raise DynamipsError("Dynamips {} is not executable".format(self._dynamips)) +# +# workdir = os.path.join(self._working_dir, "dynamips") +# try: +# os.makedirs(workdir) +# except FileExistsError: +# pass +# except OSError as e: +# raise DynamipsError("Could not create working directory {}".format(e)) +# +# # check if the working directory is writable +# if not os.access(workdir, os.W_OK): +# raise DynamipsError("Cannot write to working directory {}".format(workdir)) +# +# log.info("starting the hypervisor manager with Dynamips working directory set to '{}'".format(workdir)) +# self._hypervisor_manager = HypervisorManager(self._dynamips, workdir, self._host, self._console_host) +# +# for name, value in self._hypervisor_manager_settings.items(): +# if hasattr(self._hypervisor_manager, name) and getattr(self._hypervisor_manager, name) != value: +# setattr(self._hypervisor_manager, name, value) +# +# @IModule.route("dynamips.settings") +# def settings(self, request): +# """ +# Set or update settings. +# +# Optional request parameters: +# - path (path to the Dynamips executable) +# - working_dir (path to a working directory) +# - project_name +# +# :param request: JSON request +# """ +# +# if request is None: +# self.send_param_error() +# return +# +# log.debug("received request {}".format(request)) +# +# #TODO: JSON schema validation +# if not self._hypervisor_manager: +# +# if "path" in request: +# self._dynamips = request.pop("path") +# +# if "working_dir" in request: +# self._working_dir = request.pop("working_dir") +# log.info("this server is local") +# else: +# self._working_dir = os.path.join(self._projects_dir, request["project_name"]) +# log.info("this server is remote with working directory path to {}".format(self._working_dir)) +# +# self._hypervisor_manager_settings = request +# +# else: +# if "project_name" in request: +# # for remote server +# new_working_dir = os.path.join(self._projects_dir, request["project_name"]) +# +# if self._projects_dir != self._working_dir != new_working_dir: +# +# # trick to avoid file locks by Dynamips on Windows +# if sys.platform.startswith("win"): +# self._hypervisor_manager.working_dir = tempfile.gettempdir() +# +# if not os.path.isdir(new_working_dir): +# try: +# self.delete_dynamips_files() +# 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 +# +# elif "working_dir" in request: +# # for local server +# new_working_dir = request.pop("working_dir") +# +# try: +# self._hypervisor_manager.working_dir = new_working_dir +# except DynamipsError as e: +# log.error("could not change working directory: {}".format(e)) +# return +# +# self._working_dir = new_working_dir +# +# # apply settings to the hypervisor manager +# for name, value in request.items(): +# if hasattr(self._hypervisor_manager, name) and getattr(self._hypervisor_manager, name) != value: +# setattr(self._hypervisor_manager, name, value) +# +# @IModule.route("dynamips.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) +# +# def create_nio(self, node, request): +# """ +# Creates a new NIO. +# +# :param node: node requesting the NIO +# :param request: the original request with the +# necessary information to create the NIO +# +# :returns: a NIO object +# """ +# +# 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 DynamipsError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e)) +# # check if we have an allocated NIO UDP auto +# nio = node.hypervisor.get_nio_udp_auto(lport) +# if not nio: +# # otherwise create an NIO UDP +# nio = NIO_UDP(node.hypervisor, lport, rhost, rport) +# else: +# nio.connect(rhost, rport) +# elif request["nio"]["type"] == "nio_generic_ethernet": +# ethernet_device = request["nio"]["ethernet_device"] +# if sys.platform.startswith("win"): +# # replace the interface name by the GUID on Windows +# interfaces = get_windows_interfaces() +# npf_interface = None +# for interface in interfaces: +# if interface["name"] == ethernet_device: +# npf_interface = interface["id"] +# if not npf_interface: +# raise DynamipsError("Could not find interface {} on this host".format(ethernet_device)) +# else: +# ethernet_device = npf_interface +# nio = NIO_GenericEthernet(node.hypervisor, ethernet_device) +# elif request["nio"]["type"] == "nio_linux_ethernet": +# if sys.platform.startswith("win"): +# raise DynamipsError("This NIO type is not supported on Windows") +# ethernet_device = request["nio"]["ethernet_device"] +# nio = NIO_LinuxEthernet(node.hypervisor, ethernet_device) +# elif request["nio"]["type"] == "nio_tap": +# tap_device = request["nio"]["tap_device"] +# nio = NIO_TAP(node.hypervisor, tap_device) +# elif request["nio"]["type"] == "nio_unix": +# local_file = request["nio"]["local_file"] +# remote_file = request["nio"]["remote_file"] +# nio = NIO_UNIX(node.hypervisor, local_file, remote_file) +# elif request["nio"]["type"] == "nio_vde": +# control_file = request["nio"]["control_file"] +# local_file = request["nio"]["local_file"] +# nio = NIO_VDE(node.hypervisor, control_file, local_file) +# elif request["nio"]["type"] == "nio_null": +# nio = NIO_Null(node.hypervisor) +# return nio +# +# def allocate_udp_port(self, node): +# """ +# Allocates a UDP port in order to create an UDP NIO. +# +# :param node: the node that needs to allocate an UDP port +# +# :returns: dictionary with the allocated host/port info +# """ +# +# port = node.hypervisor.allocate_udp_port() +# host = node.hypervisor.host +# +# log.info("{} [id={}] has allocated UDP port {} with host {}".format(node.name, +# node.id, +# port, +# host)) +# response = {"lport": port} +# return response +# +# def set_ghost_ios(self, router): +# """ +# Manages Ghost IOS support. +# +# :param router: Router instance +# """ +# +# if not router.mmap: +# raise DynamipsError("mmap support is required to enable ghost IOS support") +# +# ghost_instance = router.formatted_ghost_file() +# all_ghosts = [] +# +# # search of an existing ghost instance across all hypervisors +# for hypervisor in self._hypervisor_manager.hypervisors: +# all_ghosts.extend(hypervisor.ghosts) +# +# if ghost_instance not in all_ghosts: +# # create a new ghost IOS instance +# ghost = Router(router.hypervisor, "ghost-" + ghost_instance, router.platform, ghost_flag=True) +# ghost.image = router.image +# # for 7200s, the NPE must be set when using an NPE-G2. +# if router.platform == "c7200": +# ghost.npe = router.npe +# ghost.ghost_status = 1 +# ghost.ghost_file = ghost_instance +# ghost.ram = router.ram +# try: +# ghost.start() +# ghost.stop() +# except DynamipsError: +# raise +# finally: +# ghost.clean_delete() +# +# if router.ghost_file != ghost_instance: +# # set the ghost file to the router +# router.ghost_status = 2 +# router.ghost_file = ghost_instance +# +# def create_config_from_file(self, local_base_config, router, destination_config_path): +# """ +# Creates a config file from a local base config +# +# :param local_base_config: path the a local base config +# :param router: router instance +# :param destination_config_path: path to the destination config file +# +# :returns: relative path to the created config file +# """ +# +# log.info("creating config file {} from {}".format(destination_config_path, local_base_config)) +# config_path = destination_config_path +# config_dir = os.path.dirname(destination_config_path) +# try: +# os.makedirs(config_dir) +# except FileExistsError: +# pass +# except OSError as e: +# raise DynamipsError("Could not create configs directory: {}".format(e)) +# +# try: +# with open(local_base_config, "r", errors="replace") as f: +# config = f.read() +# with open(config_path, "w") as f: +# config = "!\n" + config.replace("\r", "") +# config = config.replace('%h', router.name) +# f.write(config) +# except OSError as e: +# raise DynamipsError("Could not save the configuration from {} to {}: {}".format(local_base_config, config_path, e)) +# return "configs" + os.sep + os.path.basename(config_path) +# +# def create_config_from_base64(self, config_base64, router, destination_config_path): +# """ +# Creates a config file from a base64 encoded config. +# +# :param config_base64: base64 encoded config +# :param router: router instance +# :param destination_config_path: path to the destination config file +# +# :returns: relative path to the created config file +# """ +# +# log.info("creating config file {} from base64".format(destination_config_path)) +# config = base64.decodebytes(config_base64.encode("utf-8")).decode("utf-8") +# config = "!\n" + config.replace("\r", "") +# config = config.replace('%h', router.name) +# config_dir = os.path.dirname(destination_config_path) +# try: +# os.makedirs(config_dir) +# except FileExistsError: +# pass +# except OSError as e: +# raise DynamipsError("Could not create configs directory: {}".format(e)) +# +# config_path = destination_config_path +# try: +# with open(config_path, "w") as f: +# log.info("saving startup-config to {}".format(config_path)) +# f.write(config) +# except OSError as e: +# raise DynamipsError("Could not save the configuration {}: {}".format(config_path, e)) +# return "configs" + os.sep + os.path.basename(config_path) diff --git a/gns3server/old_modules/dynamips/adapters/__init__.py b/gns3server/modules/dynamips/adapters/__init__.py similarity index 100% rename from gns3server/old_modules/dynamips/adapters/__init__.py rename to gns3server/modules/dynamips/adapters/__init__.py diff --git a/gns3server/old_modules/dynamips/adapters/adapter.py b/gns3server/modules/dynamips/adapters/adapter.py similarity index 99% rename from gns3server/old_modules/dynamips/adapters/adapter.py rename to gns3server/modules/dynamips/adapters/adapter.py index 40d82c7e..d963933e 100644 --- a/gns3server/old_modules/dynamips/adapters/adapter.py +++ b/gns3server/modules/dynamips/adapters/adapter.py @@ -17,7 +17,6 @@ class Adapter(object): - """ Base class for adapters. diff --git a/gns3server/old_modules/dynamips/adapters/c1700_mb_1fe.py b/gns3server/modules/dynamips/adapters/c1700_mb_1fe.py similarity index 99% rename from gns3server/old_modules/dynamips/adapters/c1700_mb_1fe.py rename to gns3server/modules/dynamips/adapters/c1700_mb_1fe.py index c94f551d..3c67f3df 100644 --- a/gns3server/old_modules/dynamips/adapters/c1700_mb_1fe.py +++ b/gns3server/modules/dynamips/adapters/c1700_mb_1fe.py @@ -19,7 +19,6 @@ from .adapter import Adapter class C1700_MB_1FE(Adapter): - """ Integrated 1 port FastEthernet adapter for c1700 platform. """ diff --git a/gns3server/old_modules/dynamips/adapters/c1700_mb_wic1.py b/gns3server/modules/dynamips/adapters/c1700_mb_wic1.py similarity index 99% rename from gns3server/old_modules/dynamips/adapters/c1700_mb_wic1.py rename to gns3server/modules/dynamips/adapters/c1700_mb_wic1.py index 9c6d2190..eca72358 100644 --- a/gns3server/old_modules/dynamips/adapters/c1700_mb_wic1.py +++ b/gns3server/modules/dynamips/adapters/c1700_mb_wic1.py @@ -19,7 +19,6 @@ from .adapter import Adapter class C1700_MB_WIC1(Adapter): - """ Fake module to provide a placeholder for slot 1 interfaces when WICs are inserted into WIC slot 1. diff --git a/gns3server/old_modules/dynamips/adapters/c2600_mb_1e.py b/gns3server/modules/dynamips/adapters/c2600_mb_1e.py similarity index 99% rename from gns3server/old_modules/dynamips/adapters/c2600_mb_1e.py rename to gns3server/modules/dynamips/adapters/c2600_mb_1e.py index bebe7fa9..26fe5497 100644 --- a/gns3server/old_modules/dynamips/adapters/c2600_mb_1e.py +++ b/gns3server/modules/dynamips/adapters/c2600_mb_1e.py @@ -19,7 +19,6 @@ from .adapter import Adapter class C2600_MB_1E(Adapter): - """ Integrated 1 port Ethernet adapter for the c2600 platform. """ diff --git a/gns3server/old_modules/dynamips/adapters/c2600_mb_1fe.py b/gns3server/modules/dynamips/adapters/c2600_mb_1fe.py similarity index 99% rename from gns3server/old_modules/dynamips/adapters/c2600_mb_1fe.py rename to gns3server/modules/dynamips/adapters/c2600_mb_1fe.py index 1ad294f2..768d9c95 100644 --- a/gns3server/old_modules/dynamips/adapters/c2600_mb_1fe.py +++ b/gns3server/modules/dynamips/adapters/c2600_mb_1fe.py @@ -19,7 +19,6 @@ from .adapter import Adapter class C2600_MB_1FE(Adapter): - """ Integrated 1 port FastEthernet adapter for the c2600 platform. """ diff --git a/gns3server/old_modules/dynamips/adapters/c2600_mb_2e.py b/gns3server/modules/dynamips/adapters/c2600_mb_2e.py similarity index 99% rename from gns3server/old_modules/dynamips/adapters/c2600_mb_2e.py rename to gns3server/modules/dynamips/adapters/c2600_mb_2e.py index 1e42d5dd..c2ca7442 100644 --- a/gns3server/old_modules/dynamips/adapters/c2600_mb_2e.py +++ b/gns3server/modules/dynamips/adapters/c2600_mb_2e.py @@ -19,7 +19,6 @@ from .adapter import Adapter class C2600_MB_2E(Adapter): - """ Integrated 2 port Ethernet adapter for the c2600 platform. """ diff --git a/gns3server/old_modules/dynamips/adapters/c2600_mb_2fe.py b/gns3server/modules/dynamips/adapters/c2600_mb_2fe.py similarity index 99% rename from gns3server/old_modules/dynamips/adapters/c2600_mb_2fe.py rename to gns3server/modules/dynamips/adapters/c2600_mb_2fe.py index dcd96581..a7e6df14 100644 --- a/gns3server/old_modules/dynamips/adapters/c2600_mb_2fe.py +++ b/gns3server/modules/dynamips/adapters/c2600_mb_2fe.py @@ -19,7 +19,6 @@ from .adapter import Adapter class C2600_MB_2FE(Adapter): - """ Integrated 2 port FastEthernet adapter for the c2600 platform. """ diff --git a/gns3server/old_modules/dynamips/adapters/c7200_io_2fe.py b/gns3server/modules/dynamips/adapters/c7200_io_2fe.py similarity index 99% rename from gns3server/old_modules/dynamips/adapters/c7200_io_2fe.py rename to gns3server/modules/dynamips/adapters/c7200_io_2fe.py index 8b545e99..0b8ae8a4 100644 --- a/gns3server/old_modules/dynamips/adapters/c7200_io_2fe.py +++ b/gns3server/modules/dynamips/adapters/c7200_io_2fe.py @@ -19,7 +19,6 @@ from .adapter import Adapter class C7200_IO_2FE(Adapter): - """ C7200-IO-2FE FastEthernet Input/Ouput controller. """ diff --git a/gns3server/old_modules/dynamips/adapters/c7200_io_fe.py b/gns3server/modules/dynamips/adapters/c7200_io_fe.py similarity index 99% rename from gns3server/old_modules/dynamips/adapters/c7200_io_fe.py rename to gns3server/modules/dynamips/adapters/c7200_io_fe.py index 784b154d..56e86cf1 100644 --- a/gns3server/old_modules/dynamips/adapters/c7200_io_fe.py +++ b/gns3server/modules/dynamips/adapters/c7200_io_fe.py @@ -19,7 +19,6 @@ from .adapter import Adapter class C7200_IO_FE(Adapter): - """ C7200-IO-FE FastEthernet Input/Ouput controller. """ diff --git a/gns3server/old_modules/dynamips/adapters/c7200_io_ge_e.py b/gns3server/modules/dynamips/adapters/c7200_io_ge_e.py similarity index 99% rename from gns3server/old_modules/dynamips/adapters/c7200_io_ge_e.py rename to gns3server/modules/dynamips/adapters/c7200_io_ge_e.py index f233dffd..12ebaed6 100644 --- a/gns3server/old_modules/dynamips/adapters/c7200_io_ge_e.py +++ b/gns3server/modules/dynamips/adapters/c7200_io_ge_e.py @@ -19,7 +19,6 @@ from .adapter import Adapter class C7200_IO_GE_E(Adapter): - """ C7200-IO-GE-E GigabitEthernet Input/Ouput controller. """ diff --git a/gns3server/old_modules/dynamips/adapters/gt96100_fe.py b/gns3server/modules/dynamips/adapters/gt96100_fe.py similarity index 100% rename from gns3server/old_modules/dynamips/adapters/gt96100_fe.py rename to gns3server/modules/dynamips/adapters/gt96100_fe.py diff --git a/gns3server/old_modules/dynamips/adapters/leopard_2fe.py b/gns3server/modules/dynamips/adapters/leopard_2fe.py similarity index 99% rename from gns3server/old_modules/dynamips/adapters/leopard_2fe.py rename to gns3server/modules/dynamips/adapters/leopard_2fe.py index db6ad9c2..0afa95c0 100644 --- a/gns3server/old_modules/dynamips/adapters/leopard_2fe.py +++ b/gns3server/modules/dynamips/adapters/leopard_2fe.py @@ -19,7 +19,6 @@ from .adapter import Adapter class Leopard_2FE(Adapter): - """ Integrated 2 port FastEthernet adapter for c3660 router. """ diff --git a/gns3server/old_modules/dynamips/adapters/nm_16esw.py b/gns3server/modules/dynamips/adapters/nm_16esw.py similarity index 99% rename from gns3server/old_modules/dynamips/adapters/nm_16esw.py rename to gns3server/modules/dynamips/adapters/nm_16esw.py index 31e74565..fc3755cd 100644 --- a/gns3server/old_modules/dynamips/adapters/nm_16esw.py +++ b/gns3server/modules/dynamips/adapters/nm_16esw.py @@ -19,7 +19,6 @@ from .adapter import Adapter class NM_16ESW(Adapter): - """ NM-16ESW FastEthernet network module. """ diff --git a/gns3server/old_modules/dynamips/adapters/nm_1e.py b/gns3server/modules/dynamips/adapters/nm_1e.py similarity index 99% rename from gns3server/old_modules/dynamips/adapters/nm_1e.py rename to gns3server/modules/dynamips/adapters/nm_1e.py index 59ac5569..ac200247 100644 --- a/gns3server/old_modules/dynamips/adapters/nm_1e.py +++ b/gns3server/modules/dynamips/adapters/nm_1e.py @@ -19,7 +19,6 @@ from .adapter import Adapter class NM_1E(Adapter): - """ NM-1E Ethernet network module. """ diff --git a/gns3server/old_modules/dynamips/adapters/nm_1fe_tx.py b/gns3server/modules/dynamips/adapters/nm_1fe_tx.py similarity index 99% rename from gns3server/old_modules/dynamips/adapters/nm_1fe_tx.py rename to gns3server/modules/dynamips/adapters/nm_1fe_tx.py index 26568306..9723f703 100644 --- a/gns3server/old_modules/dynamips/adapters/nm_1fe_tx.py +++ b/gns3server/modules/dynamips/adapters/nm_1fe_tx.py @@ -19,7 +19,6 @@ from .adapter import Adapter class NM_1FE_TX(Adapter): - """ NM-1FE-TX FastEthernet network module. """ diff --git a/gns3server/old_modules/dynamips/adapters/nm_4e.py b/gns3server/modules/dynamips/adapters/nm_4e.py similarity index 99% rename from gns3server/old_modules/dynamips/adapters/nm_4e.py rename to gns3server/modules/dynamips/adapters/nm_4e.py index 086b04ee..ae6a51ed 100644 --- a/gns3server/old_modules/dynamips/adapters/nm_4e.py +++ b/gns3server/modules/dynamips/adapters/nm_4e.py @@ -19,7 +19,6 @@ from .adapter import Adapter class NM_4E(Adapter): - """ NM-4E Ethernet network module. """ diff --git a/gns3server/old_modules/dynamips/adapters/nm_4t.py b/gns3server/modules/dynamips/adapters/nm_4t.py similarity index 99% rename from gns3server/old_modules/dynamips/adapters/nm_4t.py rename to gns3server/modules/dynamips/adapters/nm_4t.py index 77c3ecc8..df6db299 100644 --- a/gns3server/old_modules/dynamips/adapters/nm_4t.py +++ b/gns3server/modules/dynamips/adapters/nm_4t.py @@ -19,7 +19,6 @@ from .adapter import Adapter class NM_4T(Adapter): - """ NM-4T Serial network module. """ diff --git a/gns3server/old_modules/dynamips/adapters/pa_2fe_tx.py b/gns3server/modules/dynamips/adapters/pa_2fe_tx.py similarity index 99% rename from gns3server/old_modules/dynamips/adapters/pa_2fe_tx.py rename to gns3server/modules/dynamips/adapters/pa_2fe_tx.py index 09b677f3..8589ff2e 100644 --- a/gns3server/old_modules/dynamips/adapters/pa_2fe_tx.py +++ b/gns3server/modules/dynamips/adapters/pa_2fe_tx.py @@ -19,7 +19,6 @@ from .adapter import Adapter class PA_2FE_TX(Adapter): - """ PA-2FE-TX FastEthernet port adapter. """ diff --git a/gns3server/old_modules/dynamips/adapters/pa_4e.py b/gns3server/modules/dynamips/adapters/pa_4e.py similarity index 99% rename from gns3server/old_modules/dynamips/adapters/pa_4e.py rename to gns3server/modules/dynamips/adapters/pa_4e.py index d5981860..32564992 100644 --- a/gns3server/old_modules/dynamips/adapters/pa_4e.py +++ b/gns3server/modules/dynamips/adapters/pa_4e.py @@ -19,7 +19,6 @@ from .adapter import Adapter class PA_4E(Adapter): - """ PA-4E Ethernet port adapter. """ diff --git a/gns3server/old_modules/dynamips/adapters/pa_4t.py b/gns3server/modules/dynamips/adapters/pa_4t.py similarity index 99% rename from gns3server/old_modules/dynamips/adapters/pa_4t.py rename to gns3server/modules/dynamips/adapters/pa_4t.py index 5a1393bc..6a098a24 100644 --- a/gns3server/old_modules/dynamips/adapters/pa_4t.py +++ b/gns3server/modules/dynamips/adapters/pa_4t.py @@ -19,7 +19,6 @@ from .adapter import Adapter class PA_4T(Adapter): - """ PA-4T+ Serial port adapter. """ diff --git a/gns3server/old_modules/dynamips/adapters/pa_8e.py b/gns3server/modules/dynamips/adapters/pa_8e.py similarity index 99% rename from gns3server/old_modules/dynamips/adapters/pa_8e.py rename to gns3server/modules/dynamips/adapters/pa_8e.py index 96684055..a6b5075f 100644 --- a/gns3server/old_modules/dynamips/adapters/pa_8e.py +++ b/gns3server/modules/dynamips/adapters/pa_8e.py @@ -19,7 +19,6 @@ from .adapter import Adapter class PA_8E(Adapter): - """ PA-8E Ethernet port adapter. """ diff --git a/gns3server/old_modules/dynamips/adapters/pa_8t.py b/gns3server/modules/dynamips/adapters/pa_8t.py similarity index 99% rename from gns3server/old_modules/dynamips/adapters/pa_8t.py rename to gns3server/modules/dynamips/adapters/pa_8t.py index 723e026f..600a5c29 100644 --- a/gns3server/old_modules/dynamips/adapters/pa_8t.py +++ b/gns3server/modules/dynamips/adapters/pa_8t.py @@ -19,7 +19,6 @@ from .adapter import Adapter class PA_8T(Adapter): - """ PA-8T Serial port adapter. """ diff --git a/gns3server/old_modules/dynamips/adapters/pa_a1.py b/gns3server/modules/dynamips/adapters/pa_a1.py similarity index 99% rename from gns3server/old_modules/dynamips/adapters/pa_a1.py rename to gns3server/modules/dynamips/adapters/pa_a1.py index 469d9ce4..21d51f15 100644 --- a/gns3server/old_modules/dynamips/adapters/pa_a1.py +++ b/gns3server/modules/dynamips/adapters/pa_a1.py @@ -19,7 +19,6 @@ from .adapter import Adapter class PA_A1(Adapter): - """ PA-A1 ATM port adapter. """ diff --git a/gns3server/old_modules/dynamips/adapters/pa_fe_tx.py b/gns3server/modules/dynamips/adapters/pa_fe_tx.py similarity index 99% rename from gns3server/old_modules/dynamips/adapters/pa_fe_tx.py rename to gns3server/modules/dynamips/adapters/pa_fe_tx.py index 6434d2b4..70ce8489 100644 --- a/gns3server/old_modules/dynamips/adapters/pa_fe_tx.py +++ b/gns3server/modules/dynamips/adapters/pa_fe_tx.py @@ -19,7 +19,6 @@ from .adapter import Adapter class PA_FE_TX(Adapter): - """ PA-FE-TX FastEthernet port adapter. """ diff --git a/gns3server/old_modules/dynamips/adapters/pa_ge.py b/gns3server/modules/dynamips/adapters/pa_ge.py similarity index 99% rename from gns3server/old_modules/dynamips/adapters/pa_ge.py rename to gns3server/modules/dynamips/adapters/pa_ge.py index e466d905..f0287408 100644 --- a/gns3server/old_modules/dynamips/adapters/pa_ge.py +++ b/gns3server/modules/dynamips/adapters/pa_ge.py @@ -19,7 +19,6 @@ from .adapter import Adapter class PA_GE(Adapter): - """ PA-GE GigabitEthernet port adapter. """ diff --git a/gns3server/old_modules/dynamips/adapters/pa_pos_oc3.py b/gns3server/modules/dynamips/adapters/pa_pos_oc3.py similarity index 99% rename from gns3server/old_modules/dynamips/adapters/pa_pos_oc3.py rename to gns3server/modules/dynamips/adapters/pa_pos_oc3.py index de0bc5d1..b120de97 100644 --- a/gns3server/old_modules/dynamips/adapters/pa_pos_oc3.py +++ b/gns3server/modules/dynamips/adapters/pa_pos_oc3.py @@ -19,7 +19,6 @@ from .adapter import Adapter class PA_POS_OC3(Adapter): - """ PA-POS-OC3 port adapter. """ diff --git a/gns3server/old_modules/dynamips/adapters/wic_1enet.py b/gns3server/modules/dynamips/adapters/wic_1enet.py similarity index 99% rename from gns3server/old_modules/dynamips/adapters/wic_1enet.py rename to gns3server/modules/dynamips/adapters/wic_1enet.py index 2d5e62b7..dac79b6b 100644 --- a/gns3server/old_modules/dynamips/adapters/wic_1enet.py +++ b/gns3server/modules/dynamips/adapters/wic_1enet.py @@ -17,7 +17,6 @@ class WIC_1ENET(object): - """ WIC-1ENET Ethernet """ diff --git a/gns3server/old_modules/dynamips/adapters/wic_1t.py b/gns3server/modules/dynamips/adapters/wic_1t.py similarity index 99% rename from gns3server/old_modules/dynamips/adapters/wic_1t.py rename to gns3server/modules/dynamips/adapters/wic_1t.py index 2067246d..0f7cb3ad 100644 --- a/gns3server/old_modules/dynamips/adapters/wic_1t.py +++ b/gns3server/modules/dynamips/adapters/wic_1t.py @@ -17,7 +17,6 @@ class WIC_1T(object): - """ WIC-1T Serial """ diff --git a/gns3server/old_modules/dynamips/adapters/wic_2t.py b/gns3server/modules/dynamips/adapters/wic_2t.py similarity index 99% rename from gns3server/old_modules/dynamips/adapters/wic_2t.py rename to gns3server/modules/dynamips/adapters/wic_2t.py index b5af954e..2bf2d565 100644 --- a/gns3server/old_modules/dynamips/adapters/wic_2t.py +++ b/gns3server/modules/dynamips/adapters/wic_2t.py @@ -17,7 +17,6 @@ class WIC_2T(object): - """ WIC-2T Serial """ diff --git a/gns3server/old_modules/dynamips/dynamips_error.py b/gns3server/modules/dynamips/dynamips_error.py similarity index 96% rename from gns3server/old_modules/dynamips/dynamips_error.py rename to gns3server/modules/dynamips/dynamips_error.py index 332c6bbb..58c306ee 100644 --- a/gns3server/old_modules/dynamips/dynamips_error.py +++ b/gns3server/modules/dynamips/dynamips_error.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/old_modules/dynamips/dynamips_hypervisor.py b/gns3server/modules/dynamips/dynamips_hypervisor.py similarity index 58% rename from gns3server/old_modules/dynamips/dynamips_hypervisor.py rename to gns3server/modules/dynamips/dynamips_hypervisor.py index 1ac01ee1..954d6d4b 100644 --- a/gns3server/old_modules/dynamips/dynamips_hypervisor.py +++ b/gns3server/modules/dynamips/dynamips_hypervisor.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 @@ -23,14 +23,15 @@ http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L46 import socket import re import logging +import asyncio + from .dynamips_error import DynamipsError from .nios.nio_udp_auto import NIO_UDP_auto log = logging.getLogger(__name__) -class DynamipsHypervisor(object): - +class DynamipsHypervisor: """ Creates a new connection to a Dynamips server (also called hypervisor) @@ -54,18 +55,20 @@ class DynamipsHypervisor(object): self._ghosts = {} self._jitsharing_groups = {} self._working_dir = working_dir - self._console_start_port_range = 2001 - self._console_end_port_range = 2500 - self._aux_start_port_range = 2501 - self._aux_end_port_range = 3000 - self._udp_start_port_range = 10001 - self._udp_end_port_range = 20000 + # self._console_start_port_range = 2001 + # self._console_end_port_range = 2500 + # self._aux_start_port_range = 2501 + # self._aux_end_port_range = 3000 + # self._udp_start_port_range = 10001 + # self._udp_end_port_range = 20000 self._nio_udp_auto_instances = {} self._version = "N/A" self._timeout = timeout - self._socket = None self._uuid = None + self._reader = None + self._writer = None + @asyncio.coroutine def connect(self): """ Connects to the hypervisor. @@ -81,19 +84,22 @@ class DynamipsHypervisor(object): host = self._host try: - self._socket = socket.create_connection((host, self._port), self._timeout) + self._reader, self._writer = yield from asyncio.wait_for(asyncio.open_connection(host, self._port), timeout=self._timeout) except OSError as e: - raise DynamipsError("Could not connect to server: {}".format(e)) + raise DynamipsError("Could not connect to hypervisor {}:{} {}".format(host, self._port, e)) + except asyncio.TimeoutError: + raise DynamipsError("Timeout error while connecting to hypervisor {}:{}".format(host, self._port)) try: - self._version = self.send("hypervisor version")[0].split("-", 1)[0] + version = yield from self.send("hypervisor version") + self._version = version[0].split("-", 1)[0] except IndexError: self._version = "Unknown" - self._uuid = self.send("hypervisor uuid") + self._uuid = yield from self.send("hypervisor uuid") # this forces to send the working dir to Dynamips - self.working_dir = self._working_dir + yield from self.set_working_dir(self._working_dir) @property def version(self): @@ -105,66 +111,38 @@ class DynamipsHypervisor(object): return self._version - def module_list(self): - """ - Returns the modules supported by this hypervisor. - - :returns: module list - """ - - return self.send("hypervisor module_list") - - def cmd_list(self, module): - """ - Returns commands recognized by the specified module. - - :param module: the module name - :returns: command list - """ - - return self.send("hypervisor cmd_list {}".format(module)) - + @asyncio.coroutine def close(self): """ Closes the connection to this hypervisor (but leave it running). """ - self.send("hypervisor close") - self._socket.shutdown(socket.SHUT_RDWR) - self._socket.close() - self._socket = None + yield from self.send("hypervisor close") + self._writer.close() + self._reader, self._writer = None + @asyncio.coroutine def stop(self): """ Stops this hypervisor (will no longer run). """ - self.send("hypervisor stop") - self._socket.shutdown(socket.SHUT_RDWR) - self._socket.close() - self._socket = None + yield from self.send("hypervisor stop") + self._writer.close() + self._reader, self._writer = None self._nio_udp_auto_instances.clear() + @asyncio.coroutine def reset(self): """ Resets this hypervisor (used to get an empty configuration). """ - self.send('hypervisor reset') + yield from self.send("hypervisor reset") self._nio_udp_auto_instances.clear() - @property - def working_dir(self): - """ - Returns current working directory - - :returns: path to the working directory - """ - - return self._working_dir - - @working_dir.setter - def working_dir(self, working_dir): + @asyncio.coroutine + def set_working_dir(self, working_dir): """ Sets the working directory for this hypervisor. @@ -172,19 +150,19 @@ class DynamipsHypervisor(object): """ # encase working_dir in quotes to protect spaces in the path - self.send("hypervisor working_dir {}".format('"' + working_dir + '"')) + yield from self.send("hypervisor working_dir {}".format('"' + working_dir + '"')) self._working_dir = working_dir - log.debug("working directory set to {}".format(self._working_dir)) + log.debug("Working directory set to {}".format(self._working_dir)) - def save_config(self, filename): + @property + def working_dir(self): """ - Saves the configuration of all Dynamips instances into the specified file. + Returns current working directory - :param filename: path string + :returns: path to the working directory """ - # encase working_dir in quotes to protect spaces in the path - self.send("hypervisor save_config {}".format('"' + filename + '"')) + return self._working_dir @property def uuid(self): @@ -196,17 +174,6 @@ class DynamipsHypervisor(object): return self._uuid - @property - def socket(self): - """ - Returns the current socket used to communicate with this hypervisor. - - :returns: socket instance - """ - - assert self._socket - return self._socket - @property def devices(self): """ @@ -217,136 +184,136 @@ class DynamipsHypervisor(object): return self._devices - @devices.setter - def devices(self, devices): - """ - Sets the list of devices managed by this hypervisor instance. - This method is for internal use. - - :param devices: a list of device objects - """ - - self._devices = devices - - @property - def console_start_port_range(self): - """ - Returns the console start port range value - - :returns: console start port range value (integer) - """ - - return self._console_start_port_range - - @console_start_port_range.setter - def console_start_port_range(self, console_start_port_range): - """ - Set a new console start port range value - - :param console_start_port_range: console start port range value (integer) - """ - - self._console_start_port_range = console_start_port_range - - @property - def console_end_port_range(self): - """ - Returns the console end port range value - - :returns: console end port range value (integer) - """ - - return self._console_end_port_range - - @console_end_port_range.setter - def console_end_port_range(self, console_end_port_range): - """ - Set a new console end port range value - - :param console_end_port_range: console end port range value (integer) - """ - - self._console_end_port_range = console_end_port_range - - @property - def aux_start_port_range(self): - """ - Returns the auxiliary console start port range value - - :returns: auxiliary console start port range value (integer) - """ - - return self._aux_start_port_range - - @aux_start_port_range.setter - def aux_start_port_range(self, aux_start_port_range): - """ - Sets a new auxiliary console start port range value - - :param aux_start_port_range: auxiliary console start port range value (integer) - """ - - self._aux_start_port_range = aux_start_port_range - - @property - def aux_end_port_range(self): - """ - Returns the auxiliary console end port range value - - :returns: auxiliary console end port range value (integer) - """ - - return self._aux_end_port_range - - @aux_end_port_range.setter - def aux_end_port_range(self, aux_end_port_range): - """ - Sets a new auxiliary console end port range value - - :param aux_end_port_range: auxiliary console end port range value (integer) - """ - - self._aux_end_port_range = aux_end_port_range - - @property - def udp_start_port_range(self): - """ - Returns the UDP start port range value - - :returns: UDP start port range value (integer) - """ - - return self._udp_start_port_range - - @udp_start_port_range.setter - def udp_start_port_range(self, udp_start_port_range): - """ - Sets a new UDP start port range value - - :param udp_start_port_range: UDP start port range value (integer) - """ - - self._udp_start_port_range = udp_start_port_range - - @property - def udp_end_port_range(self): - """ - Returns the UDP end port range value - - :returns: UDP end port range value (integer) - """ - - return self._udp_end_port_range - - @udp_end_port_range.setter - def udp_end_port_range(self, udp_end_port_range): - """ - Sets an new UDP end port range value - - :param udp_end_port_range: UDP end port range value (integer) - """ - - self._udp_end_port_range = udp_end_port_range + # @devices.setter + # def devices(self, devices): + # """ + # Sets the list of devices managed by this hypervisor instance. + # This method is for internal use. + # + # :param devices: a list of device objects + # """ + # + # self._devices = devices + + # @property + # def console_start_port_range(self): + # """ + # Returns the console start port range value + # + # :returns: console start port range value (integer) + # """ + # + # return self._console_start_port_range + + # @console_start_port_range.setter + # def console_start_port_range(self, console_start_port_range): + # """ + # Set a new console start port range value + # + # :param console_start_port_range: console start port range value (integer) + # """ + # + # self._console_start_port_range = console_start_port_range + # + # @property + # def console_end_port_range(self): + # """ + # Returns the console end port range value + # + # :returns: console end port range value (integer) + # """ + # + # return self._console_end_port_range + # + # @console_end_port_range.setter + # def console_end_port_range(self, console_end_port_range): + # """ + # Set a new console end port range value + # + # :param console_end_port_range: console end port range value (integer) + # """ + # + # self._console_end_port_range = console_end_port_range + + # @property + # def aux_start_port_range(self): + # """ + # Returns the auxiliary console start port range value + # + # :returns: auxiliary console start port range value (integer) + # """ + # + # return self._aux_start_port_range + # + # @aux_start_port_range.setter + # def aux_start_port_range(self, aux_start_port_range): + # """ + # Sets a new auxiliary console start port range value + # + # :param aux_start_port_range: auxiliary console start port range value (integer) + # """ + # + # self._aux_start_port_range = aux_start_port_range + # + # @property + # def aux_end_port_range(self): + # """ + # Returns the auxiliary console end port range value + # + # :returns: auxiliary console end port range value (integer) + # """ + # + # return self._aux_end_port_range + # + # @aux_end_port_range.setter + # def aux_end_port_range(self, aux_end_port_range): + # """ + # Sets a new auxiliary console end port range value + # + # :param aux_end_port_range: auxiliary console end port range value (integer) + # """ + # + # self._aux_end_port_range = aux_end_port_range + + # @property + # def udp_start_port_range(self): + # """ + # Returns the UDP start port range value + # + # :returns: UDP start port range value (integer) + # """ + # + # return self._udp_start_port_range + # + # @udp_start_port_range.setter + # def udp_start_port_range(self, udp_start_port_range): + # """ + # Sets a new UDP start port range value + # + # :param udp_start_port_range: UDP start port range value (integer) + # """ + # + # self._udp_start_port_range = udp_start_port_range + # + # @property + # def udp_end_port_range(self): + # """ + # Returns the UDP end port range value + # + # :returns: UDP end port range value (integer) + # """ + # + # return self._udp_end_port_range + # + # @udp_end_port_range.setter + # def udp_end_port_range(self, udp_end_port_range): + # """ + # Sets an new UDP end port range value + # + # :param udp_end_port_range: UDP end port range value (integer) + # """ + # + # self._udp_end_port_range = udp_end_port_range @property def ghosts(self): @@ -388,25 +355,45 @@ class DynamipsHypervisor(object): self._jitsharing_groups[image_name] = group_number + @property + def port(self): + """ + Returns the port used to start the hypervisor. + + :returns: port number (integer) + """ + + return self._port + + @port.setter + def port(self, port): + """ + Sets the port used to start the hypervisor. + + :param port: port number (integer) + """ + + self._port = port + @property def host(self): """ - Returns this hypervisor host. + Returns the host (binding) used to start the hypervisor. - :returns: host (string) + :returns: host/address (string) """ return self._host - @property - def port(self): + @host.setter + def host(self, host): """ - Returns this hypervisor port. + Sets the host (binding) used to start the hypervisor. - :returns: port (integer) + :param host: host/address (string) """ - return self._port + self._host = host def get_nio_udp_auto(self, port): """ @@ -433,18 +420,7 @@ class DynamipsHypervisor(object): allocated_port = nio.lport return allocated_port - def send_raw(self, string): - """ - Sends a raw command to this hypervisor. Use sparingly. - - :param string: command string. - - :returns: command result (string) - """ - - result = self.send(string) - return result - + @asyncio.coroutine def send(self, command): """ Sends commands to this hypervisor. @@ -467,13 +443,13 @@ class DynamipsHypervisor(object): # but still have more data. The only thing we know for sure is the last line # will begin with '100-' or a '2xx-' and end with '\r\n' - if not self._socket: + if self._writer is None or self._reader is None: raise DynamipsError("Not connected") try: command = command.strip() + '\n' log.debug("sending {}".format(command)) - self.socket.sendall(command.encode('utf-8')) + self._writer.write(command.encode()) except OSError as e: raise DynamipsError("Lost communication with {host}:{port} :{error}, Dynamips process running: {run}" .format(host=self._host, port=self._port, error=e, run=self.is_running())) @@ -483,8 +459,8 @@ class DynamipsHypervisor(object): buf = '' while True: try: - chunk = self.socket.recv(1024) # match to Dynamips' buffer size - buf += chunk.decode("utf-8") + chunk = yield from self._reader.read(1024) # match to Dynamips' buffer size + buf += chunk.decode() except OSError as e: raise DynamipsError("Communication timed out with {host}:{port} :{error}, Dynamips process running: {run}" .format(host=self._host, port=self._port, error=e, run=self.is_running())) diff --git a/gns3server/old_modules/dynamips/hypervisor.py b/gns3server/modules/dynamips/hypervisor.py similarity index 57% rename from gns3server/old_modules/dynamips/hypervisor.py rename to gns3server/modules/dynamips/hypervisor.py index ffce2935..87e05f75 100644 --- a/gns3server/old_modules/dynamips/hypervisor.py +++ b/gns3server/modules/dynamips/hypervisor.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 @@ -20,9 +20,9 @@ Represents a Dynamips hypervisor and starts/stops the associated Dynamips proces """ import os -import time import subprocess import tempfile +import asyncio from .dynamips_hypervisor import DynamipsHypervisor from .dynamips_error import DynamipsError @@ -32,7 +32,6 @@ log = logging.getLogger(__name__) class Hypervisor(DynamipsHypervisor): - """ Hypervisor. @@ -58,11 +57,6 @@ class Hypervisor(DynamipsHypervisor): self._stdout_file = "" self._started = False - # settings used the load-balance hypervisors - # (for the hypervisor manager) - self._memory_load = 0 - self._ios_image_ref = "" - @property def id(self): """ @@ -103,99 +97,7 @@ class Hypervisor(DynamipsHypervisor): self._path = path - @property - def port(self): - """ - Returns the port used to start the Dynamips hypervisor. - - :returns: port number (integer) - """ - - return self._port - - @port.setter - def port(self, port): - """ - Sets the port used to start the Dynamips hypervisor. - - :param port: port number (integer) - """ - - self._port = port - - @property - def host(self): - """ - Returns the host (binding) used to start the Dynamips hypervisor. - - :returns: host/address (string) - """ - - return self._host - - @host.setter - def host(self, host): - """ - Sets the host (binding) used to start the Dynamips hypervisor. - - :param host: host/address (string) - """ - - self._host = host - - @property - def image_ref(self): - """ - Returns the reference IOS image name - (used by the hypervisor manager for load-balancing purposes). - - :returns: image reference name - """ - - return self._ios_image_ref - - @image_ref.setter - def image_ref(self, ios_image_name): - """ - Sets the reference IOS image name - (used by the hypervisor manager for load-balancing purposes). - - :param ios_image_name: image reference name - """ - - self._ios_image_ref = ios_image_name - - def increase_memory_load(self, memory): - """ - Increases the memory load of this hypervisor. - (used by the hypervisor manager for load-balancing purposes). - - :param memory: amount of RAM (integer) - """ - - self._memory_load += memory - - def decrease_memory_load(self, memory): - """ - Decreases the memory load of this hypervisor. - (used by the hypervisor manager for load-balancing purposes). - - :param memory: amount of RAM (integer) - """ - - self._memory_load -= memory - - @property - def memory_load(self): - """ - Returns the memory load of this hypervisor. - (used by the hypervisor manager for load-balancing purposes). - - :returns: amount of RAM (integer) - """ - - return self._memory_load - + @asyncio.coroutine def start(self): """ Starts the Dynamips hypervisor process. @@ -203,37 +105,38 @@ class Hypervisor(DynamipsHypervisor): self._command = self._build_command() try: - log.info("starting Dynamips: {}".format(self._command)) + log.info("Starting Dynamips: {}".format(self._command)) + with tempfile.NamedTemporaryFile(delete=False) as fd: self._stdout_file = fd.name log.info("Dynamips process logging to {}".format(fd.name)) - self._process = subprocess.Popen(self._command, - stdout=fd, - stderr=subprocess.STDOUT, - cwd=self._working_dir) - log.info("Dynamips started PID={}".format(self._process.pid)) + self._process = yield from asyncio.create_subprocess_exec(*self._command, + stdout=fd, + stderr=subprocess.STDOUT, + cwd=self._working_dir) + log.info("Dynamips process started PID={}".format(self._process.pid)) self._started = True except (OSError, subprocess.SubprocessError) as e: - log.error("could not start Dynamips: {}".format(e)) - raise DynamipsError("could not start Dynamips: {}".format(e)) + log.error("Could not start Dynamips: {}".format(e)) + raise DynamipsError("Could not start Dynamips: {}".format(e)) + @asyncio.coroutine def stop(self): """ Stops the Dynamips hypervisor process. """ if self.is_running(): + log.info("Stopping Dynamips process PID={}".format(self._process.pid)) DynamipsHypervisor.stop(self) - log.info("stopping Dynamips PID={}".format(self._process.pid)) + # give some time for the hypervisor to properly stop. + # time to delete UNIX NIOs for instance. + yield from asyncio.sleep(0.01) try: - # give some time for the hypervisor to properly stop. - # time to delete UNIX NIOs for instance. - time.sleep(0.01) - self._process.terminate() - self._process.wait(1) - except subprocess.TimeoutExpired: + yield from asyncio.wait_for(self._process.wait(), timeout=3) + except asyncio.TimeoutError: self._process.kill() - if self._process.poll() is None: + if self._process.returncode is None: log.warn("Dynamips process {} is still running".format(self._process.pid)) if self._stdout_file and os.access(self._stdout_file, os.W_OK): @@ -265,7 +168,7 @@ class Hypervisor(DynamipsHypervisor): :returns: True or False """ - if self._process and self._process.poll() is None: + if self._process and self._process.returncode is None: return True return False diff --git a/gns3server/old_modules/dynamips/backends/__init__.py b/gns3server/modules/dynamips/nios/__init__.py similarity index 100% rename from gns3server/old_modules/dynamips/backends/__init__.py rename to gns3server/modules/dynamips/nios/__init__.py diff --git a/gns3server/old_modules/dynamips/nios/nio.py b/gns3server/modules/dynamips/nios/nio.py similarity index 79% rename from gns3server/old_modules/dynamips/nios/nio.py rename to gns3server/modules/dynamips/nios/nio.py index 3b66d54f..256ddc1b 100644 --- a/gns3server/old_modules/dynamips/nios/nio.py +++ b/gns3server/modules/dynamips/nios/nio.py @@ -20,23 +20,24 @@ Base interface for Dynamips Network Input/Output (NIO) module ("nio"). http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L451 """ +import asyncio from ..dynamips_error import DynamipsError import logging log = logging.getLogger(__name__) -class NIO(object): - +class NIO: """ Base NIO class :param hypervisor: Dynamips hypervisor instance """ - def __init__(self, hypervisor): + def __init__(self, name, hypervisor): self._hypervisor = hypervisor + self._name = None self._bandwidth = None # no bandwidth constraint by default self._input_filter = None # no input filter applied by default self._output_filter = None # no output filter applied by default @@ -44,6 +45,7 @@ class NIO(object): self._output_filter_options = None # no output filter options by default self._dynamips_direction = {"in": 0, "out": 1, "both": 2} + @asyncio.coroutine def list(self): """ Returns all NIOs. @@ -51,18 +53,21 @@ class NIO(object): :returns: NIO list """ - return self._hypervisor.send("nio list") + nio_list = yield from self._hypervisor.send("nio list") + return nio_list + @asyncio.coroutine def delete(self): """ Deletes this NIO. """ if self._input_filter or self._output_filter: - self.unbind_filter("both") - self._hypervisor.send("nio delete {}".format(self._name)) + yield from self.unbind_filter("both") + yield from self._hypervisor.send("nio delete {}".format(self._name)) log.info("NIO {name} has been deleted".format(name=self._name)) + @asyncio.coroutine def rename(self, new_name): """ Renames this NIO @@ -70,13 +75,12 @@ class NIO(object): :param new_name: new NIO name """ - self._hypervisor.send("nio rename {name} {new_name}".format(name=self._name, - new_name=new_name)) + yield from self._hypervisor.send("nio rename {name} {new_name}".format(name=self._name, new_name=new_name)) - log.info("NIO {name} renamed to {new_name}".format(name=self._name, - new_name=new_name)) + log.info("NIO {name} renamed to {new_name}".format(name=self._name, new_name=new_name)) self._name = new_name + @asyncio.coroutine def debug(self, debug): """ Enables/Disables debugging for this NIO. @@ -84,9 +88,9 @@ class NIO(object): :param debug: debug value (0 = disable, enable = 1) """ - self._hypervisor.send("nio set_debug {name} {debug}".format(name=self._name, - debug=debug)) + yield from self._hypervisor.send("nio set_debug {name} {debug}".format(name=self._name, debug=debug)) + @asyncio.coroutine def bind_filter(self, direction, filter_name): """ Adds a packet filter to this NIO. @@ -101,9 +105,9 @@ class NIO(object): raise DynamipsError("Unknown direction {} to bind filter {}:".format(direction, filter_name)) dynamips_direction = self._dynamips_direction[direction] - self._hypervisor.send("nio bind_filter {name} {direction} {filter}".format(name=self._name, - direction=dynamips_direction, - filter=filter_name)) + yield from self._hypervisor.send("nio bind_filter {name} {direction} {filter}".format(name=self._name, + direction=dynamips_direction, + filter=filter_name)) if direction == "in": self._input_filter = filter_name @@ -113,6 +117,7 @@ class NIO(object): self._input_filter = filter_name self._output_filter = filter_name + @asyncio.coroutine def unbind_filter(self, direction): """ Removes packet filter for this NIO. @@ -124,8 +129,8 @@ class NIO(object): raise DynamipsError("Unknown direction {} to unbind filter:".format(direction)) dynamips_direction = self._dynamips_direction[direction] - self._hypervisor.send("nio unbind_filter {name} {direction}".format(name=self._name, - direction=dynamips_direction)) + yield from self._hypervisor.send("nio unbind_filter {name} {direction}".format(name=self._name, + direction=dynamips_direction)) if direction == "in": self._input_filter = None @@ -135,6 +140,7 @@ class NIO(object): self._input_filter = None self._output_filter = None + @asyncio.coroutine def setup_filter(self, direction, options): """ Setups a packet filter bound with this NIO. @@ -157,9 +163,9 @@ class NIO(object): raise DynamipsError("Unknown direction {} to setup filter:".format(direction)) dynamips_direction = self._dynamips_direction[direction] - self._hypervisor.send("nio setup_filter {name} {direction} {options}".format(name=self._name, - direction=dynamips_direction, - options=options)) + yield from self._hypervisor.send("nio setup_filter {name} {direction} {options}".format(name=self._name, + direction=dynamips_direction, + options=options)) if direction == "in": self._input_filter_options = options @@ -189,6 +195,7 @@ class NIO(object): return self._output_filter, self._output_filter_options + @asyncio.coroutine def get_stats(self): """ Gets statistics for this NIO. @@ -196,25 +203,16 @@ class NIO(object): :returns: NIO statistics (string with packets in, packets out, bytes in, bytes out) """ - return self._hypervisor.send("nio get_stats {}".format(self._name))[0] + stats = yield from self._hypervisor.send("nio get_stats {}".format(self._name)) + return stats[0] + @asyncio.coroutine def reset_stats(self): """ Resets statistics for this NIO. """ - self._hypervisor.send("nio reset_stats {}".format(self._name)) - - def set_bandwidth(self, bandwidth): - """ - Sets bandwidth constraint. - - :param bandwidth: bandwidth integer value (in Kb/s) - """ - - self._hypervisor.send("nio set_bandwidth {name} {bandwidth}".format(name=self._name, - bandwidth=bandwidth)) - self._bandwidth = bandwidth + yield from self._hypervisor.send("nio reset_stats {}".format(self._name)) @property def bandwidth(self): @@ -226,6 +224,17 @@ class NIO(object): return self._bandwidth + @asyncio.coroutine + def set_bandwidth(self, bandwidth): + """ + Sets bandwidth constraint. + + :param bandwidth: bandwidth integer value (in Kb/s) + """ + + yield from self._hypervisor.send("nio set_bandwidth {name} {bandwidth}".format(name=self._name, bandwidth=bandwidth)) + self._bandwidth = bandwidth + def __str__(self): """ NIO string representation. diff --git a/gns3server/old_modules/dynamips/nios/nio_fifo.py b/gns3server/modules/dynamips/nios/nio_fifo.py similarity index 85% rename from gns3server/old_modules/dynamips/nios/nio_fifo.py rename to gns3server/modules/dynamips/nios/nio_fifo.py index c85b679d..768d87af 100644 --- a/gns3server/old_modules/dynamips/nios/nio_fifo.py +++ b/gns3server/modules/dynamips/nios/nio_fifo.py @@ -19,6 +19,7 @@ Interface for FIFO NIOs. """ +import asyncio from .nio import NIO import logging @@ -26,7 +27,6 @@ log = logging.getLogger(__name__) class NIO_FIFO(NIO): - """ Dynamips FIFO NIO. @@ -44,10 +44,6 @@ class NIO_FIFO(NIO): NIO_FIFO._instance_count += 1 self._name = 'nio_fifo' + str(self._id) - self._hypervisor.send("nio create_fifo {}".format(self._name)) - - log.info("NIO FIFO {name} created.".format(name=self._name)) - @classmethod def reset(cls): """ @@ -56,6 +52,13 @@ class NIO_FIFO(NIO): cls._instance_count = 0 + @asyncio.coroutine + def create(self): + + yield from self._hypervisor.send("nio create_fifo {}".format(self._name)) + log.info("NIO FIFO {name} created.".format(name=self._name)) + + @asyncio.coroutine def crossconnect(self, nio): """ Establishes a cross-connect between this FIFO NIO and another one. @@ -63,7 +66,7 @@ class NIO_FIFO(NIO): :param nio: FIFO NIO instance """ - self._hypervisor.send("nio crossconnect_fifo {name} {nio}".format(name=self._name, - nio=nio)) + yield from self._hypervisor.send("nio crossconnect_fifo {name} {nio}".format(name=self._name, + nio=nio)) log.info("NIO FIFO {name} crossconnected with {nio_name}.".format(name=self._name, nio_name=nio.name)) diff --git a/gns3server/old_modules/dynamips/nios/nio_generic_ethernet.py b/gns3server/modules/dynamips/nios/nio_generic_ethernet.py similarity index 87% rename from gns3server/old_modules/dynamips/nios/nio_generic_ethernet.py rename to gns3server/modules/dynamips/nios/nio_generic_ethernet.py index 2a8b1443..4428e2bf 100644 --- a/gns3server/old_modules/dynamips/nios/nio_generic_ethernet.py +++ b/gns3server/modules/dynamips/nios/nio_generic_ethernet.py @@ -19,6 +19,7 @@ Interface for generic Ethernet NIOs (PCAP library). """ +import asyncio from .nio import NIO import logging @@ -26,7 +27,6 @@ log = logging.getLogger(__name__) class NIO_GenericEthernet(NIO): - """ Dynamips generic Ethernet NIO. @@ -46,11 +46,7 @@ class NIO_GenericEthernet(NIO): self._name = 'nio_gen_eth' + str(self._id) self._ethernet_device = ethernet_device - self._hypervisor.send("nio create_gen_eth {name} {eth_device}".format(name=self._name, - eth_device=ethernet_device)) - log.info("NIO Generic Ethernet {name} created with device {device}".format(name=self._name, - device=ethernet_device)) @classmethod def reset(cls): @@ -60,6 +56,15 @@ class NIO_GenericEthernet(NIO): cls._instance_count = 0 + @asyncio.coroutine + def create(self): + + yield from self._hypervisor.send("nio create_gen_eth {name} {eth_device}".format(name=self._name, + eth_device=self._ethernet_device)) + + log.info("NIO Generic Ethernet {name} created with device {device}".format(name=self._name, + device=self._ethernet_device)) + @property def ethernet_device(self): """ diff --git a/gns3server/old_modules/dynamips/nios/nio_linux_ethernet.py b/gns3server/modules/dynamips/nios/nio_linux_ethernet.py similarity index 87% rename from gns3server/old_modules/dynamips/nios/nio_linux_ethernet.py rename to gns3server/modules/dynamips/nios/nio_linux_ethernet.py index 25988aa8..28bfbe89 100644 --- a/gns3server/old_modules/dynamips/nios/nio_linux_ethernet.py +++ b/gns3server/modules/dynamips/nios/nio_linux_ethernet.py @@ -19,6 +19,7 @@ Interface for Linux Ethernet NIOs (Linux only). """ +import asyncio from .nio import NIO import logging @@ -26,7 +27,6 @@ log = logging.getLogger(__name__) class NIO_LinuxEthernet(NIO): - """ Dynamips Linux Ethernet NIO. @@ -46,12 +46,6 @@ class NIO_LinuxEthernet(NIO): self._name = 'nio_linux_eth' + str(self._id) self._ethernet_device = ethernet_device - self._hypervisor.send("nio create_linux_eth {name} {eth_device}".format(name=self._name, - eth_device=ethernet_device)) - - log.info("NIO Linux Ethernet {name} created with device {device}".format(name=self._name, - device=ethernet_device)) - @classmethod def reset(cls): """ @@ -60,6 +54,15 @@ class NIO_LinuxEthernet(NIO): cls._instance_count = 0 + @asyncio.coroutine + def create(self): + + yield from self._hypervisor.send("nio create_linux_eth {name} {eth_device}".format(name=self._name, + eth_device=self._ethernet_device)) + + log.info("NIO Linux Ethernet {name} created with device {device}".format(name=self._name, + device=self._ethernet_device)) + @property def ethernet_device(self): """ diff --git a/gns3server/old_modules/dynamips/nios/nio_mcast.py b/gns3server/modules/dynamips/nios/nio_mcast.py similarity index 83% rename from gns3server/old_modules/dynamips/nios/nio_mcast.py rename to gns3server/modules/dynamips/nios/nio_mcast.py index bcd42670..fe32135b 100644 --- a/gns3server/old_modules/dynamips/nios/nio_mcast.py +++ b/gns3server/modules/dynamips/nios/nio_mcast.py @@ -19,6 +19,7 @@ Interface for multicast NIOs. """ +import asyncio from .nio import NIO import logging @@ -26,7 +27,6 @@ log = logging.getLogger(__name__) class NIO_Mcast(NIO): - """ Dynamips Linux Ethernet NIO. @@ -49,14 +49,6 @@ class NIO_Mcast(NIO): self._port = port self._ttl = 1 # default TTL - self._hypervisor.send("nio create_mcast {name} {mgroup} {mport}".format(name=self._name, - mgroup=group, - mport=port)) - - log.info("NIO Multicast {name} created with mgroup={group}, mport={port}".format(name=self._name, - group=group, - port=port)) - @classmethod def reset(cls): """ @@ -65,6 +57,17 @@ class NIO_Mcast(NIO): cls._instance_count = 0 + @asyncio.coroutine + def create(self): + + yield from self._hypervisor.send("nio create_mcast {name} {mgroup} {mport}".format(name=self._name, + mgroup=self._group, + mport=self._port)) + + log.info("NIO Multicast {name} created with mgroup={group}, mport={port}".format(name=self._name, + group=self._group, + port=self._port)) + @property def group(self): """ @@ -95,14 +98,13 @@ class NIO_Mcast(NIO): return self._ttl - @ttl.setter - def ttl(self, ttl): + def set_ttl(self, ttl): """ Sets the TTL for the multicast address :param ttl: TTL value """ - self._hypervisor.send("nio set_mcast_ttl {name} {ttl}".format(name=self._name, - ttl=ttl)) + yield from self._hypervisor.send("nio set_mcast_ttl {name} {ttl}".format(name=self._name, + ttl=ttl)) self._ttl = ttl diff --git a/gns3server/old_modules/dynamips/nios/nio_null.py b/gns3server/modules/dynamips/nios/nio_null.py similarity index 90% rename from gns3server/old_modules/dynamips/nios/nio_null.py rename to gns3server/modules/dynamips/nios/nio_null.py index 1cde2a52..b8113e59 100644 --- a/gns3server/old_modules/dynamips/nios/nio_null.py +++ b/gns3server/modules/dynamips/nios/nio_null.py @@ -19,6 +19,7 @@ Interface for dummy NIOs (mostly for tests). """ +import asyncio from .nio import NIO import logging @@ -26,7 +27,6 @@ log = logging.getLogger(__name__) class NIO_Null(NIO): - """ Dynamips NULL NIO. @@ -44,9 +44,6 @@ class NIO_Null(NIO): NIO_Null._instance_count += 1 self._name = 'nio_null' + str(self._id) - self._hypervisor.send("nio create_null {}".format(self._name)) - log.info("NIO NULL {name} created.".format(name=self._name)) - @classmethod def reset(cls): """ @@ -54,3 +51,9 @@ class NIO_Null(NIO): """ cls._instance_count = 0 + + @asyncio.coroutine + def create(self): + + yield from self._hypervisor.send("nio create_null {}".format(self._name)) + log.info("NIO NULL {name} created.".format(name=self._name)) diff --git a/gns3server/old_modules/dynamips/nios/nio_tap.py b/gns3server/modules/dynamips/nios/nio_tap.py similarity index 85% rename from gns3server/old_modules/dynamips/nios/nio_tap.py rename to gns3server/modules/dynamips/nios/nio_tap.py index d24e9109..efe47a9e 100644 --- a/gns3server/old_modules/dynamips/nios/nio_tap.py +++ b/gns3server/modules/dynamips/nios/nio_tap.py @@ -19,6 +19,7 @@ Interface for TAP NIOs (UNIX based OSes only). """ +import asyncio from .nio import NIO import logging @@ -26,7 +27,6 @@ log = logging.getLogger(__name__) class NIO_TAP(NIO): - """ Dynamips TAP NIO. @@ -46,12 +46,6 @@ class NIO_TAP(NIO): self._name = 'nio_tap' + str(self._id) self._tap_device = tap_device - self._hypervisor.send("nio create_tap {name} {tap}".format(name=self._name, - tap=tap_device)) - - log.info("NIO TAP {name} created with device {device}".format(name=self._name, - device=tap_device)) - @classmethod def reset(cls): """ @@ -60,6 +54,12 @@ class NIO_TAP(NIO): cls._instance_count = 0 + @asyncio.coroutine + def create(self): + + yield from self._hypervisor.send("nio create_tap {name} {tap}".format(name=self._name, tap=self._tap_device)) + log.info("NIO TAP {name} created with device {device}".format(name=self._name, device=self._tap_device)) + @property def tap_device(self): """ diff --git a/gns3server/old_modules/dynamips/nios/nio_udp.py b/gns3server/modules/dynamips/nios/nio_udp.py similarity index 83% rename from gns3server/old_modules/dynamips/nios/nio_udp.py rename to gns3server/modules/dynamips/nios/nio_udp.py index d9e2d294..0bae8bbf 100644 --- a/gns3server/old_modules/dynamips/nios/nio_udp.py +++ b/gns3server/modules/dynamips/nios/nio_udp.py @@ -19,6 +19,7 @@ Interface for UDP NIOs. """ +import asyncio from .nio import NIO import logging @@ -26,7 +27,6 @@ log = logging.getLogger(__name__) class NIO_UDP(NIO): - """ Dynamips UDP NIO. @@ -50,16 +50,6 @@ class NIO_UDP(NIO): self._rhost = rhost self._rport = rport - self._hypervisor.send("nio create_udp {name} {lport} {rhost} {rport}".format(name=self._name, - lport=lport, - rhost=rhost, - rport=rport)) - - log.info("NIO UDP {name} created with lport={lport}, rhost={rhost}, rport={rport}".format(name=self._name, - lport=lport, - rhost=rhost, - rport=rport)) - @classmethod def reset(cls): """ @@ -68,6 +58,19 @@ class NIO_UDP(NIO): cls._instance_count = 0 + @asyncio.coroutine + def create(self): + + yield from self._hypervisor.send("nio create_udp {name} {lport} {rhost} {rport}".format(name=self._name, + lport=self._lport, + rhost=self._rhost, + rport=self._rport)) + + log.info("NIO UDP {name} created with lport={lport}, rhost={rhost}, rport={rport}".format(name=self._name, + lport=self._lport, + rhost=self._rhost, + rport=self._rport)) + @property def lport(self): """ diff --git a/gns3server/old_modules/dynamips/nios/nio_udp_auto.py b/gns3server/modules/dynamips/nios/nio_udp_auto.py similarity index 83% rename from gns3server/old_modules/dynamips/nios/nio_udp_auto.py rename to gns3server/modules/dynamips/nios/nio_udp_auto.py index 03d290d6..eb42e580 100644 --- a/gns3server/old_modules/dynamips/nios/nio_udp_auto.py +++ b/gns3server/modules/dynamips/nios/nio_udp_auto.py @@ -19,6 +19,7 @@ Interface for automatic UDP NIOs. """ +import asyncio from .nio import NIO import logging @@ -26,7 +27,6 @@ log = logging.getLogger(__name__) class NIO_UDP_auto(NIO): - """ Dynamips auto UDP NIO. @@ -48,15 +48,7 @@ class NIO_UDP_auto(NIO): self._name = 'nio_udp_auto' + str(self._id) self._laddr = laddr - self._lport = int(self._hypervisor.send("nio create_udp_auto {name} {laddr} {lport_start} {lport_end}".format(name=self._name, - laddr=laddr, - lport_start=lport_start, - lport_end=lport_end))[0]) - - log.info("NIO UDP AUTO {name} created with laddr={laddr}, lport_start={start}, lport_end={end}".format(name=self._name, - laddr=laddr, - start=lport_start, - end=lport_end)) + self._lport = None self._raddr = None self._rport = None @@ -68,6 +60,20 @@ class NIO_UDP_auto(NIO): cls._instance_count = 0 + @asyncio.coroutine + def create(self): + + port = yield from self._hypervisor.send("nio create_udp_auto {name} {laddr} {lport_start} {lport_end}".format(name=self._name, + laddr=self._laddr, + lport_start=self._lport_start, + lport_end=self._lport_end)) + self._lport = int(port[0]) + + log.info("NIO UDP AUTO {name} created with laddr={laddr}, lport_start={start}, lport_end={end}".format(name=self._name, + laddr=self._laddr, + start=self._lport_start, + end=self._lport_end)) + @property def laddr(self): """ @@ -108,6 +114,7 @@ class NIO_UDP_auto(NIO): return self._rport + @asyncio.coroutine def connect(self, raddr, rport): """ Connects this NIO to a remote socket @@ -116,9 +123,9 @@ class NIO_UDP_auto(NIO): :param rport: remote port number """ - self._hypervisor.send("nio connect_udp_auto {name} {raddr} {rport}".format(name=self._name, - raddr=raddr, - rport=rport)) + yield from self._hypervisor.send("nio connect_udp_auto {name} {raddr} {rport}".format(name=self._name, + raddr=raddr, + rport=rport)) self._raddr = raddr self._rport = rport diff --git a/gns3server/old_modules/dynamips/nios/nio_unix.py b/gns3server/modules/dynamips/nios/nio_unix.py similarity index 85% rename from gns3server/old_modules/dynamips/nios/nio_unix.py rename to gns3server/modules/dynamips/nios/nio_unix.py index af100d2e..af913f88 100644 --- a/gns3server/old_modules/dynamips/nios/nio_unix.py +++ b/gns3server/modules/dynamips/nios/nio_unix.py @@ -19,6 +19,7 @@ Interface for UNIX NIOs (Unix based OSes only). """ +import asyncio from .nio import NIO import logging @@ -26,7 +27,6 @@ log = logging.getLogger(__name__) class NIO_UNIX(NIO): - """ Dynamips UNIX NIO. @@ -48,14 +48,6 @@ class NIO_UNIX(NIO): self._local_file = local_file self._remote_file = remote_file - self._hypervisor.send("nio create_unix {name} {local} {remote}".format(name=self._name, - local=local_file, - remote=remote_file)) - - log.info("NIO UNIX {name} created with local file {local} and remote file {remote}".format(name=self._name, - local=local_file, - remote=remote_file)) - @classmethod def reset(cls): """ @@ -64,6 +56,17 @@ class NIO_UNIX(NIO): cls._instance_count = 0 + @asyncio.coroutine + def create(self): + + yield from self._hypervisor.send("nio create_unix {name} {local} {remote}".format(name=self._name, + local=self._local_file, + remote=self._remote_file)) + + log.info("NIO UNIX {name} created with local file {local} and remote file {remote}".format(name=self._name, + local=self._local_file, + remote=self._remote_file)) + @property def local_file(self): """ diff --git a/gns3server/old_modules/dynamips/nios/nio_vde.py b/gns3server/modules/dynamips/nios/nio_vde.py similarity index 99% rename from gns3server/old_modules/dynamips/nios/nio_vde.py rename to gns3server/modules/dynamips/nios/nio_vde.py index 7157834f..79af96d7 100644 --- a/gns3server/old_modules/dynamips/nios/nio_vde.py +++ b/gns3server/modules/dynamips/nios/nio_vde.py @@ -26,7 +26,6 @@ log = logging.getLogger(__name__) class NIO_VDE(NIO): - """ Dynamips VDE NIO. diff --git a/gns3server/old_modules/dynamips/nios/__init__.py b/gns3server/modules/dynamips/nodes/__init__.py similarity index 100% rename from gns3server/old_modules/dynamips/nios/__init__.py rename to gns3server/modules/dynamips/nodes/__init__.py diff --git a/gns3server/old_modules/dynamips/nodes/c1700.py b/gns3server/modules/dynamips/nodes/c1700.py similarity index 99% rename from gns3server/old_modules/dynamips/nodes/c1700.py rename to gns3server/modules/dynamips/nodes/c1700.py index 249ab508..906abe3e 100644 --- a/gns3server/old_modules/dynamips/nodes/c1700.py +++ b/gns3server/modules/dynamips/nodes/c1700.py @@ -29,7 +29,6 @@ log = logging.getLogger(__name__) class C1700(Router): - """ Dynamips c1700 router. diff --git a/gns3server/old_modules/dynamips/nodes/c2600.py b/gns3server/modules/dynamips/nodes/c2600.py similarity index 99% rename from gns3server/old_modules/dynamips/nodes/c2600.py rename to gns3server/modules/dynamips/nodes/c2600.py index 083bbce6..b5e46e89 100644 --- a/gns3server/old_modules/dynamips/nodes/c2600.py +++ b/gns3server/modules/dynamips/nodes/c2600.py @@ -31,7 +31,6 @@ log = logging.getLogger(__name__) class C2600(Router): - """ Dynamips c2600 router. diff --git a/gns3server/old_modules/dynamips/nodes/c2691.py b/gns3server/modules/dynamips/nodes/c2691.py similarity index 99% rename from gns3server/old_modules/dynamips/nodes/c2691.py rename to gns3server/modules/dynamips/nodes/c2691.py index fca62624..0dc0ef28 100644 --- a/gns3server/old_modules/dynamips/nodes/c2691.py +++ b/gns3server/modules/dynamips/nodes/c2691.py @@ -28,7 +28,6 @@ log = logging.getLogger(__name__) class C2691(Router): - """ Dynamips c2691 router. diff --git a/gns3server/old_modules/dynamips/nodes/c3600.py b/gns3server/modules/dynamips/nodes/c3600.py similarity index 99% rename from gns3server/old_modules/dynamips/nodes/c3600.py rename to gns3server/modules/dynamips/nodes/c3600.py index 8b9e8966..32e2bbe7 100644 --- a/gns3server/old_modules/dynamips/nodes/c3600.py +++ b/gns3server/modules/dynamips/nodes/c3600.py @@ -28,7 +28,6 @@ log = logging.getLogger(__name__) class C3600(Router): - """ Dynamips c3600 router. diff --git a/gns3server/old_modules/dynamips/nodes/c3725.py b/gns3server/modules/dynamips/nodes/c3725.py similarity index 99% rename from gns3server/old_modules/dynamips/nodes/c3725.py rename to gns3server/modules/dynamips/nodes/c3725.py index 76ba4d9f..9317a393 100644 --- a/gns3server/old_modules/dynamips/nodes/c3725.py +++ b/gns3server/modules/dynamips/nodes/c3725.py @@ -28,7 +28,6 @@ log = logging.getLogger(__name__) class C3725(Router): - """ Dynamips c3725 router. diff --git a/gns3server/old_modules/dynamips/nodes/c3745.py b/gns3server/modules/dynamips/nodes/c3745.py similarity index 99% rename from gns3server/old_modules/dynamips/nodes/c3745.py rename to gns3server/modules/dynamips/nodes/c3745.py index 0903b789..8002909a 100644 --- a/gns3server/old_modules/dynamips/nodes/c3745.py +++ b/gns3server/modules/dynamips/nodes/c3745.py @@ -28,7 +28,6 @@ log = logging.getLogger(__name__) class C3745(Router): - """ Dynamips c3745 router. diff --git a/gns3server/old_modules/dynamips/nodes/c7200.py b/gns3server/modules/dynamips/nodes/c7200.py similarity index 97% rename from gns3server/old_modules/dynamips/nodes/c7200.py rename to gns3server/modules/dynamips/nodes/c7200.py index 8ba10f8e..21ab4aa6 100644 --- a/gns3server/old_modules/dynamips/nodes/c7200.py +++ b/gns3server/modules/dynamips/nodes/c7200.py @@ -30,7 +30,6 @@ log = logging.getLogger(__name__) class C7200(Router): - """ Dynamips c7200 router (model is 7206). @@ -228,9 +227,9 @@ class C7200(Router): powered_on=power_supply)) log.info("router {name} [id={id}]: power supply {power_supply_id} state updated to {powered_on}".format(name=self._name, - id=self._id, - power_supply_id=power_supply_id, - powered_on=power_supply)) + id=self._id, + power_supply_id=power_supply_id, + powered_on=power_supply)) power_supply_id += 1 self._power_supplies = power_supplies diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py new file mode 100644 index 00000000..722100f4 --- /dev/null +++ b/gns3server/modules/dynamips/nodes/router.py @@ -0,0 +1,1514 @@ +# -*- 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 . + +""" +Interface for Dynamips virtual Machine module ("vm") +http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L77 +""" + +from ...base_vm import BaseVM +from ..dynamips_error import DynamipsError + +import asyncio +import time +import sys +import os +import base64 + +import logging +log = logging.getLogger(__name__) + + +class Router(BaseVM): + + """ + Dynamips router implementation. + """ + + _status = {0: "inactive", + 1: "shutting down", + 2: "running", + 3: "suspended"} + + def __init__(self, name, vm_id, project, manager, platform="c7200", ghost_flag=False): + + super().__init__(name, vm_id, project, manager) + + self._hypervisor = None + self._closed = False + self._name = name + self._platform = platform + self._image = "" + self._startup_config = "" + self._private_config = "" + self._ram = 128 # Megabytes + self._nvram = 128 # Kilobytes + self._mmap = True + self._sparsemem = True + self._clock_divisor = 8 + self._idlepc = "" + self._idlemax = 500 + self._idlesleep = 30 + self._ghost_file = "" + self._ghost_status = 0 + if sys.platform.startswith("win"): + self._exec_area = 16 # 16 MB by default on Windows (Cygwin) + else: + self._exec_area = 64 # 64 MB on other systems + self._disk0 = 0 # Megabytes + self._disk1 = 0 # Megabytes + self._confreg = "0x2102" + self._console = None + self._aux = None + self._mac_addr = None + self._system_id = "FTX0945W0MY" # processor board ID in IOS + self._slots = [] + self._ghost_flag = ghost_flag + + if not ghost_flag: + + if self._console is not None: + self._console = self._manager.port_manager.reserve_console_port(self._console) + else: + self._console = self._manager.port_manager.get_free_console_port() + + if self._aux is not None: + self._aux = self._manager.port_manager.reserve_console_port(self._aux) + else: + self._aux = self._manager.port_manager.get_free_console_port() + + def __json__(self): + + router_info = {"name": self.name, + "vm_id": self.id, + "project_id": self.project.id, + "platform": self._platform, + "image": self._image, + "startup_config": self._startup_config, + "private_config": self._private_config, + "ram": self._ram, + "nvram": self._nvram, + "mmap": self._mmap, + "sparsemem": self._sparsemem, + "clock_divisor": self._clock_divisor, + "idlepc": self._idlepc, + "idlemax": self._idlemax, + "idlesleep": self._idlesleep, + "exec_area": self._exec_area, + "disk0": self._disk0, + "disk1": self._disk1, + "confreg": self._confreg, + "console": self._console, + "aux": self._aux, + "mac_addr": self._mac_addr, + "system_id": self._system_id} + + # FIXME: add default slots/wics + #slot_id = 0 + #for slot in self._slots: + # if slot: + # slot = str(slot) + # router_defaults["slot" + str(slot_id)] = slot + # slot_id += 1 + + #if self._slots[0] and self._slots[0].wics: + # for wic_slot_id in range(0, len(self._slots[0].wics)): + # router_defaults["wic" + str(wic_slot_id)] = None + + return router_info + + @asyncio.coroutine + def create(self): + + self._hypervisor = yield from self.manager.start_new_hypervisor() + + yield from self._hypervisor.send("vm create '{name}' {id} {platform}".format(name=self._name, + id=self._id, + platform=self._platform)) + + if not self._ghost_flag: + + log.info("Router {platform} '{name}' [{id}] has been created".format(name=self._name, + platform=self._platform, + id=self._id)) + + yield from self._hypervisor.send("vm set_con_tcp_port '{name}' {console}".format(name=self._name, console=self._console)) + yield from self._hypervisor.send("vm set_aux_tcp_port '{name}' {aux}".format(name=self._name, aux=self._aux)) + + # get the default base MAC address + mac_addr = yield from self._hypervisor.send("{platform} get_mac_addr '{name}'".format(platform=self._platform, + name=self._name)) + self._mac_addr = mac_addr[0] + + self._hypervisor.devices.append(self) + + @asyncio.coroutine + def get_status(self): + """ + Returns the status of this router + + :returns: inactive, shutting down, running or suspended. + """ + + status = yield from self._hypervisor.send("vm get_status '{name}'".format(name=self._name)) + return self._status[int(status[0])] + + @asyncio.coroutine + def start(self): + """ + Starts this router. + At least the IOS image must be set before it can start. + """ + + status = yield from self.get_status() + if status == "suspended": + yield from self.resume() + elif status == "inactive": + + if not os.path.isfile(self._image) or not os.path.exists(self._image): + if os.path.islink(self._image): + raise DynamipsError("IOS image '{}' linked to '{}' is not accessible".format(self._image, os.path.realpath(self._image))) + else: + raise DynamipsError("IOS image '{}' is not accessible".format(self._image)) + + try: + with open(self._image, "rb") as f: + # read the first 7 bytes of the file. + elf_header_start = f.read(7) + except OSError as e: + raise DynamipsError("Cannot read ELF header for IOS image {}: {}".format(self._image, e)) + + # IOS images must start with the ELF magic number, be 32-bit, big endian and have an ELF version of 1 + if elf_header_start != b'\x7fELF\x01\x02\x01': + raise DynamipsError("'{}' is not a valid IOS image".format(self._image)) + + yield from self._hypervisor.send("vm start '{}'".format(self._name)) + log.info("router '{name}' [{id}] has been started".format(name=self._name, id=self._id)) + + @asyncio.coroutine + def stop(self): + """ + Stops this router. + """ + + status = yield from self.get_status() + if status != "inactive": + yield from self._hypervisor.send("vm stop '{name}'".format(self._name)) + log.info("Router '{name}' [{id}] has been stopped".format(name=self._name, id=self._id)) + + @asyncio.coroutine + def suspend(self): + """ + Suspends this router. + """ + + status = yield from self.get_status() + if status == "running": + yield from self._hypervisor.send("vm suspend '{}'".format(self._name)) + log.info("Router '{name}' [{id}] has been suspended".format(name=self._name, id=self._id)) + + @asyncio.coroutine + def resume(self): + """ + Resumes this suspended router + """ + + yield from self._hypervisor.send("vm resume '{}'".format(self._name)) + log.info("Router '{name}' [{id}] has been resumed".format(name=self._name, id=self._id)) + + @asyncio.coroutine + def is_running(self): + """ + Checks if this router is running. + + :returns: True if running, False otherwise + """ + + status = yield from self.get_status() + if status == "running": + return True + return False + + @asyncio.coroutine + def close(self): + + if self._closed: + # router is already closed + return + + if self._hypervisor: + yield from self.stop() + yield from self.hypervisor.stop() + + if self._console: + self._manager.port_manager.release_console_port(self._console) + self._console = None + + if self._aux: + self._manager.port_manager.release_console_port(self._aux) + self._aux = None + + self._closed = True + + @asyncio.coroutine + def delete(self): + """ + Deletes this router. + """ + + yield from self.close() + yield from self._hypervisor.send("vm delete '{}'".format(self._name)) + self._hypervisor.devices.remove(self) + log.info("router '{name}' [{id}] has been deleted".format(name=self._name, id=self._id)) + + @property + def platform(self): + """ + Returns the platform of this router. + + :returns: platform name (string): + c7200, c3745, c3725, c3600, c2691, c2600 or c1700 + """ + + return self._platform + + @property + def hypervisor(self): + """ + Returns the current hypervisor. + + :returns: hypervisor instance + """ + + return self._hypervisor + + @asyncio.coroutine + def list(self): + """ + Returns all VM instances + + :returns: list of all VM instances + """ + + vm_list = yield from self._hypervisor.send("vm list") + return vm_list + + @asyncio.coroutine + def list_con_ports(self): + """ + Returns all VM console TCP ports + + :returns: list of port numbers + """ + + port_list = yield from self._hypervisor.send("vm list_con_ports") + return port_list + + @asyncio.coroutine + def set_debug_level(self, level): + """ + Sets the debug level for this router (default is 0). + + :param level: level number + """ + + yield from self._hypervisor.send("vm set_debug_level '{name}' {level}".format(name=self._name, level=level)) + + @property + def image(self): + """ + Returns this IOS image for this router. + + :returns: path to IOS image file + """ + + return self._image + + @asyncio.coroutine + def set_image(self, image): + """ + Sets the IOS image for this router. + There is no default. + + :param image: path to IOS image file + """ + + # encase image in quotes to protect spaces in the path + yield from self._hypervisor.send("vm set_ios {name} {image}".format(name=self._name, image='"' + image + '"')) + + log.info("Router '{name}' [{id}]: has a new IOS image set: {image}".format(name=self._name, + id=self._id, + image='"' + image + '"')) + + self._image = image + + @property + def ram(self): + """ + Returns the amount of RAM allocated to this router. + + :returns: amount of RAM in Mbytes (integer) + """ + + return self._ram + + @asyncio.coroutine + def set_ram(self, ram): + """ + Sets amount of RAM allocated to this router + + :param ram: amount of RAM in Mbytes (integer) + """ + + if self._ram == ram: + return + + yield from self._hypervisor.send("vm set_ram '{name}' {ram}".format(name=self._name, ram=ram)) + log.info("Router '{name}' [{id}]: RAM updated from {old_ram}MB to {new_ram}MB".format(name=self._name, + id=self._id, + old_ram=self._ram, + new_ram=ram)) + self._ram = ram + + @property + def nvram(self): + """ + Returns the mount of NVRAM allocated to this router. + + :returns: amount of NVRAM in Kbytes (integer) + """ + + return self._nvram + + @asyncio.coroutine + def set_nvram(self, nvram): + """ + Sets amount of NVRAM allocated to this router + + :param nvram: amount of NVRAM in Kbytes (integer) + """ + + if self._nvram == nvram: + return + + yield from self._hypervisor.send("vm set_nvram '{name}' {nvram}".format(name=self._name, nvram=nvram)) + log.info("Router '{name}' [{id}]: NVRAM updated from {old_nvram}KB to {new_nvram}KB".format(name=self._name, + id=self._id, + old_nvram=self._nvram, + new_nvram=nvram)) + self._nvram = nvram + + @property + def mmap(self): + """ + Returns True if a mapped file is used to simulate this router memory. + + :returns: boolean either mmap is activated or not + """ + + return self._mmap + + @asyncio.coroutine + def set_mmap(self, mmap): + """ + Enable/Disable use of a mapped file to simulate router memory. + By default, a mapped file is used. This is a bit slower, but requires less memory. + + :param mmap: activate/deactivate mmap (boolean) + """ + + if mmap: + flag = 1 + else: + flag = 0 + + yield from self._hypervisor.send("vm set_ram_mmap '{name}' {mmap}".format(name=self._name, mmap=flag)) + + if mmap: + log.info("Router '{name}' [{id}]: mmap enabled".format(name=self._name, id=self._id)) + else: + log.info("Router '{name}' [{id}]: mmap disabled".format(name=self._name, id=self._id)) + self._mmap = mmap + + @property + def sparsemem(self): + """ + Returns True if sparse memory is used on this router. + + :returns: boolean either mmap is activated or not + """ + + return self._sparsemem + + @asyncio.coroutine + def set_sparsemem(self, sparsemem): + """ + Enable/disable use of sparse memory + + :param sparsemem: activate/deactivate sparsemem (boolean) + """ + + if sparsemem: + flag = 1 + else: + flag = 0 + yield from self._hypervisor.send("vm set_sparse_mem '{name}' {sparsemem}".format(name=self._name, sparsemem=flag)) + + if sparsemem: + log.info("Router '{name}' [{id}]: sparse memory enabled".format(name=self._name, id=self._id)) + else: + log.info("Router '{name}' [{id}]: sparse memory disabled".format(name=self._name, id=self._id)) + self._sparsemem = sparsemem + + @property + def clock_divisor(self): + """ + Returns the clock divisor value for this router. + + :returns: clock divisor value (integer) + """ + + return self._clock_divisor + + @asyncio.coroutine + def set_clock_divisor(self, clock_divisor): + """ + Sets the clock divisor value. The higher is the value, the faster is the clock in the + virtual machine. The default is 4, but it is often required to adjust it. + + :param clock_divisor: clock divisor value (integer) + """ + + yield from self._hypervisor.send("vm set_clock_divisor '{name}' {clock}".format(name=self._name, clock=clock_divisor)) + log.info("Router '{name}' [{id}]: clock divisor updated from {old_clock} to {new_clock}".format(name=self._name, + id=self._id, + old_clock=self._clock_divisor, + new_clock=clock_divisor)) + self._clock_divisor = clock_divisor + + @property + def idlepc(self): + """ + Returns the idle Pointer Counter (PC). + + :returns: idlepc value (string) + """ + + return self._idlepc + + @asyncio.coroutine + def set_idlepc(self, idlepc): + """ + Sets the idle Pointer Counter (PC) + + :param idlepc: idlepc value (string) + """ + + if not idlepc: + idlepc = "0x0" + + is_running = yield from self.is_running() + if not is_running: + # router is not running + yield from self._hypervisor.send("vm set_idle_pc '{name}' {idlepc}".format(name=self._name, idlepc=idlepc)) + else: + yield from self._hypervisor.send("vm set_idle_pc_online '{name}' 0 {idlepc}".format(name=self._name, idlepc=idlepc)) + + log.info("Router '{name}' [{id}]: idle-PC set to {idlepc}".format(name=self._name, id=self._id, idlepc=idlepc)) + self._idlepc = idlepc + + @asyncio.coroutine + def get_idle_pc_prop(self): + """ + Gets the idle PC proposals. + Takes 1000 measurements and records up to 10 idle PC proposals. + There is a 10ms wait between each measurement. + + :returns: list of idle PC proposal + """ + + is_running = yield from self.is_running() + if not is_running: + # router is not running + raise DynamipsError("Router '{name}' is not running".format(name=self._name)) + + log.info("Router '{name}' [{id}] has started calculating Idle-PC values".format(name=self._name, id=self._id)) + begin = time.time() + idlepcs = yield from self._hypervisor.send("vm get_idle_pc_prop '{}' 0".format(self._name)) + log.info("Router '{name}' [{id}] has finished calculating Idle-PC values after {time:.4f} seconds".format(name=self._name, + id=self._id, + time=time.time() - begin)) + return idlepcs + + @asyncio.coroutine + def show_idle_pc_prop(self): + """ + Dumps the idle PC proposals (previously generated). + + :returns: list of idle PC proposal + """ + + is_running = yield from self.is_running() + if not is_running: + # router is not running + raise DynamipsError("Router '{name}' is not running".format(name=self._name)) + + proposals = yield from self._hypervisor.send("vm show_idle_pc_prop '{}' 0".format(self._name)) + return proposals + + @property + def idlemax(self): + """ + Returns CPU idle max value. + + :returns: idle max (integer) + """ + + return self._idlemax + + @asyncio.coroutine + def set_idlemax(self, idlemax): + """ + Sets CPU idle max value + + :param idlemax: idle max value (integer) + """ + + is_running = yield from self.is_running() + if is_running: # router is running + yield from self._hypervisor.send("vm set_idle_max '{name}' 0 {idlemax}".format(name=self._name, idlemax=idlemax)) + + log.info("Router '{name}' [{id}]: idlemax updated from {old_idlemax} to {new_idlemax}".format(name=self._name, + id=self._id, + old_idlemax=self._idlemax, + new_idlemax=idlemax)) + + self._idlemax = idlemax + + @property + def idlesleep(self): + """ + Returns CPU idle sleep time value. + + :returns: idle sleep (integer) + """ + + return self._idlesleep + + @asyncio.coroutine + def set_idlesleep(self, idlesleep): + """ + Sets CPU idle sleep time value. + + :param idlesleep: idle sleep value (integer) + """ + + is_running = yield from self.is_running() + if is_running: # router is running + yield from self._hypervisor.send("vm set_idle_sleep_time '{name}' 0 {idlesleep}".format(name=self._name, + idlesleep=idlesleep)) + + log.info("Router '{name}' [{id}]: idlesleep updated from {old_idlesleep} to {new_idlesleep}".format(name=self._name, + id=self._id, + old_idlesleep=self._idlesleep, + new_idlesleep=idlesleep)) + + self._idlesleep = idlesleep + + @property + def ghost_file(self): + """ + Returns ghost RAM file. + + :returns: path to ghost file + """ + + return self._ghost_file + + @asyncio.coroutine + def set_ghost_file(self, ghost_file): + """ + Sets ghost RAM file + + :ghost_file: path to ghost file + """ + + yield from self._hypervisor.send("vm set_ghost_file '{name}' {ghost_file}".format(name=self._name, + ghost_file=ghost_file)) + + log.info("Router '{name}' [{id}]: ghost file set to {ghost_file}".format(name=self._name, + id=self._id, + ghost_file=ghost_file)) + + self._ghost_file = ghost_file + + # this is a ghost instance, track this as a hosted ghost instance by this hypervisor + if self.ghost_status == 1: + self._hypervisor.add_ghost(ghost_file, self) + + def formatted_ghost_file(self): + """ + Returns a properly formatted ghost file name. + + :returns: formatted ghost_file name (string) + """ + + # replace specials characters in 'drive:\filename' in Linux and Dynamips in MS Windows or viceversa. + ghost_file = "{}-{}.ghost".format(os.path.basename(self._image), self._ram) + ghost_file = ghost_file.replace('\\', '-').replace('/', '-').replace(':', '-') + return ghost_file + + @property + def ghost_status(self): + """Returns ghost RAM status + + :returns: ghost status (integer) + """ + + return self._ghost_status + + @asyncio.coroutine + def set_ghost_status(self, ghost_status): + """ + Sets ghost RAM status + + :param ghost_status: state flag indicating status + 0 => Do not use IOS ghosting + 1 => This is a ghost instance + 2 => Use an existing ghost instance + """ + + yield from self._hypervisor.send("vm set_ghost_status '{name}' {ghost_status}".format(name=self._name, + ghost_status=ghost_status)) + + log.info("Router '{name}' [{id}]: ghost status set to {ghost_status}".format(name=self._name, + id=self._id, + ghost_status=ghost_status)) + self._ghost_status = ghost_status + + @property + def exec_area(self): + """ + Returns the exec area value. + + :returns: exec area value (integer) + """ + + return self._exec_area + + @asyncio.coroutine + def set_exec_area(self, exec_area): + """ + Sets the exec area value. + The exec area is a pool of host memory used to store pages + translated by the JIT (they contain the native code + corresponding to MIPS code pages). + + :param exec_area: exec area value (integer) + """ + + yield from self._hypervisor.send("vm set_exec_area '{name}' {exec_area}".format(name=self._name, + exec_area=exec_area)) + + log.info("Router '{name}' [{id}]: exec area updated from {old_exec}MB to {new_exec}MB".format(name=self._name, + id=self._id, + old_exec=self._exec_area, + new_exec=exec_area)) + self._exec_area = exec_area + + @property + def disk0(self): + """ + Returns the size (MB) for PCMCIA disk0. + + :returns: disk0 size (integer) + """ + + return self._disk0 + + @asyncio.coroutine + def set_disk0(self, disk0): + """ + Sets the size (MB) for PCMCIA disk0. + + :param disk0: disk0 size (integer) + """ + + yield from self._hypervisor.send("vm set_disk0 '{name}' {disk0}".format(name=self._name, disk0=disk0)) + + log.info("Router {name} [{id}]: disk0 updated from {old_disk0}MB to {new_disk0}MB".format(name=self._name, + id=self._id, + old_disk0=self._disk0, + new_disk0=disk0)) + self._disk0 = disk0 + + @property + def disk1(self): + """ + Returns the size (MB) for PCMCIA disk1. + + :returns: disk1 size (integer) + """ + + return self._disk1 + + @asyncio.coroutine + def disk1(self, disk1): + """ + Sets the size (MB) for PCMCIA disk1. + + :param disk1: disk1 size (integer) + """ + + yield from self._hypervisor.send("vm set_disk1 '{name}' {disk1}".format(name=self._name, disk1=disk1)) + + log.info("Router '{name}' [{id}]: disk1 updated from {old_disk1}MB to {new_disk1}MB".format(name=self._name, + id=self._id, + old_disk1=self._disk1, + new_disk1=disk1)) + self._disk1 = disk1 + + @property + def confreg(self): + """ + Returns the configuration register. + The default is 0x2102. + + :returns: configuration register value (string) + """ + + return self._confreg + + @asyncio.coroutine + def set_confreg(self, confreg): + """ + Sets the configuration register. + + :param confreg: configuration register value (string) + """ + + yield from self._hypervisor.send("vm set_conf_reg '{name}' {confreg}".format(name=self._name, confreg=confreg)) + + log.info("Router '{name}' [{id}]: confreg updated from {old_confreg} to {new_confreg}".format(name=self._name, + id=self._id, + old_confreg=self._confreg, + new_confreg=confreg)) + self._confreg = confreg + + @property + def console(self): + """ + Returns the TCP console port. + + :returns: console port (integer) + """ + + return self._console + + @asyncio.coroutine + def set_console(self, console): + """ + Sets the TCP console port. + + :param console: console port (integer) + """ + + yield from self._hypervisor.send("vm set_con_tcp_port '{name}' {console}".format(name=self._name, console=console)) + + log.info("Router '{name}' [{id}]: console port updated from {old_console} to {new_console}".format(name=self._name, + id=self._id, + old_console=self._console, + new_console=console)) + + self._manager.port_manager.release_console_port(self._console) + self._console = self._manager.port_manager.reserve_console_port(console) + + @property + def aux(self): + """ + Returns the TCP auxiliary port. + + :returns: console auxiliary port (integer) + """ + + return self._aux + + @asyncio.coroutine + def set_aux(self, aux): + """ + Sets the TCP auxiliary port. + + :param aux: console auxiliary port (integer) + """ + + yield from self._hypervisor.send("vm set_aux_tcp_port '{name}' {aux}".format(name=self._name, aux=aux)) + + log.info("Router '{name}' [{id}]: aux port updated from {old_aux} to {new_aux}".format(name=self._name, + id=self._id, + old_aux=self._aux, + new_aux=aux)) + + self._manager.port_manager.release_console_port(self._aux) + self._aux = self._manager.port_manager.reserve_console_port(aux) + + @asyncio.coroutine + def get_cpu_usage(self, cpu_id=0): + """ + Shows cpu usage in seconds, "cpu_id" is ignored. + + :returns: cpu usage in seconds + """ + + cpu_usage = yield from self._hypervisor.send("vm cpu_usage '{name}' {cpu_id}".format(name=self._name, cpu_id=cpu_id)) + return int(cpu_usage[0]) + + @property + def mac_addr(self): + """ + Returns the MAC address. + + :returns: the MAC address (hexadecimal format: hh:hh:hh:hh:hh:hh) + """ + + return self._mac_addr + + @asyncio.coroutine + def set_mac_addr(self, mac_addr): + """ + Sets the MAC address. + + :param mac_addr: a MAC address (hexadecimal format: hh:hh:hh:hh:hh:hh) + """ + + yield from self._hypervisor.send("{platform} set_mac_addr '{name}' {mac_addr}".format(platform=self._platform, + name=self._name, + mac_addr=mac_addr)) + + log.info("Router '{name}' [{id}]: MAC address updated from {old_mac} to {new_mac}".format(name=self._name, + id=self._id, + old_mac=self._mac_addr, + new_mac=mac_addr)) + self._mac_addr = mac_addr + + @property + def system_id(self): + """ + Returns the system ID. + + :returns: the system ID (also called board processor ID) + """ + + return self._system_id + + @asyncio.coroutine + def set_system_id(self, system_id): + """ + Sets the system ID. + + :param system_id: a system ID (also called board processor ID) + """ + + yield from self._hypervisor.send("{platform} set_system_id '{name}' {system_id}".format(platform=self._platform, + name=self._name, + system_id=system_id)) + + log.info("Router '{name'} [{id}]: system ID updated from {old_id} to {new_id}".format(name=self._name, + id=self._id, + old_id=self._system_id, + new_id=system_id)) + self._system_id = system_id + + @asyncio.coroutine + def get_slot_bindings(self): + """ + Returns slot bindings. + + :returns: slot bindings (adapter names) list + """ + + slot_bindings = yield from self._hypervisor.send("vm slot_bindings '{}'".format(self._name)) + return slot_bindings + + @asyncio.coroutine + def slot_add_binding(self, slot_id, adapter): + """ + Adds a slot binding (a module into a slot). + + :param slot_id: slot ID + :param adapter: device to add in the corresponding slot + """ + + try: + slot = self._slots[slot_id] + except IndexError: + raise DynamipsError("Slot {slot_id} doesn't exist on router '{name}'".format(name=self._name, slot_id=slot_id)) + + if slot is not None: + current_adapter = slot + raise DynamipsError("Slot {slot_id} is already occupied by adapter {adapter} on router '{name}'".format(name=self._name, + slot_id=slot_id, + adapter=current_adapter)) + + is_running = yield from self.is_running() + + # Only c7200, c3600 and c3745 (NM-4T only) support new adapter while running + if is_running and not ((self._platform == 'c7200' and not str(adapter).startswith('C7200')) + and not (self._platform == 'c3600' and self.chassis == '3660') + and not (self._platform == 'c3745' and adapter == 'NM-4T')): + raise DynamipsError("Adapter {adapter} cannot be added while router '{name} 'is running".format(adapter=adapter, + name=self._name)) + + yield from self._hypervisor.send("vm slot_add_binding '{name}' {slot_id} 0 {adapter}".format(name=self._name, + slot_id=slot_id, + adapter=adapter)) + + log.info("Router '{name}' [{id}]: adapter {adapter} inserted into slot {slot_id}".format(name=self._name, + id=self._id, + adapter=adapter, + slot_id=slot_id)) + + self._slots[slot_id] = adapter + + # Generate an OIR event if the router is running + if is_running: + + yield from self._hypervisor.send("vm slot_oir_start '{name}' {slot_id} 0".format(name=self._name, slot_id=slot_id)) + + log.info("Router '{name}' [{id}]: OIR start event sent to slot {slot_id}".format(name=self._name, + id=self._id, + slot_id=slot_id)) + + @asyncio.coroutine + def slot_remove_binding(self, slot_id): + """ + Removes a slot binding (a module from a slot). + + :param slot_id: slot ID + """ + + try: + adapter = self._slots[slot_id] + except IndexError: + raise DynamipsError("Slot {slot_id} doesn't exist on router '{name}'".format(name=self._name, slot_id=slot_id)) + + if adapter is None: + raise DynamipsError("No adapter in slot {slot_id} on router '{name}'".format(name=self._name, + slot_id=slot_id)) + + is_running = yield from self.is_running() + + # Only c7200, c3600 and c3745 (NM-4T only) support to remove adapter while running + if is_running and not ((self._platform == 'c7200' and not str(adapter).startswith('C7200')) + and not (self._platform == 'c3600' and self.chassis == '3660') + and not (self._platform == 'c3745' and adapter == 'NM-4T')): + raise DynamipsError("Adapter {adapter} cannot be removed while router '{name}' is running".format(adapter=adapter, + name=self._name)) + + # Generate an OIR event if the router is running + if is_running: + + yield from self._hypervisor.send("vm slot_oir_stop '{name}' {slot_id} 0".format(name=self._name, slot_id=slot_id)) + + log.info("router '{name}' [{id}]: OIR stop event sent to slot {slot_id}".format(name=self._name, + id=self._id, + slot_id=slot_id)) + + yield from self._hypervisor.send("vm slot_remove_binding '{name}' {slot_id} 0".format(name=self._name, slot_id=slot_id)) + + log.info("Router '{name}' [{id}]: adapter {adapter} removed from slot {slot_id}".format(name=self._name, + id=self._id, + adapter=adapter, + slot_id=slot_id)) + self._slots[slot_id] = None + + @asyncio.coroutine + def install_wic(self, wic_slot_id, wic): + """ + Installs a WIC adapter into this router. + + :param wic_slot_id: WIC slot ID + :param wic: WIC to be installed + """ + + # WICs are always installed on adapters in slot 0 + slot_id = 0 + + # Do not check if slot has an adapter because adapters with WICs interfaces + # must be inserted by default in the router and cannot be removed. + adapter = self._slots[slot_id] + + if wic_slot_id > len(adapter.wics) - 1: + raise DynamipsError("WIC slot {wic_slot_id} doesn't exist".format(wic_slot_id=wic_slot_id)) + + if not adapter.wic_slot_available(wic_slot_id): + raise DynamipsError("WIC slot {wic_slot_id} is already occupied by another WIC".format(wic_slot_id=wic_slot_id)) + + # Dynamips WICs slot IDs start on a multiple of 16 + # WIC1 = 16, WIC2 = 32 and WIC3 = 48 + internal_wic_slot_id = 16 * (wic_slot_id + 1) + yield from self._hypervisor.send("vm slot_add_binding '{name}' {slot_id} {wic_slot_id} {wic}".format(name=self._name, + slot_id=slot_id, + wic_slot_id=internal_wic_slot_id, + wic=wic)) + + log.info("Router '{name}' [{id}]: {wic} inserted into WIC slot {wic_slot_id}".format(name=self._name, + id=self._id, + wic=wic, + wic_slot_id=wic_slot_id)) + + adapter.install_wic(wic_slot_id, wic) + + @asyncio.coroutine + def uninstall_wic(self, wic_slot_id): + """ + Uninstalls a WIC adapter from this router. + + :param wic_slot_id: WIC slot ID + """ + + # WICs are always installed on adapters in slot 0 + slot_id = 0 + + # Do not check if slot has an adapter because adapters with WICs interfaces + # must be inserted by default in the router and cannot be removed. + adapter = self._slots[slot_id] + + if wic_slot_id > len(adapter.wics) - 1: + raise DynamipsError("WIC slot {wic_slot_id} doesn't exist".format(wic_slot_id=wic_slot_id)) + + if adapter.wic_slot_available(wic_slot_id): + raise DynamipsError("No WIC is installed in WIC slot {wic_slot_id}".format(wic_slot_id=wic_slot_id)) + + # Dynamips WICs slot IDs start on a multiple of 16 + # WIC1 = 16, WIC2 = 32 and WIC3 = 48 + internal_wic_slot_id = 16 * (wic_slot_id + 1) + yield from self._hypervisor.send("vm slot_remove_binding '{name}' {slot_id} {wic_slot_id}".format(name=self._name, + slot_id=slot_id, + wic_slot_id=internal_wic_slot_id)) + + log.info("Router '{name}' [{id}]: {wic} removed from WIC slot {wic_slot_id}".format(name=self._name, + id=self._id, + wic=adapter.wics[wic_slot_id], + wic_slot_id=wic_slot_id)) + adapter.uninstall_wic(wic_slot_id) + + @asyncio.coroutine + def get_slot_nio_bindings(self, slot_id): + """ + Returns slot NIO bindings. + + :param slot_id: slot ID + + :returns: list of NIO bindings + """ + + nio_bindings = yield from self._hypervisor.send("vm slot_nio_bindings '{name}' {slot_id}".format(name=self._name, + slot_id=slot_id)) + return nio_bindings + + @asyncio.coroutine + def slot_add_nio_binding(self, slot_id, port_id, nio): + """ + Adds a slot NIO binding. + + :param slot_id: slot ID + :param port_id: port ID + :param nio: NIO instance to add to the slot/port + """ + + try: + adapter = self._slots[slot_id] + except IndexError: + raise DynamipsError("Slot {slot_id} doesn't exist on router '{name}'".format(name=self._name, + slot_id=slot_id)) + if not adapter.port_exists(port_id): + raise DynamipsError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, + port_id=port_id)) + + yield from self._hypervisor.send("vm slot_add_nio_binding '{name}' {slot_id} {port_id} {nio}".format(name=self._name, + slot_id=slot_id, + port_id=port_id, + nio=nio)) + + log.info("Router '{name}' [{id}]: NIO {nio_name} bound to port {slot_id}/{port_id}".format(name=self._name, + id=self._id, + nio_name=nio.name, + slot_id=slot_id, + port_id=port_id)) + + yield from self.slot_enable_nio(slot_id, port_id) + adapter.add_nio(port_id, nio) + + @asyncio.coroutine + def slot_remove_nio_binding(self, slot_id, port_id): + """ + Removes a slot NIO binding. + + :param slot_id: slot ID + :param port_id: port ID + + :returns: removed NIO instance + """ + + try: + adapter = self._slots[slot_id] + except IndexError: + raise DynamipsError("Slot {slot_id} doesn't exist on router '{name}'".format(name=self._name, slot_id=slot_id)) + if not adapter.port_exists(port_id): + raise DynamipsError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, port_id=port_id)) + + yield from self.slot_disable_nio(slot_id, port_id) + yield from self._hypervisor.send("vm slot_remove_nio_binding '{name}' {slot_id} {port_id}".format(name=self._name, + slot_id=slot_id, + port_id=port_id)) + + nio = adapter.get_nio(port_id) + adapter.remove_nio(port_id) + + log.info("Router '{name}' [{id}]: NIO {nio_name} removed from port {slot_id}/{port_id}".format(name=self._name, + id=self._id, + nio_name=nio.name, + slot_id=slot_id, + port_id=port_id)) + + return nio + + @asyncio.coroutine + def slot_enable_nio(self, slot_id, port_id): + """ + Enables a slot NIO binding. + + :param slot_id: slot ID + :param port_id: port ID + """ + + is_running = yield from self.is_running() + if is_running: # running router + yield from self._hypervisor.send("vm slot_enable_nio '{name}' {slot_id} {port_id}".format(name=self._name, + slot_id=slot_id, + port_id=port_id)) + + log.info("Router '{name}' [{id}]: NIO enabled on port {slot_id}/{port_id}".format(name=self._name, + id=self._id, + slot_id=slot_id, + port_id=port_id)) + @asyncio.coroutine + def slot_disable_nio(self, slot_id, port_id): + """ + Disables a slot NIO binding. + + :param slot_id: slot ID + :param port_id: port ID + """ + + is_running = yield from self.is_running() + if is_running: # running router + yield from self._hypervisor.send("vm slot_disable_nio '{name}' {slot_id} {port_id}".format(name=self._name, + slot_id=slot_id, + port_id=port_id)) + + log.info("Router '{name}' [{id}]: NIO disabled on port {slot_id}/{port_id}".format(name=self._name, + id=self._id, + slot_id=slot_id, + port_id=port_id)) + + @asyncio.coroutine + def start_capture(self, slot_id, port_id, output_file, data_link_type="DLT_EN10MB"): + """ + Starts a packet capture. + + :param slot_id: slot ID + :param port_id: port ID + :param output_file: PCAP destination file for the capture + :param data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB + """ + + try: + adapter = self._slots[slot_id] + except IndexError: + raise DynamipsError("Slot {slot_id} doesn't exist on router '{name}'".format(name=self._name, slot_id=slot_id)) + if not adapter.port_exists(port_id): + raise DynamipsError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, port_id=port_id)) + + data_link_type = data_link_type.lower() + if data_link_type.startswith("dlt_"): + data_link_type = data_link_type[4:] + + nio = adapter.get_nio(port_id) + + if nio.input_filter[0] is not None and nio.output_filter[0] is not None: + raise DynamipsError("Port {port_id} has already a filter applied on {adapter}".format(adapter=adapter, + port_id=port_id)) + + + # FIXME: capture + #try: + # os.makedirs(os.path.dirname(output_file), exist_ok=True) + #except OSError as e: + # raise DynamipsError("Could not create captures directory {}".format(e)) + + yield from nio.bind_filter("both", "capture") + yield from nio.setup_filter("both", '{} "{}"'.format(data_link_type, output_file)) + + log.info("Router '{name}' [{id}]: starting packet capture on port {slot_id}/{port_id}".format(name=self._name, + id=self._id, + nio_name=nio.name, + slot_id=slot_id, + port_id=port_id)) + @asyncio.coroutine + def stop_capture(self, slot_id, port_id): + """ + Stops a packet capture. + + :param slot_id: slot ID + :param port_id: port ID + """ + + try: + adapter = self._slots[slot_id] + except IndexError: + raise DynamipsError("Slot {slot_id} doesn't exist on router '{name}'".format(name=self._name, slot_id=slot_id)) + if not adapter.port_exists(port_id): + raise DynamipsError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, port_id=port_id)) + + nio = adapter.get_nio(port_id) + yield from nio.unbind_filter("both") + + log.info("Router '{name}' [{id}]: stopping packet capture on port {slot_id}/{port_id}".format(name=self._name, + id=self._id, + nio_name=nio.name, + slot_id=slot_id, + port_id=port_id)) + + def _create_slots(self, numslots): + """ + Creates the appropriate number of slots for this router. + + :param numslots: number of slots to create + """ + + self._slots = numslots * [None] + + @property + def slots(self): + """ + Returns the slots for this router. + + :return: slot list + """ + + return self._slots + + @property + def startup_config(self): + """ + Returns the startup-config for this router. + + :returns: path to startup-config file + """ + + return self._startup_config + + @startup_config.setter + def startup_config(self, startup_config): + """ + Sets the startup-config for this router. + + :param startup_config: path to startup-config file + """ + + self._startup_config = startup_config + + @property + def private_config(self): + """ + Returns the private-config for this router. + + :returns: path to private-config file + """ + + return self._private_config + + @private_config.setter + def private_config(self, private_config): + """ + Sets the private-config for this router. + + :param private_config: path to private-config file + """ + + self._private_config = private_config + + # def rename(self, new_name): + # """ + # Renames this router. + # + # :param new_name: new name string + # """ + # + # if self._startup_config: + # # change the hostname in the startup-config + # startup_config_path = os.path.join(self.hypervisor.working_dir, "configs", "i{}_startup-config.cfg".format(self.id)) + # if os.path.isfile(startup_config_path): + # try: + # with open(startup_config_path, "r+", errors="replace") as f: + # old_config = f.read() + # new_config = old_config.replace(self.name, new_name) + # f.seek(0) + # f.write(new_config) + # except OSError as e: + # raise DynamipsError("Could not amend the configuration {}: {}".format(startup_config_path, e)) + # + # if self._private_config: + # # change the hostname in the private-config + # private_config_path = os.path.join(self.hypervisor.working_dir, "configs", "i{}_private-config.cfg".format(self.id)) + # if os.path.isfile(private_config_path): + # try: + # with open(private_config_path, "r+", errors="replace") as f: + # old_config = f.read() + # new_config = old_config.replace(self.name, new_name) + # f.seek(0) + # f.write(new_config) + # except OSError as e: + # raise DynamipsError("Could not amend the configuration {}: {}".format(private_config_path, e)) + # + # new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces + # self._hypervisor.send("vm rename {name} {new_name}".format(name=self._name, + # new_name=new_name)) + # + # log.info("router {name} [id={id}]: renamed to {new_name}".format(name=self._name, + # id=self._id, + # new_name=new_name)) + # self._name = new_name + + # def set_config(self, startup_config, private_config=''): + # """ + # Sets the config files that are pushed to startup-config and + # private-config in NVRAM when the instance is started. + # + # :param startup_config: path to statup-config file + # :param private_config: path to private-config file + # (keep existing data when if an empty string) + # """ + # + # if self._startup_config != startup_config or self._private_config != private_config: + # + # self._hypervisor.send("vm set_config {name} {startup} {private}".format(name=self._name, + # startup='"' + startup_config + '"', + # private='"' + private_config + '"')) + # + # log.info("router {name} [id={id}]: has a startup-config set: {startup}".format(name=self._name, + # id=self._id, + # startup='"' + startup_config + '"')) + # + # self._startup_config = startup_config + # + # if private_config: + # log.info("router {name} [id={id}]: has a private-config set: {private}".format(name=self._name, + # id=self._id, + # private='"' + private_config + '"')) + # + # self._private_config = private_config + # + # def extract_config(self): + # """ + # Gets the contents of the config files + # startup-config and private-config from NVRAM. + # + # :returns: tuple (startup-config, private-config) base64 encoded + # """ + # + # try: + # reply = self._hypervisor.send("vm extract_config {}".format(self._name))[0].rsplit(' ', 2)[-2:] + # except IOError: + # #for some reason Dynamips gets frozen when it does not find the magic number in the NVRAM file. + # return None, None + # startup_config = reply[0][1:-1] # get statup-config and remove single quotes + # private_config = reply[1][1:-1] # get private-config and remove single quotes + # return startup_config, private_config + # + # def push_config(self, startup_config, private_config='(keep)'): + # """ + # Pushes configuration to the config files startup-config and private-config in NVRAM. + # The data is a Base64 encoded string, or '(keep)' to keep existing data. + # + # :param startup_config: statup-config string base64 encoded + # :param private_config: private-config string base64 encoded + # (keep existing data when if the value is ('keep')) + # """ + # + # self._hypervisor.send("vm push_config {name} {startup} {private}".format(name=self._name, + # startup=startup_config, + # private=private_config)) + # + # log.info("router {name} [id={id}]: new startup-config pushed".format(name=self._name, + # id=self._id)) + # + # if private_config != '(keep)': + # log.info("router {name} [id={id}]: new private-config pushed".format(name=self._name, + # id=self._id)) + # + # def save_configs(self): + # """ + # Saves the startup-config and private-config to files. + # """ + # + # if self.startup_config or self.private_config: + # startup_config_base64, private_config_base64 = self.extract_config() + # if startup_config_base64: + # try: + # config = base64.decodebytes(startup_config_base64.encode("utf-8")).decode("utf-8") + # config = "!\n" + config.replace("\r", "") + # config_path = os.path.join(self.hypervisor.working_dir, self.startup_config) + # with open(config_path, "w") as f: + # log.info("saving startup-config to {}".format(self.startup_config)) + # f.write(config) + # except OSError as e: + # raise DynamipsError("Could not save the startup configuration {}: {}".format(config_path, e)) + # + # if private_config_base64: + # try: + # config = base64.decodebytes(private_config_base64.encode("utf-8")).decode("utf-8") + # config = "!\n" + config.replace("\r", "") + # config_path = os.path.join(self.hypervisor.working_dir, self.private_config) + # with open(config_path, "w") as f: + # log.info("saving private-config to {}".format(self.private_config)) + # f.write(config) + # except OSError as e: + # raise DynamipsError("Could not save the private configuration {}: {}".format(config_path, e)) + + # def clean_delete(self): + # """ + # Deletes this router & associated files (nvram, disks etc.) + # """ + # + # self._hypervisor.send("vm clean_delete {}".format(self._name)) + # self._hypervisor.devices.remove(self) + # + # if self._startup_config: + # # delete the startup-config + # startup_config_path = os.path.join(self.hypervisor.working_dir, "configs", "{}.cfg".format(self.name)) + # if os.path.isfile(startup_config_path): + # os.remove(startup_config_path) + # + # if self._private_config: + # # delete the private-config + # private_config_path = os.path.join(self.hypervisor.working_dir, "configs", "{}-private.cfg".format(self.name)) + # if os.path.isfile(private_config_path): + # os.remove(private_config_path) + # + # log.info("router {name} [id={id}] has been deleted (including associated files)".format(name=self._name, id=self._id)) diff --git a/gns3server/modules/port_manager.py b/gns3server/modules/port_manager.py index 80e94087..da448cd6 100644 --- a/gns3server/modules/port_manager.py +++ b/gns3server/modules/port_manager.py @@ -185,7 +185,7 @@ class PortManager: """ if port in self._used_tcp_ports: - raise HTTPConflict(text="TCP port already {} in use on host".format(port, self._console_host)) + raise HTTPConflict(text="TCP port {} already in use on host".format(port, self._console_host)) self._used_tcp_ports.add(port) return port @@ -221,7 +221,7 @@ class PortManager: """ if port in self._used_udp_ports: - raise HTTPConflict(text="UDP port already {} in use on host".format(port, self._console_host)) + raise HTTPConflict(text="UDP port {} already in use on host".format(port, self._console_host)) self._used_udp_ports.add(port) def release_udp_port(self, port): diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index 036bfedc..9791fde0 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -254,10 +254,10 @@ class VPCSVM(BaseVM): if self.is_running(): self._terminate_process() try: - yield from asyncio.wait_for(self._process.wait(), timeout=10) + yield from asyncio.wait_for(self._process.wait(), timeout=3) except asyncio.TimeoutError: self._process.kill() - if self._process.poll() is None: + if self._process.returncode is None: log.warn("VPCS process {} is still running".format(self._process.pid)) self._process = None diff --git a/gns3server/old_modules/dynamips/__init__.py b/gns3server/old_modules/dynamips/__init__.py deleted file mode 100644 index 26347094..00000000 --- a/gns3server/old_modules/dynamips/__init__.py +++ /dev/null @@ -1,578 +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 . - -""" -Dynamips server module. -""" - -import sys -import os -import base64 -import tempfile -import shutil -import glob -import socket -from gns3server.modules import IModule -from gns3server.config import Config -from gns3server.builtins.interfaces import get_windows_interfaces - -from .hypervisor import Hypervisor -from .hypervisor_manager import HypervisorManager -from .dynamips_error import DynamipsError - -# Nodes -from .nodes.router import Router -from .nodes.c1700 import C1700 -from .nodes.c2600 import C2600 -from .nodes.c2691 import C2691 -from .nodes.c3600 import C3600 -from .nodes.c3725 import C3725 -from .nodes.c3745 import C3745 -from .nodes.c7200 import C7200 -from .nodes.bridge import Bridge -from .nodes.ethernet_switch import EthernetSwitch -from .nodes.atm_switch import ATMSwitch -from .nodes.atm_bridge import ATMBridge -from .nodes.frame_relay_switch import FrameRelaySwitch -from .nodes.hub import Hub - -# Adapters -from .adapters.c7200_io_2fe import C7200_IO_2FE -from .adapters.c7200_io_fe import C7200_IO_FE -from .adapters.c7200_io_ge_e import C7200_IO_GE_E -from .adapters.nm_16esw import NM_16ESW -from .adapters.nm_1e import NM_1E -from .adapters.nm_1fe_tx import NM_1FE_TX -from .adapters.nm_4e import NM_4E -from .adapters.nm_4t import NM_4T -from .adapters.pa_2fe_tx import PA_2FE_TX -from .adapters.pa_4e import PA_4E -from .adapters.pa_4t import PA_4T -from .adapters.pa_8e import PA_8E -from .adapters.pa_8t import PA_8T -from .adapters.pa_a1 import PA_A1 -from .adapters.pa_fe_tx import PA_FE_TX -from .adapters.pa_ge import PA_GE -from .adapters.pa_pos_oc3 import PA_POS_OC3 -from .adapters.wic_1t import WIC_1T -from .adapters.wic_2t import WIC_2T -from .adapters.wic_1enet import WIC_1ENET - -# NIOs -from .nios.nio_udp import NIO_UDP -from .nios.nio_udp_auto import NIO_UDP_auto -from .nios.nio_unix import NIO_UNIX -from .nios.nio_vde import NIO_VDE -from .nios.nio_tap import NIO_TAP -from .nios.nio_generic_ethernet import NIO_GenericEthernet -from .nios.nio_linux_ethernet import NIO_LinuxEthernet -from .nios.nio_fifo import NIO_FIFO -from .nios.nio_mcast import NIO_Mcast -from .nios.nio_null import NIO_Null - -from .backends import vm -from .backends import ethsw -from .backends import ethhub -from .backends import frsw -from .backends import atmsw - -import logging -log = logging.getLogger(__name__) - - -class Dynamips(IModule): - - """ - Dynamips 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 Dynamips location - config = Config.instance() - dynamips_config = config.get_section_config(name.upper()) - self._dynamips = dynamips_config.get("dynamips_path") - if not self._dynamips or not os.path.isfile(self._dynamips): - paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep) - # look for Dynamips in the current working directory and $PATH - for path in paths: - try: - if "dynamips" in os.listdir(path) and os.access(os.path.join(path, "dynamips"), os.X_OK): - self._dynamips = os.path.join(path, "dynamips") - break - except OSError: - continue - - if not self._dynamips: - log.warning("dynamips binary couldn't be found!") - elif not os.access(self._dynamips, os.X_OK): - log.warning("dynamips is not executable") - - IModule.__init__(self, name, *args, **kwargs) - self._hypervisor_manager = None - self._hypervisor_manager_settings = {} - self._routers = {} - self._ethernet_switches = {} - self._frame_relay_switches = {} - self._atm_switches = {} - self._ethernet_hubs = {} - self._projects_dir = kwargs["projects_dir"] - self._tempdir = kwargs["temp_dir"] - self._working_dir = self._projects_dir - self._host = dynamips_config.get("host", kwargs["host"]) - self._console_host = dynamips_config.get("console_host", kwargs["console_host"]) - - if not sys.platform.startswith("win32"): - # FIXME: pickle issues Windows - self._callback = self.add_periodic_callback(self._check_hypervisors, 5000) - self._callback.start() - - def stop(self, signum=None): - """ - Properly stops the module. - - :param signum: signal number (if called by the signal handler) - """ - - if not sys.platform.startswith("win32"): - self._callback.stop() - - # automatically save configs for all router instances - for router_id in self._routers: - router = self._routers[router_id] - try: - router.save_configs() - except DynamipsError: - continue - - # stop all Dynamips hypervisors - if self._hypervisor_manager: - self._hypervisor_manager.stop_all_hypervisors() - - self.delete_dynamips_files() - IModule.stop(self, signum) # this will stop the I/O loop - - def _check_hypervisors(self): - """ - Periodic callback to check if Dynamips hypervisors are running. - - Sends a notification to the client if not. - """ - - if self._hypervisor_manager: - for hypervisor in self._hypervisor_manager.hypervisors: - if hypervisor.started and not hypervisor.is_running(): - notification = {"module": self.name} - stdout = hypervisor.read_stdout() - device_names = [] - for device in hypervisor.devices: - device_names.append(device.name) - notification["message"] = "Dynamips has stopped running" - notification["details"] = stdout - notification["devices"] = device_names - self.send_notification("{}.dynamips_stopped".format(self.name), notification) - hypervisor.stop() - - def get_device_instance(self, device_id, instance_dict): - """ - Returns a device instance. - - :param device_id: device identifier - :param instance_dict: dictionary containing the instances - - :returns: device instance - """ - - if device_id not in instance_dict: - log.debug("device ID {} doesn't exist".format(device_id), exc_info=1) - self.send_custom_error("Device ID {} doesn't exist".format(device_id)) - return None - return instance_dict[device_id] - - def delete_dynamips_files(self): - """ - Deletes useless Dynamips files from the working directory - """ - - files = glob.glob(os.path.join(self._working_dir, "dynamips", "*.ghost")) - files += glob.glob(os.path.join(self._working_dir, "dynamips", "*_lock")) - files += glob.glob(os.path.join(self._working_dir, "dynamips", "ilt_*")) - files += glob.glob(os.path.join(self._working_dir, "dynamips", "c[0-9][0-9][0-9][0-9]_*_rommon_vars")) - files += glob.glob(os.path.join(self._working_dir, "dynamips", "c[0-9][0-9][0-9][0-9]_*_ssa")) - for file in files: - try: - log.debug("deleting file {}".format(file)) - os.remove(file) - except OSError as e: - log.warn("could not delete file {}: {}".format(file, e)) - continue - - @IModule.route("dynamips.reset") - def reset(self, request=None): - """ - Resets the module (JSON-RPC notification). - - :param request: JSON request (not used) - """ - - # automatically save configs for all router instances - for router_id in self._routers: - router = self._routers[router_id] - try: - router.save_configs() - except DynamipsError: - continue - - # stop all Dynamips hypervisors - if self._hypervisor_manager: - self._hypervisor_manager.stop_all_hypervisors() - - # resets the instance counters - Router.reset() - EthernetSwitch.reset() - Hub.reset() - FrameRelaySwitch.reset() - ATMSwitch.reset() - NIO_UDP.reset() - NIO_UDP_auto.reset() - NIO_UNIX.reset() - NIO_VDE.reset() - NIO_TAP.reset() - NIO_GenericEthernet.reset() - NIO_LinuxEthernet.reset() - NIO_FIFO.reset() - NIO_Mcast.reset() - NIO_Null.reset() - - self._routers.clear() - self._ethernet_switches.clear() - self._frame_relay_switches.clear() - self._atm_switches.clear() - - self.delete_dynamips_files() - - self._hypervisor_manager = None - self._working_dir = self._projects_dir - log.info("dynamips module has been reset") - - def start_hypervisor_manager(self): - """ - Starts the hypervisor manager. - """ - - # check if Dynamips path exists - if not os.path.isfile(self._dynamips): - raise DynamipsError("Dynamips executable {} doesn't exist".format(self._dynamips)) - - # check if Dynamips is executable - if not os.access(self._dynamips, os.X_OK): - raise DynamipsError("Dynamips {} is not executable".format(self._dynamips)) - - workdir = os.path.join(self._working_dir, "dynamips") - try: - os.makedirs(workdir) - except FileExistsError: - pass - except OSError as e: - raise DynamipsError("Could not create working directory {}".format(e)) - - # check if the working directory is writable - if not os.access(workdir, os.W_OK): - raise DynamipsError("Cannot write to working directory {}".format(workdir)) - - log.info("starting the hypervisor manager with Dynamips working directory set to '{}'".format(workdir)) - self._hypervisor_manager = HypervisorManager(self._dynamips, workdir, self._host, self._console_host) - - for name, value in self._hypervisor_manager_settings.items(): - if hasattr(self._hypervisor_manager, name) and getattr(self._hypervisor_manager, name) != value: - setattr(self._hypervisor_manager, name, value) - - @IModule.route("dynamips.settings") - def settings(self, request): - """ - Set or update settings. - - Optional request parameters: - - path (path to the Dynamips executable) - - working_dir (path to a working directory) - - project_name - - :param request: JSON request - """ - - if request is None: - self.send_param_error() - return - - log.debug("received request {}".format(request)) - - # TODO: JSON schema validation - if not self._hypervisor_manager: - - if "path" in request: - self._dynamips = request.pop("path") - - if "working_dir" in request: - self._working_dir = request.pop("working_dir") - log.info("this server is local") - else: - self._working_dir = os.path.join(self._projects_dir, request["project_name"]) - log.info("this server is remote with working directory path to {}".format(self._working_dir)) - - self._hypervisor_manager_settings = request - - else: - if "project_name" in request: - # for remote server - new_working_dir = os.path.join(self._projects_dir, request["project_name"]) - - if self._projects_dir != self._working_dir != new_working_dir: - - # trick to avoid file locks by Dynamips on Windows - if sys.platform.startswith("win"): - self._hypervisor_manager.working_dir = tempfile.gettempdir() - - if not os.path.isdir(new_working_dir): - try: - self.delete_dynamips_files() - 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 - - elif "working_dir" in request: - # for local server - new_working_dir = request.pop("working_dir") - - try: - self._hypervisor_manager.working_dir = new_working_dir - except DynamipsError as e: - log.error("could not change working directory: {}".format(e)) - return - - self._working_dir = new_working_dir - - # apply settings to the hypervisor manager - for name, value in request.items(): - if hasattr(self._hypervisor_manager, name) and getattr(self._hypervisor_manager, name) != value: - setattr(self._hypervisor_manager, name, value) - - @IModule.route("dynamips.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) - - def create_nio(self, node, request): - """ - Creates a new NIO. - - :param node: node requesting the NIO - :param request: the original request with the - necessary information to create the NIO - - :returns: a NIO object - """ - - 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 DynamipsError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e)) - # check if we have an allocated NIO UDP auto - nio = node.hypervisor.get_nio_udp_auto(lport) - if not nio: - # otherwise create an NIO UDP - nio = NIO_UDP(node.hypervisor, lport, rhost, rport) - else: - nio.connect(rhost, rport) - elif request["nio"]["type"] == "nio_generic_ethernet": - ethernet_device = request["nio"]["ethernet_device"] - if sys.platform.startswith("win"): - # replace the interface name by the GUID on Windows - interfaces = get_windows_interfaces() - npf_interface = None - for interface in interfaces: - if interface["name"] == ethernet_device: - npf_interface = interface["id"] - if not npf_interface: - raise DynamipsError("Could not find interface {} on this host".format(ethernet_device)) - else: - ethernet_device = npf_interface - nio = NIO_GenericEthernet(node.hypervisor, ethernet_device) - elif request["nio"]["type"] == "nio_linux_ethernet": - if sys.platform.startswith("win"): - raise DynamipsError("This NIO type is not supported on Windows") - ethernet_device = request["nio"]["ethernet_device"] - nio = NIO_LinuxEthernet(node.hypervisor, ethernet_device) - elif request["nio"]["type"] == "nio_tap": - tap_device = request["nio"]["tap_device"] - nio = NIO_TAP(node.hypervisor, tap_device) - elif request["nio"]["type"] == "nio_unix": - local_file = request["nio"]["local_file"] - remote_file = request["nio"]["remote_file"] - nio = NIO_UNIX(node.hypervisor, local_file, remote_file) - elif request["nio"]["type"] == "nio_vde": - control_file = request["nio"]["control_file"] - local_file = request["nio"]["local_file"] - nio = NIO_VDE(node.hypervisor, control_file, local_file) - elif request["nio"]["type"] == "nio_null": - nio = NIO_Null(node.hypervisor) - return nio - - def allocate_udp_port(self, node): - """ - Allocates a UDP port in order to create an UDP NIO. - - :param node: the node that needs to allocate an UDP port - - :returns: dictionary with the allocated host/port info - """ - - port = node.hypervisor.allocate_udp_port() - host = node.hypervisor.host - - log.info("{} [id={}] has allocated UDP port {} with host {}".format(node.name, - node.id, - port, - host)) - response = {"lport": port} - return response - - def set_ghost_ios(self, router): - """ - Manages Ghost IOS support. - - :param router: Router instance - """ - - if not router.mmap: - raise DynamipsError("mmap support is required to enable ghost IOS support") - - ghost_instance = router.formatted_ghost_file() - all_ghosts = [] - - # search of an existing ghost instance across all hypervisors - for hypervisor in self._hypervisor_manager.hypervisors: - all_ghosts.extend(hypervisor.ghosts) - - if ghost_instance not in all_ghosts: - # create a new ghost IOS instance - ghost = Router(router.hypervisor, "ghost-" + ghost_instance, router.platform, ghost_flag=True) - ghost.image = router.image - # for 7200s, the NPE must be set when using an NPE-G2. - if router.platform == "c7200": - ghost.npe = router.npe - ghost.ghost_status = 1 - ghost.ghost_file = ghost_instance - ghost.ram = router.ram - try: - ghost.start() - ghost.stop() - except DynamipsError: - raise - finally: - ghost.clean_delete() - - if router.ghost_file != ghost_instance: - # set the ghost file to the router - router.ghost_status = 2 - router.ghost_file = ghost_instance - - def create_config_from_file(self, local_base_config, router, destination_config_path): - """ - Creates a config file from a local base config - - :param local_base_config: path the a local base config - :param router: router instance - :param destination_config_path: path to the destination config file - - :returns: relative path to the created config file - """ - - log.info("creating config file {} from {}".format(destination_config_path, local_base_config)) - config_path = destination_config_path - config_dir = os.path.dirname(destination_config_path) - try: - os.makedirs(config_dir) - except FileExistsError: - pass - except OSError as e: - raise DynamipsError("Could not create configs directory: {}".format(e)) - - try: - with open(local_base_config, "r", errors="replace") as f: - config = f.read() - with open(config_path, "w") as f: - config = "!\n" + config.replace("\r", "") - config = config.replace('%h', router.name) - f.write(config) - except OSError as e: - raise DynamipsError("Could not save the configuration from {} to {}: {}".format(local_base_config, config_path, e)) - return "configs" + os.sep + os.path.basename(config_path) - - def create_config_from_base64(self, config_base64, router, destination_config_path): - """ - Creates a config file from a base64 encoded config. - - :param config_base64: base64 encoded config - :param router: router instance - :param destination_config_path: path to the destination config file - - :returns: relative path to the created config file - """ - - log.info("creating config file {} from base64".format(destination_config_path)) - config = base64.decodebytes(config_base64.encode("utf-8")).decode("utf-8") - config = "!\n" + config.replace("\r", "") - config = config.replace('%h', router.name) - config_dir = os.path.dirname(destination_config_path) - try: - os.makedirs(config_dir) - except FileExistsError: - pass - except OSError as e: - raise DynamipsError("Could not create configs directory: {}".format(e)) - - config_path = destination_config_path - try: - with open(config_path, "w") as f: - log.info("saving startup-config to {}".format(config_path)) - f.write(config) - except OSError as e: - raise DynamipsError("Could not save the configuration {}: {}".format(config_path, e)) - return "configs" + os.sep + os.path.basename(config_path) diff --git a/gns3server/old_modules/dynamips/backends/atmsw.py b/gns3server/old_modules/dynamips/backends/atmsw.py deleted file mode 100644 index 2ce0410b..00000000 --- a/gns3server/old_modules/dynamips/backends/atmsw.py +++ /dev/null @@ -1,395 +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 . - -import re -import os -from gns3server.modules import IModule -from ..nodes.atm_switch import ATMSwitch -from ..dynamips_error import DynamipsError - -from ..schemas.atmsw import ATMSW_CREATE_SCHEMA -from ..schemas.atmsw import ATMSW_DELETE_SCHEMA -from ..schemas.atmsw import ATMSW_UPDATE_SCHEMA -from ..schemas.atmsw import ATMSW_ALLOCATE_UDP_PORT_SCHEMA -from ..schemas.atmsw import ATMSW_ADD_NIO_SCHEMA -from ..schemas.atmsw import ATMSW_DELETE_NIO_SCHEMA -from ..schemas.atmsw import ATMSW_START_CAPTURE_SCHEMA -from ..schemas.atmsw import ATMSW_STOP_CAPTURE_SCHEMA - -import logging -log = logging.getLogger(__name__) - - -class ATMSW(object): - - @IModule.route("dynamips.atmsw.create") - def atmsw_create(self, request): - """ - Creates a new ATM switch. - - Mandatory request parameters: - - name (switch name) - - Response parameters: - - id (switch identifier) - - name (switch name) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, ATMSW_CREATE_SCHEMA): - return - - name = request["name"] - try: - if not self._hypervisor_manager: - self.start_hypervisor_manager() - - hypervisor = self._hypervisor_manager.allocate_hypervisor_for_simulated_device() - atmsw = ATMSwitch(hypervisor, name) - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - response = {"name": atmsw.name, - "id": atmsw.id} - - self._atm_switches[atmsw.id] = atmsw - self.send_response(response) - - @IModule.route("dynamips.atmsw.delete") - def atmsw_delete(self, request): - """ - Deletes a ATM switch. - - Mandatory request parameters: - - id (switch identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, ATMSW_DELETE_SCHEMA): - return - - # get the ATM switch instance - atmsw_id = request["id"] - atmsw = self.get_device_instance(atmsw_id, self._atm_switches) - if not atmsw: - return - - try: - atmsw.delete() - self._hypervisor_manager.unallocate_hypervisor_for_simulated_device(atmsw) - del self._atm_switches[atmsw_id] - except DynamipsError as e: - self.send_custom_error(str(e)) - return - self.send_response(True) - - @IModule.route("dynamips.atmsw.update") - def atmsw_update(self, request): - """ - Updates a ATM switch. - - Mandatory request parameters: - - id (switch identifier) - - Optional request parameters: - - name (new switch name) - - Response parameters: - - name if changed - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, ATMSW_UPDATE_SCHEMA): - return - - # get the ATM switch instance - atmsw = self.get_device_instance(request["id"], self._atm_switches) - if not atmsw: - return - - response = {} - # rename the switch if requested - if "name" in request and atmsw.name != request["name"]: - try: - atmsw.name = request["name"] - response["name"] = atmsw.name - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - self.send_response(response) - - @IModule.route("dynamips.atmsw.allocate_udp_port") - def atmsw_allocate_udp_port(self, request): - """ - Allocates a UDP port in order to create an UDP NIO for an - ATM switch. - - Mandatory request parameters: - - id (switch identifier) - - port_id (port identifier) - - Response parameters: - - port_id (port identifier) - - lport (allocated local port) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, ATMSW_ALLOCATE_UDP_PORT_SCHEMA): - return - - # get the ATM switch instance - atmsw = self.get_device_instance(request["id"], self._atm_switches) - if not atmsw: - return - - try: - # allocate a new UDP port - response = self.allocate_udp_port(atmsw) - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - response["port_id"] = request["port_id"] - self.send_response(response) - - @IModule.route("dynamips.atmsw.add_nio") - def atmsw_add_nio(self, request): - """ - Adds an NIO (Network Input/Output) for an ATM switch. - - Mandatory request parameters: - - id (switch identifier) - - port (port identifier) - - port_id (port identifier) - - mappings (VCs/VPs mapped to the port) - - nio (one of the following) - - type "nio_udp" - - lport (local port) - - rhost (remote host) - - rport (remote port) - - type "nio_generic_ethernet" - - ethernet_device (Ethernet device name e.g. eth0) - - type "nio_linux_ethernet" - - ethernet_device (Ethernet device name e.g. eth0) - - type "nio_tap" - - tap_device (TAP device name e.g. tap0) - - type "nio_unix" - - local_file (path to UNIX socket file) - - remote_file (path to UNIX socket file) - - type "nio_vde" - - control_file (path to VDE control file) - - local_file (path to VDE local file) - - type "nio_null" - - Response parameters: - - port_id (unique port identifier) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, ATMSW_ADD_NIO_SCHEMA): - return - - # get the ATM switch instance - atmsw = self.get_device_instance(request["id"], self._atm_switches) - if not atmsw: - return - - port = request["port"] - mappings = request["mappings"] - - try: - nio = self.create_nio(atmsw, request) - if not nio: - raise DynamipsError("Requested NIO doesn't exist: {}".format(request["nio"])) - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - try: - atmsw.add_nio(nio, port) - pvc_entry = re.compile(r"""^([0-9]*):([0-9]*):([0-9]*)$""") - for source, destination in mappings.items(): - match_source_pvc = pvc_entry.search(source) - match_destination_pvc = pvc_entry.search(destination) - if match_source_pvc and match_destination_pvc: - # add the virtual channels mapped with this port/nio - source_port, source_vpi, source_vci = map(int, match_source_pvc.group(1, 2, 3)) - destination_port, destination_vpi, destination_vci = map(int, match_destination_pvc.group(1, 2, 3)) - if atmsw.has_port(destination_port): - if (source_port, source_vpi, source_vci) not in atmsw.mapping and \ - (destination_port, destination_vpi, destination_vci) not in atmsw.mapping: - atmsw.map_pvc(source_port, source_vpi, source_vci, destination_port, destination_vpi, destination_vci) - atmsw.map_pvc(destination_port, destination_vpi, destination_vci, source_port, source_vpi, source_vci) - else: - # add the virtual paths mapped with this port/nio - source_port, source_vpi = map(int, source.split(':')) - destination_port, destination_vpi = map(int, destination.split(':')) - if atmsw.has_port(destination_port): - if (source_port, source_vpi) not in atmsw.mapping and (destination_port, destination_vpi) not in atmsw.mapping: - atmsw.map_vp(source_port, source_vpi, destination_port, destination_vpi) - atmsw.map_vp(destination_port, destination_vpi, source_port, source_vpi) - - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - self.send_response({"port_id": request["port_id"]}) - - @IModule.route("dynamips.atmsw.delete_nio") - def atmsw_delete_nio(self, request): - """ - Deletes an NIO (Network Input/Output). - - Mandatory request parameters: - - id (switch identifier) - - port (port identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, ATMSW_DELETE_NIO_SCHEMA): - return - - # get the ATM switch instance - atmsw = self.get_device_instance(request["id"], self._atm_switches) - if not atmsw: - return - - port = request["port"] - try: - for source, destination in atmsw.mapping.copy().items(): - if len(source) == 3 and len(destination) == 3: - # remove the virtual channels mapped with this port/nio - source_port, source_vpi, source_vci = source - destination_port, destination_vpi, destination_vci = destination - if port == source_port: - atmsw.unmap_pvc(source_port, source_vpi, source_vci, destination_port, destination_vpi, destination_vci) - atmsw.unmap_pvc(destination_port, destination_vpi, destination_vci, source_port, source_vpi, source_vci) - else: - # remove the virtual paths mapped with this port/nio - source_port, source_vpi = source - destination_port, destination_vpi = destination - if port == source_port: - atmsw.unmap_vp(source_port, source_vpi, destination_port, destination_vpi) - atmsw.unmap_vp(destination_port, destination_vpi, source_port, source_vpi) - - nio = atmsw.remove_nio(port) - nio.delete() - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - self.send_response(True) - - @IModule.route("dynamips.atmsw.start_capture") - def atmsw_start_capture(self, request): - """ - Starts a packet capture. - - Mandatory request parameters: - - id (vm identifier) - - port (port identifier) - - port_id (port identifier) - - capture_file_name - - Optional request parameters: - - data_link_type (PCAP DLT_* value) - - Response parameters: - - port_id (port identifier) - - capture_file_path (path to the capture file) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, ATMSW_START_CAPTURE_SCHEMA): - return - - # get the ATM switch instance - atmsw = self.get_device_instance(request["id"], self._atm_switches) - if not atmsw: - return - - port = request["port"] - capture_file_name = request["capture_file_name"] - data_link_type = request.get("data_link_type") - - try: - capture_file_path = os.path.join(atmsw.hypervisor.working_dir, "captures", capture_file_name) - atmsw.start_capture(port, capture_file_path, data_link_type) - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - response = {"port_id": request["port_id"], - "capture_file_path": capture_file_path} - self.send_response(response) - - @IModule.route("dynamips.atmsw.stop_capture") - def atmsw_stop_capture(self, request): - """ - Stops a packet capture. - - Mandatory request parameters: - - id (vm identifier) - - port_id (port identifier) - - port (port number) - - Response parameters: - - port_id (port identifier) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, ATMSW_STOP_CAPTURE_SCHEMA): - return - - # get the ATM switch instance - atmsw = self.get_device_instance(request["id"], self._atm_switches) - if not atmsw: - return - - port = request["port"] - try: - atmsw.stop_capture(port) - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - response = {"port_id": request["port_id"]} - self.send_response(response) diff --git a/gns3server/old_modules/dynamips/backends/ethhub.py b/gns3server/old_modules/dynamips/backends/ethhub.py deleted file mode 100644 index 97c9df7f..00000000 --- a/gns3server/old_modules/dynamips/backends/ethhub.py +++ /dev/null @@ -1,353 +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 . - -import os -from gns3server.modules import IModule -from ..nodes.hub import Hub -from ..dynamips_error import DynamipsError - -from ..schemas.ethhub import ETHHUB_CREATE_SCHEMA -from ..schemas.ethhub import ETHHUB_DELETE_SCHEMA -from ..schemas.ethhub import ETHHUB_UPDATE_SCHEMA -from ..schemas.ethhub import ETHHUB_ALLOCATE_UDP_PORT_SCHEMA -from ..schemas.ethhub import ETHHUB_ADD_NIO_SCHEMA -from ..schemas.ethhub import ETHHUB_DELETE_NIO_SCHEMA -from ..schemas.ethhub import ETHHUB_START_CAPTURE_SCHEMA -from ..schemas.ethhub import ETHHUB_STOP_CAPTURE_SCHEMA - -import logging -log = logging.getLogger(__name__) - - -class ETHHUB(object): - - @IModule.route("dynamips.ethhub.create") - def ethhub_create(self, request): - """ - Creates a new Ethernet hub. - - Mandatory request parameters: - - name (hub name) - - Response parameters: - - id (hub identifier) - - name (hub name) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, ETHHUB_CREATE_SCHEMA): - return - - name = request["name"] - try: - if not self._hypervisor_manager: - self.start_hypervisor_manager() - - hypervisor = self._hypervisor_manager.allocate_hypervisor_for_simulated_device() - ethhub = Hub(hypervisor, name) - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - response = {"name": ethhub.name, - "id": ethhub.id} - - self._ethernet_hubs[ethhub.id] = ethhub - self.send_response(response) - - @IModule.route("dynamips.ethhub.delete") - def ethhub_delete(self, request): - """ - Deletes a Ethernet hub. - - Mandatory request parameters: - - id (hub identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, ETHHUB_DELETE_SCHEMA): - return - - # get the Ethernet hub instance - ethhub_id = request["id"] - ethhub = self.get_device_instance(ethhub_id, self._ethernet_hubs) - if not ethhub: - return - - try: - ethhub.delete() - self._hypervisor_manager.unallocate_hypervisor_for_simulated_device(ethhub) - del self._ethernet_hubs[ethhub_id] - except DynamipsError as e: - self.send_custom_error(str(e)) - return - self.send_response(request) - - @IModule.route("dynamips.ethhub.update") - def ethhub_update(self, request): - """ - Updates a Ethernet hub. - - Mandatory request parameters: - - id (hub identifier) - - Optional request parameters: - - name (new hub name) - - Response parameters: - - name if changed - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, ETHHUB_UPDATE_SCHEMA): - return - - # get the Ethernet hub instance - ethhub = self.get_device_instance(request["id"], self._ethernet_hubs) - if not ethhub: - return - - response = {} - # rename the hub if requested - if "name" in request and ethhub.name != request["name"]: - try: - ethhub.name = request["name"] - response["name"] = ethhub.name - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - self.send_response(request) - - @IModule.route("dynamips.ethhub.allocate_udp_port") - def ethhub_allocate_udp_port(self, request): - """ - Allocates a UDP port in order to create an UDP NIO for an - Ethernet hub. - - Mandatory request parameters: - - id (hub identifier) - - port_id (port identifier) - - Response parameters: - - port_id (port identifier) - - lport (allocated local port) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, ETHHUB_ALLOCATE_UDP_PORT_SCHEMA): - return - - # get the Ethernet hub instance - ethhub = self.get_device_instance(request["id"], self._ethernet_hubs) - if not ethhub: - return - - try: - # allocate a new UDP port - response = self.allocate_udp_port(ethhub) - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - response["port_id"] = request["port_id"] - self.send_response(response) - - @IModule.route("dynamips.ethhub.add_nio") - def ethhub_add_nio(self, request): - """ - Adds an NIO (Network Input/Output) for an Ethernet hub. - - Mandatory request parameters: - - id (hub identifier) - - port (port identifier) - - port_id (port identifier) - - nio (one of the following) - - type "nio_udp" - - lport (local port) - - rhost (remote host) - - rport (remote port) - - type "nio_generic_ethernet" - - ethernet_device (Ethernet device name e.g. eth0) - - type "nio_linux_ethernet" - - ethernet_device (Ethernet device name e.g. eth0) - - type "nio_tap" - - tap_device (TAP device name e.g. tap0) - - type "nio_unix" - - local_file (path to UNIX socket file) - - remote_file (path to UNIX socket file) - - type "nio_vde" - - control_file (path to VDE control file) - - local_file (path to VDE local file) - - type "nio_null" - - Response parameters: - - port_id (unique port identifier) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, ETHHUB_ADD_NIO_SCHEMA): - return - - # get the Ethernet hub instance - ethhub = self.get_device_instance(request["id"], self._ethernet_hubs) - if not ethhub: - return - - port = request["port"] - try: - nio = self.create_nio(ethhub, request) - if not nio: - raise DynamipsError("Requested NIO doesn't exist: {}".format(request["nio"])) - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - try: - ethhub.add_nio(nio, port) - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - self.send_response({"port_id": request["port_id"]}) - - @IModule.route("dynamips.ethhub.delete_nio") - def ethhub_delete_nio(self, request): - """ - Deletes an NIO (Network Input/Output). - - Mandatory request parameters: - - id (hub identifier) - - port (port identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, ETHHUB_DELETE_NIO_SCHEMA): - return - - # get the Ethernet hub instance - ethhub = self.get_device_instance(request["id"], self._ethernet_hubs) - if not ethhub: - return - - port = request["port"] - try: - nio = ethhub.remove_nio(port) - nio.delete() - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - self.send_response(True) - - @IModule.route("dynamips.ethhub.start_capture") - def ethhub_start_capture(self, request): - """ - Starts a packet capture. - - Mandatory request parameters: - - id (vm identifier) - - port (port identifier) - - port_id (port identifier) - - capture_file_name - - Optional request parameters: - - data_link_type (PCAP DLT_* value) - - Response parameters: - - port_id (port identifier) - - capture_file_path (path to the capture file) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, ETHHUB_START_CAPTURE_SCHEMA): - return - - # get the Ethernet hub instance - ethhub = self.get_device_instance(request["id"], self._ethernet_hubs) - if not ethhub: - return - - port = request["port"] - capture_file_name = request["capture_file_name"] - data_link_type = request.get("data_link_type") - - try: - capture_file_path = os.path.join(ethhub.hypervisor.working_dir, "captures", capture_file_name) - ethhub.start_capture(port, capture_file_path, data_link_type) - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - response = {"port_id": request["port_id"], - "capture_file_path": capture_file_path} - self.send_response(response) - - @IModule.route("dynamips.ethhub.stop_capture") - def ethhub_stop_capture(self, request): - """ - Stops a packet capture. - - Mandatory request parameters: - - id (vm identifier) - - port_id (port identifier) - - port (port number) - - Response parameters: - - port_id (port identifier) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, ETHHUB_STOP_CAPTURE_SCHEMA): - return - - # get the Ethernet hub instance - ethhub = self.get_device_instance(request["id"], self._ethernet_hubs) - if not ethhub: - return - - port = request["port"] - try: - ethhub.stop_capture(port) - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - response = {"port_id": request["port_id"]} - self.send_response(response) diff --git a/gns3server/old_modules/dynamips/backends/ethsw.py b/gns3server/old_modules/dynamips/backends/ethsw.py deleted file mode 100644 index e251e158..00000000 --- a/gns3server/old_modules/dynamips/backends/ethsw.py +++ /dev/null @@ -1,382 +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 . - -import os -from gns3server.modules import IModule -from ..nodes.ethernet_switch import EthernetSwitch -from ..dynamips_error import DynamipsError - -from ..schemas.ethsw import ETHSW_CREATE_SCHEMA -from ..schemas.ethsw import ETHSW_DELETE_SCHEMA -from ..schemas.ethsw import ETHSW_UPDATE_SCHEMA -from ..schemas.ethsw import ETHSW_ALLOCATE_UDP_PORT_SCHEMA -from ..schemas.ethsw import ETHSW_ADD_NIO_SCHEMA -from ..schemas.ethsw import ETHSW_DELETE_NIO_SCHEMA -from ..schemas.ethsw import ETHSW_START_CAPTURE_SCHEMA -from ..schemas.ethsw import ETHSW_STOP_CAPTURE_SCHEMA - -import logging -log = logging.getLogger(__name__) - - -class ETHSW(object): - - @IModule.route("dynamips.ethsw.create") - def ethsw_create(self, request): - """ - Creates a new Ethernet switch. - - Mandatory request parameters: - - name (switch name) - - Response parameters: - - id (switch identifier) - - name (switch name) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, ETHSW_CREATE_SCHEMA): - return - - name = request["name"] - try: - if not self._hypervisor_manager: - self.start_hypervisor_manager() - - hypervisor = self._hypervisor_manager.allocate_hypervisor_for_simulated_device() - ethsw = EthernetSwitch(hypervisor, name) - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - response = {"name": ethsw.name, - "id": ethsw.id} - - self._ethernet_switches[ethsw.id] = ethsw - self.send_response(response) - - @IModule.route("dynamips.ethsw.delete") - def ethsw_delete(self, request): - """ - Deletes a Ethernet switch. - - Mandatory request parameters: - - id (switch identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, ETHSW_DELETE_SCHEMA): - return - - # get the Ethernet switch instance - ethsw_id = request["id"] - ethsw = self.get_device_instance(ethsw_id, self._ethernet_switches) - if not ethsw: - return - - try: - ethsw.delete() - self._hypervisor_manager.unallocate_hypervisor_for_simulated_device(ethsw) - del self._ethernet_switches[ethsw_id] - except DynamipsError as e: - self.send_custom_error(str(e)) - return - self.send_response(True) - - @IModule.route("dynamips.ethsw.update") - def ethsw_update(self, request): - """ - Updates a Ethernet switch. - - Mandatory request parameters: - - id (switch identifier) - - Optional request parameters: - - name (new switch name) - - ports (ports settings) - - Response parameters: - - name if changed - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, ETHSW_UPDATE_SCHEMA): - return - - # get the Ethernet switch instance - ethsw = self.get_device_instance(request["id"], self._ethernet_switches) - if not ethsw: - return - - if "ports" in request: - ports = request["ports"] - - # update the port settings - for port, info in ports.items(): - vlan = info["vlan"] - port_type = info["type"] - try: - if port_type == "access": - ethsw.set_access_port(int(port), vlan) - elif port_type == "dot1q": - ethsw.set_dot1q_port(int(port), vlan) - elif port_type == "qinq": - ethsw.set_qinq_port(int(port), vlan) - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - response = {} - # rename the switch if requested - if "name" in request and ethsw.name != request["name"]: - try: - ethsw.name = request["name"] - response["name"] = ethsw.name - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - self.send_response(response) - - @IModule.route("dynamips.ethsw.allocate_udp_port") - def ethsw_allocate_udp_port(self, request): - """ - Allocates a UDP port in order to create an UDP NIO for an - Ethernet switch. - - Mandatory request parameters: - - id (switch identifier) - - port_id (port identifier) - - Response parameters: - - port_id (port identifier) - - lport (allocated local port) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, ETHSW_ALLOCATE_UDP_PORT_SCHEMA): - return - - # get the Ethernet switch instance - ethsw = self.get_device_instance(request["id"], self._ethernet_switches) - if not ethsw: - return - - try: - # allocate a new UDP port - response = self.allocate_udp_port(ethsw) - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - response["port_id"] = request["port_id"] - self.send_response(response) - - @IModule.route("dynamips.ethsw.add_nio") - def ethsw_add_nio(self, request): - """ - Adds an NIO (Network Input/Output) for an Ethernet switch. - - Mandatory request parameters: - - id (switch identifier) - - port (port identifier) - - port_id (port identifier) - - vlan (vlan identifier) - - port_type ("access", "dot1q" or "qinq") - - nio (one of the following) - - type "nio_udp" - - lport (local port) - - rhost (remote host) - - rport (remote port) - - type "nio_generic_ethernet" - - ethernet_device (Ethernet device name e.g. eth0) - - type "nio_linux_ethernet" - - ethernet_device (Ethernet device name e.g. eth0) - - type "nio_tap" - - tap_device (TAP device name e.g. tap0) - - type "nio_unix" - - local_file (path to UNIX socket file) - - remote_file (path to UNIX socket file) - - type "nio_vde" - - control_file (path to VDE control file) - - local_file (path to VDE local file) - - type "nio_null" - - Response parameters: - - port_id (unique port identifier) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, ETHSW_ADD_NIO_SCHEMA): - return - - # get the Ethernet switch instance - ethsw = self.get_device_instance(request["id"], self._ethernet_switches) - if not ethsw: - return - - port = request["port"] - vlan = request["vlan"] - port_type = request["port_type"] - try: - nio = self.create_nio(ethsw, request) - if not nio: - raise DynamipsError("Requested NIO doesn't exist: {}".format(request["nio"])) - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - try: - ethsw.add_nio(nio, port) - if port_type == "access": - ethsw.set_access_port(port, vlan) - elif port_type == "dot1q": - ethsw.set_dot1q_port(port, vlan) - elif port_type == "qinq": - ethsw.set_qinq_port(port, vlan) - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - self.send_response({"port_id": request["port_id"]}) - - @IModule.route("dynamips.ethsw.delete_nio") - def ethsw_delete_nio(self, request): - """ - Deletes an NIO (Network Input/Output). - - Mandatory request parameters: - - id (switch identifier) - - port (port identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, ETHSW_DELETE_NIO_SCHEMA): - return - - # get the Ethernet switch instance - ethsw = self.get_device_instance(request["id"], self._ethernet_switches) - if not ethsw: - return - - port = request["port"] - try: - nio = ethsw.remove_nio(port) - nio.delete() - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - self.send_response(True) - - @IModule.route("dynamips.ethsw.start_capture") - def ethsw_start_capture(self, request): - """ - Starts a packet capture. - - Mandatory request parameters: - - id (vm identifier) - - port (port identifier) - - port_id (port identifier) - - capture_file_name - - Optional request parameters: - - data_link_type (PCAP DLT_* value) - - Response parameters: - - port_id (port identifier) - - capture_file_path (path to the capture file) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, ETHSW_START_CAPTURE_SCHEMA): - return - - # get the Ethernet switch instance - ethsw = self.get_device_instance(request["id"], self._ethernet_switches) - if not ethsw: - return - - port = request["port"] - capture_file_name = request["capture_file_name"] - data_link_type = request.get("data_link_type") - - try: - capture_file_path = os.path.join(ethsw.hypervisor.working_dir, "captures", capture_file_name) - ethsw.start_capture(port, capture_file_path, data_link_type) - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - response = {"port_id": request["port_id"], - "capture_file_path": capture_file_path} - self.send_response(response) - - @IModule.route("dynamips.ethsw.stop_capture") - def ethsw_stop_capture(self, request): - """ - Stops a packet capture. - - Mandatory request parameters: - - id (vm identifier) - - port_id (port identifier) - - port (port number) - - Response parameters: - - port_id (port identifier) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, ETHSW_STOP_CAPTURE_SCHEMA): - return - - # get the Ethernet switch instance - ethsw = self.get_device_instance(request["id"], self._ethernet_switches) - if not ethsw: - return - - port = request["port"] - try: - ethsw.stop_capture(port) - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - response = {"port_id": request["port_id"]} - self.send_response(response) diff --git a/gns3server/old_modules/dynamips/backends/frsw.py b/gns3server/old_modules/dynamips/backends/frsw.py deleted file mode 100644 index ed63f501..00000000 --- a/gns3server/old_modules/dynamips/backends/frsw.py +++ /dev/null @@ -1,374 +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 . - -import os -from gns3server.modules import IModule -from ..nodes.frame_relay_switch import FrameRelaySwitch -from ..dynamips_error import DynamipsError - -from ..schemas.frsw import FRSW_CREATE_SCHEMA -from ..schemas.frsw import FRSW_DELETE_SCHEMA -from ..schemas.frsw import FRSW_UPDATE_SCHEMA -from ..schemas.frsw import FRSW_ALLOCATE_UDP_PORT_SCHEMA -from ..schemas.frsw import FRSW_ADD_NIO_SCHEMA -from ..schemas.frsw import FRSW_DELETE_NIO_SCHEMA -from ..schemas.frsw import FRSW_START_CAPTURE_SCHEMA -from ..schemas.frsw import FRSW_STOP_CAPTURE_SCHEMA - -import logging -log = logging.getLogger(__name__) - - -class FRSW(object): - - @IModule.route("dynamips.frsw.create") - def frsw_create(self, request): - """ - Creates a new Frame-Relay switch. - - Mandatory request parameters: - - name (switch name) - - Response parameters: - - id (switch identifier) - - name (switch name) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, FRSW_CREATE_SCHEMA): - return - - name = request["name"] - try: - if not self._hypervisor_manager: - self.start_hypervisor_manager() - - hypervisor = self._hypervisor_manager.allocate_hypervisor_for_simulated_device() - frsw = FrameRelaySwitch(hypervisor, name) - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - response = {"name": frsw.name, - "id": frsw.id} - - self._frame_relay_switches[frsw.id] = frsw - self.send_response(response) - - @IModule.route("dynamips.frsw.delete") - def frsw_delete(self, request): - """ - Deletes a Frame Relay switch. - - Mandatory request parameters: - - id (switch identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, FRSW_DELETE_SCHEMA): - return - - # get the Frame relay switch instance - frsw_id = request["id"] - frsw = self.get_device_instance(frsw_id, self._frame_relay_switches) - if not frsw: - return - - try: - frsw.delete() - self._hypervisor_manager.unallocate_hypervisor_for_simulated_device(frsw) - del self._frame_relay_switches[frsw_id] - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - self.send_response(True) - - @IModule.route("dynamips.frsw.update") - def frsw_update(self, request): - """ - Updates a Frame Relay switch. - - Mandatory request parameters: - - id (switch identifier) - - Optional request parameters: - - name (new switch name) - - Response parameters: - - name if updated - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, FRSW_UPDATE_SCHEMA): - return - - # get the Frame relay switch instance - frsw = self.get_device_instance(request["id"], self._frame_relay_switches) - if not frsw: - return - - response = {} - # rename the switch if requested - if "name" in request and frsw.name != request["name"]: - try: - frsw.name = request["name"] - response["name"] = frsw.name - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - self.send_response(request) - - @IModule.route("dynamips.frsw.allocate_udp_port") - def frsw_allocate_udp_port(self, request): - """ - Allocates a UDP port in order to create an UDP NIO for an - Frame Relay switch. - - Mandatory request parameters: - - id (switch identifier) - - port_id (port identifier) - - Response parameters: - - port_id (port identifier) - - lport (allocated local port) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, FRSW_ALLOCATE_UDP_PORT_SCHEMA): - return - - # get the Frame relay switch instance - frsw = self.get_device_instance(request["id"], self._frame_relay_switches) - if not frsw: - return - - try: - # allocate a new UDP port - response = self.allocate_udp_port(frsw) - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - response["port_id"] = request["port_id"] - self.send_response(response) - - @IModule.route("dynamips.frsw.add_nio") - def frsw_add_nio(self, request): - """ - Adds an NIO (Network Input/Output) for an Frame-Relay switch. - - Mandatory request parameters: - - id (switch identifier) - - port (port identifier) - - port_id (port identifier) - - mappings (VCs mapped to the port) - - nio (one of the following) - - type "nio_udp" - - lport (local port) - - rhost (remote host) - - rport (remote port) - - type "nio_generic_ethernet" - - ethernet_device (Ethernet device name e.g. eth0) - - type "nio_linux_ethernet" - - ethernet_device (Ethernet device name e.g. eth0) - - type "nio_tap" - - tap_device (TAP device name e.g. tap0) - - type "nio_unix" - - local_file (path to UNIX socket file) - - remote_file (path to UNIX socket file) - - type "nio_vde" - - control_file (path to VDE control file) - - local_file (path to VDE local file) - - type "nio_null" - - Response parameters: - - port_id (unique port identifier) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, FRSW_ADD_NIO_SCHEMA): - return - - # get the Frame relay switch instance - frsw = self.get_device_instance(request["id"], self._frame_relay_switches) - if not frsw: - return - - port = request["port"] - mappings = request["mappings"] - - try: - nio = self.create_nio(frsw, request) - if not nio: - raise DynamipsError("Requested NIO doesn't exist: {}".format(request["nio"])) - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - try: - frsw.add_nio(nio, port) - - # add the VCs mapped with this port/nio - for source, destination in mappings.items(): - source_port, source_dlci = map(int, source.split(':')) - destination_port, destination_dlci = map(int, destination.split(':')) - if frsw.has_port(destination_port): - if (source_port, source_dlci) not in frsw.mapping and (destination_port, destination_dlci) not in frsw.mapping: - frsw.map_vc(source_port, source_dlci, destination_port, destination_dlci) - frsw.map_vc(destination_port, destination_dlci, source_port, source_dlci) - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - self.send_response({"port_id": request["port_id"]}) - - @IModule.route("dynamips.frsw.delete_nio") - def frsw_delete_nio(self, request): - """ - Deletes an NIO (Network Input/Output). - - Mandatory request parameters: - - id (switch identifier) - - port (port identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, FRSW_DELETE_NIO_SCHEMA): - return - - # get the Frame relay switch instance - frsw = self.get_device_instance(request["id"], self._frame_relay_switches) - if not frsw: - return - - port = request["port"] - try: - # remove the VCs mapped with this port/nio - for source, destination in frsw.mapping.copy().items(): - source_port, source_dlci = source - destination_port, destination_dlci = destination - if port == source_port: - frsw.unmap_vc(source_port, source_dlci, destination_port, destination_dlci) - frsw.unmap_vc(destination_port, destination_dlci, source_port, source_dlci) - - nio = frsw.remove_nio(port) - nio.delete() - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - self.send_response(True) - - @IModule.route("dynamips.frsw.start_capture") - def frsw_start_capture(self, request): - """ - Starts a packet capture. - - Mandatory request parameters: - - id (vm identifier) - - port (port identifier) - - port_id (port identifier) - - capture_file_name - - Optional request parameters: - - data_link_type (PCAP DLT_* value) - - Response parameters: - - port_id (port identifier) - - capture_file_path (path to the capture file) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, FRSW_START_CAPTURE_SCHEMA): - return - - # get the Frame relay switch instance - frsw = self.get_device_instance(request["id"], self._frame_relay_switches) - if not frsw: - return - - port = request["port"] - capture_file_name = request["capture_file_name"] - data_link_type = request.get("data_link_type") - - try: - capture_file_path = os.path.join(frsw.hypervisor.working_dir, "captures", capture_file_name) - frsw.start_capture(port, capture_file_path, data_link_type) - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - response = {"port_id": request["port_id"], - "capture_file_path": capture_file_path} - self.send_response(response) - - @IModule.route("dynamips.frsw.stop_capture") - def frsw_stop_capture(self, request): - """ - Stops a packet capture. - - Mandatory request parameters: - - id (vm identifier) - - port_id (port identifier) - - port (port number) - - Response parameters: - - port_id (port identifier) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, FRSW_STOP_CAPTURE_SCHEMA): - return - - # get the Frame relay switch instance - frsw = self.get_device_instance(request["id"], self._frame_relay_switches) - if not frsw: - return - - port = request["port"] - try: - frsw.stop_capture(port) - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - response = {"port_id": request["port_id"]} - self.send_response(response) diff --git a/gns3server/old_modules/dynamips/backends/vm.py b/gns3server/old_modules/dynamips/backends/vm.py deleted file mode 100644 index e40e79d6..00000000 --- a/gns3server/old_modules/dynamips/backends/vm.py +++ /dev/null @@ -1,905 +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 . - -import os -import ntpath -import time -from gns3server.modules import IModule -from gns3dms.cloud.rackspace_ctrl import get_provider -from ..dynamips_error import DynamipsError - -from ..nodes.c1700 import C1700 -from ..nodes.c2600 import C2600 -from ..nodes.c2691 import C2691 -from ..nodes.c3600 import C3600 -from ..nodes.c3725 import C3725 -from ..nodes.c3745 import C3745 -from ..nodes.c7200 import C7200 - -from ..adapters.c7200_io_2fe import C7200_IO_2FE -from ..adapters.c7200_io_fe import C7200_IO_FE -from ..adapters.c7200_io_ge_e import C7200_IO_GE_E -from ..adapters.nm_16esw import NM_16ESW -from ..adapters.nm_1e import NM_1E -from ..adapters.nm_1fe_tx import NM_1FE_TX -from ..adapters.nm_4e import NM_4E -from ..adapters.nm_4t import NM_4T -from ..adapters.pa_2fe_tx import PA_2FE_TX -from ..adapters.pa_4e import PA_4E -from ..adapters.pa_4t import PA_4T -from ..adapters.pa_8e import PA_8E -from ..adapters.pa_8t import PA_8T -from ..adapters.pa_a1 import PA_A1 -from ..adapters.pa_fe_tx import PA_FE_TX -from ..adapters.pa_ge import PA_GE -from ..adapters.pa_pos_oc3 import PA_POS_OC3 -from ..adapters.wic_1enet import WIC_1ENET -from ..adapters.wic_1t import WIC_1T -from ..adapters.wic_2t import WIC_2T - -from ..schemas.vm import VM_CREATE_SCHEMA -from ..schemas.vm import VM_DELETE_SCHEMA -from ..schemas.vm import VM_START_SCHEMA -from ..schemas.vm import VM_STOP_SCHEMA -from ..schemas.vm import VM_SUSPEND_SCHEMA -from ..schemas.vm import VM_RELOAD_SCHEMA -from ..schemas.vm import VM_UPDATE_SCHEMA -from ..schemas.vm import VM_START_CAPTURE_SCHEMA -from ..schemas.vm import VM_STOP_CAPTURE_SCHEMA -from ..schemas.vm import VM_SAVE_CONFIG_SCHEMA -from ..schemas.vm import VM_EXPORT_CONFIG_SCHEMA -from ..schemas.vm import VM_IDLEPCS_SCHEMA -from ..schemas.vm import VM_AUTO_IDLEPC_SCHEMA -from ..schemas.vm import VM_ALLOCATE_UDP_PORT_SCHEMA -from ..schemas.vm import VM_ADD_NIO_SCHEMA -from ..schemas.vm import VM_DELETE_NIO_SCHEMA - -import logging -log = logging.getLogger(__name__) - - -ADAPTER_MATRIX = {"C7200-IO-2FE": C7200_IO_2FE, - "C7200-IO-FE": C7200_IO_FE, - "C7200-IO-GE-E": C7200_IO_GE_E, - "NM-16ESW": NM_16ESW, - "NM-1E": NM_1E, - "NM-1FE-TX": NM_1FE_TX, - "NM-4E": NM_4E, - "NM-4T": NM_4T, - "PA-2FE-TX": PA_2FE_TX, - "PA-4E": PA_4E, - "PA-4T+": PA_4T, - "PA-8E": PA_8E, - "PA-8T": PA_8T, - "PA-A1": PA_A1, - "PA-FE-TX": PA_FE_TX, - "PA-GE": PA_GE, - "PA-POS-OC3": PA_POS_OC3} - -WIC_MATRIX = {"WIC-1ENET": WIC_1ENET, - "WIC-1T": WIC_1T, - "WIC-2T": WIC_2T} - -PLATFORMS = {'c1700': C1700, - 'c2600': C2600, - 'c2691': C2691, - 'c3725': C3725, - 'c3745': C3745, - 'c3600': C3600, - 'c7200': C7200} - - -class VM(object): - - @IModule.route("dynamips.vm.create") - def vm_create(self, request): - """ - Creates a new VM (router). - - Mandatory request parameters: - - name (vm name) - - platform (platform name e.g. c7200) - - image (path to IOS image) - - ram (amount of RAM in MB) - - Optional request parameters: - - console (console port number) - - aux (auxiliary console port number) - - mac_addr (MAC address) - - chassis (router chassis model) - - Response parameters: - - id (vm identifier) - - name (vm name) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VM_CREATE_SCHEMA): - return - - name = request["name"] - platform = request["platform"] - image = request["image"] - ram = request["ram"] - hypervisor = None - chassis = request.get("chassis") - router_id = request.get("router_id") - - # Locate the image - updated_image_path = os.path.join(self.images_directory, image) - if os.path.isfile(updated_image_path): - image = updated_image_path - else: - if not os.path.exists(self.images_directory): - os.mkdir(self.images_directory) - cloud_path = request.get("cloud_path", None) - if cloud_path is not None: - # Download the image from cloud files - _, filename = ntpath.split(image) - src = '{}/{}'.format(cloud_path, filename) - provider = get_provider(self._cloud_settings) - log.debug("Downloading file from {} to {}...".format(src, updated_image_path)) - provider.download_file(src, updated_image_path) - log.debug("Download of {} complete.".format(src)) - image = updated_image_path - - try: - if platform not in PLATFORMS: - raise DynamipsError("Unknown router platform: {}".format(platform)) - - if not self._hypervisor_manager: - self.start_hypervisor_manager() - - hypervisor = self._hypervisor_manager.allocate_hypervisor_for_router(image, ram) - - if chassis: - router = PLATFORMS[platform](hypervisor, name, router_id, chassis=chassis) - elif platform == "c7200" and os.path.basename(image).lower().startswith("c7200p"): - router = PLATFORMS[platform](hypervisor, name, router_id, npe="npe-g2") - else: - router = PLATFORMS[platform](hypervisor, name, router_id) - router.ram = ram - router.image = image - if platform not in ("c1700", "c2600"): - router.sparsemem = self._hypervisor_manager.sparse_memory_support - router.mmap = self._hypervisor_manager.mmap_support - if "console" in request: - router.console = request["console"] - if "aux" in request: - router.aux = request["aux"] - if "mac_addr" in request: - router.mac_addr = request["mac_addr"] - - # JIT sharing support - if self._hypervisor_manager.jit_sharing_support: - jitsharing_groups = hypervisor.jitsharing_groups - ios_image = os.path.basename(image) - if ios_image in jitsharing_groups: - router.jit_sharing_group = jitsharing_groups[ios_image] - else: - new_jit_group = -1 - for jit_group in range(0, 127): - if jit_group not in jitsharing_groups.values(): - new_jit_group = jit_group - break - if new_jit_group == -1: - raise DynamipsError("All JIT groups are allocated!") - router.jit_sharing_group = new_jit_group - - # Ghost IOS support - if self._hypervisor_manager.ghost_ios_support: - self.set_ghost_ios(router) - - except DynamipsError as e: - dynamips_stdout = "" - if hypervisor: - hypervisor.decrease_memory_load(ram) - if hypervisor.memory_load == 0 and not hypervisor.devices: - hypervisor.stop() - self._hypervisor_manager.hypervisors.remove(hypervisor) - dynamips_stdout = hypervisor.read_stdout() - self.send_custom_error(str(e) + dynamips_stdout) - return - - response = {"name": router.name, - "id": router.id} - defaults = router.defaults() - response.update(defaults) - self._routers[router.id] = router - self.send_response(response) - - @IModule.route("dynamips.vm.delete") - def vm_delete(self, request): - """ - Deletes a VM (router). - - Mandatory request parameters: - - id (vm identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VM_DELETE_SCHEMA): - return - - # get the router instance - router_id = request["id"] - router = self.get_device_instance(router_id, self._routers) - if not router: - return - - try: - router.clean_delete() - self._hypervisor_manager.unallocate_hypervisor_for_router(router) - del self._routers[router_id] - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - self.send_response(True) - - @IModule.route("dynamips.vm.start") - def vm_start(self, request): - """ - Starts a VM (router) - - Mandatory request parameters: - - id (vm identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VM_START_SCHEMA): - return - - # get the router instance - router = self.get_device_instance(request["id"], self._routers) - if not router: - return - - try: - router.start() - except DynamipsError as e: - self.send_custom_error(str(e)) - return - self.send_response(True) - - @IModule.route("dynamips.vm.stop") - def vm_stop(self, request): - """ - Stops a VM (router) - - Mandatory request parameters: - - id (vm identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VM_STOP_SCHEMA): - return - - # get the router instance - router = self.get_device_instance(request["id"], self._routers) - if not router: - return - - try: - router.stop() - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - self.send_response(True) - - @IModule.route("dynamips.vm.suspend") - def vm_suspend(self, request): - """ - Suspends a VM (router) - - Mandatory request parameters: - - id (vm identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VM_SUSPEND_SCHEMA): - return - - # get the router instance - router = self.get_device_instance(request["id"], self._routers) - if not router: - return - - try: - router.suspend() - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - self.send_response(True) - - @IModule.route("dynamips.vm.reload") - def vm_reload(self, request): - """ - Reloads a VM (router) - - Mandatory request parameters: - - id (vm identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VM_RELOAD_SCHEMA): - return - - # get the router instance - router = self.get_device_instance(request["id"], self._routers) - if not router: - return - - try: - if router.get_status() != "inactive": - router.stop() - router.start() - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - self.send_response(True) - - @IModule.route("dynamips.vm.update") - def vm_update(self, request): - """ - Updates settings for a VM (router). - - Mandatory request parameters: - - id (vm identifier) - - Optional request parameters: - - any setting to update - - startup_config_base64 (startup-config base64 encoded) - - private_config_base64 (private-config base64 encoded) - - Response parameters: - - updated settings - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VM_UPDATE_SCHEMA): - return - - # get the router instance - router = self.get_device_instance(request["id"], self._routers) - if not router: - return - - response = {} - try: - default_startup_config_path = os.path.join(router.hypervisor.working_dir, "configs", "i{}_startup-config.cfg".format(router.id)) - default_private_config_path = os.path.join(router.hypervisor.working_dir, "configs", "i{}_private-config.cfg".format(router.id)) - - # a new startup-config has been pushed - if "startup_config_base64" in request: - # update the request with the new local startup-config path - request["startup_config"] = self.create_config_from_base64(request["startup_config_base64"], router, default_startup_config_path) - - # a new private-config has been pushed - if "private_config_base64" in request: - # update the request with the new local private-config path - request["private_config"] = self.create_config_from_base64(request["private_config_base64"], router, default_private_config_path) - - if "startup_config" in request: - startup_config_path = request["startup_config"].replace("\\", '/') - if os.path.isfile(startup_config_path) and startup_config_path != default_startup_config_path: - # this is a local file set in the GUI - startup_config_path = self.create_config_from_file(startup_config_path, router, default_startup_config_path) - router.set_config(startup_config_path) - else: - router.set_config(startup_config_path) - response["startup_config"] = startup_config_path - del request["startup_config"] - - if "private_config" in request: - private_config_path = request["private_config"].replace("\\", '/') - if os.path.isfile(private_config_path) and private_config_path != default_private_config_path: - # this is a local file set in the GUI - private_config_path = self.create_config_from_file(private_config_path, router, default_private_config_path) - router.set_config(router.startup_config, private_config_path) - else: - router.set_config(router.startup_config, private_config_path) - response["private_config"] = private_config_path - del request["private_config"] - - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - # update the settings - for name, value in request.items(): - if hasattr(router, name) and getattr(router, name) != value: - try: - setattr(router, name, value) - response[name] = value - except DynamipsError as e: - self.send_custom_error(str(e)) - return - elif name.startswith("slot") and value in ADAPTER_MATRIX: - slot_id = int(name[-1]) - adapter_name = value - adapter = ADAPTER_MATRIX[adapter_name]() - try: - if router.slots[slot_id] and not isinstance(router.slots[slot_id], type(adapter)): - router.slot_remove_binding(slot_id) - router.slot_add_binding(slot_id, adapter) - response[name] = value - except DynamipsError as e: - self.send_custom_error(str(e)) - return - elif name.startswith("slot") and value is None: - slot_id = int(name[-1]) - if router.slots[slot_id]: - try: - router.slot_remove_binding(slot_id) - response[name] = value - except DynamipsError as e: - self.send_custom_error(str(e)) - return - elif name.startswith("wic") and value in WIC_MATRIX: - wic_slot_id = int(name[-1]) - wic_name = value - wic = WIC_MATRIX[wic_name]() - try: - if router.slots[0].wics[wic_slot_id] and not isinstance(router.slots[0].wics[wic_slot_id], type(wic)): - router.uninstall_wic(wic_slot_id) - router.install_wic(wic_slot_id, wic) - response[name] = value - except DynamipsError as e: - self.send_custom_error(str(e)) - return - elif name.startswith("wic") and value is None: - wic_slot_id = int(name[-1]) - if router.slots[0].wics and router.slots[0].wics[wic_slot_id]: - try: - router.uninstall_wic(wic_slot_id) - response[name] = value - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - # Update the ghost IOS file in case the RAM size has changed - if self._hypervisor_manager.ghost_ios_support: - try: - self.set_ghost_ios(router) - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - self.send_response(response) - - @IModule.route("dynamips.vm.start_capture") - def vm_start_capture(self, request): - """ - Starts a packet capture. - - Mandatory request parameters: - - id (vm identifier) - - port_id (port identifier) - - slot (slot number) - - port (port number) - - capture_file_name - - Optional request parameters: - - data_link_type (PCAP DLT_* value) - - Response parameters: - - port_id (port identifier) - - capture_file_path (path to the capture file) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VM_START_CAPTURE_SCHEMA): - return - - # get the router instance - router = self.get_device_instance(request["id"], self._routers) - if not router: - return - - slot = request["slot"] - port = request["port"] - capture_file_name = request["capture_file_name"] - data_link_type = request.get("data_link_type") - - try: - capture_file_path = os.path.join(router.hypervisor.working_dir, "captures", capture_file_name) - router.start_capture(slot, port, capture_file_path, data_link_type) - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - response = {"port_id": request["port_id"], - "capture_file_path": capture_file_path} - self.send_response(response) - - @IModule.route("dynamips.vm.stop_capture") - def vm_stop_capture(self, request): - """ - Stops a packet capture. - - Mandatory request parameters: - - id (vm identifier) - - port_id (port identifier) - - slot (slot number) - - port (port number) - - Response parameters: - - port_id (port identifier) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VM_STOP_CAPTURE_SCHEMA): - return - - # get the router instance - router = self.get_device_instance(request["id"], self._routers) - if not router: - return - - slot = request["slot"] - port = request["port"] - try: - router.stop_capture(slot, port) - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - response = {"port_id": request["port_id"]} - self.send_response(response) - - @IModule.route("dynamips.vm.save_config") - def vm_save_config(self, request): - """ - Save the configs for a VM (router). - - Mandatory request parameters: - - id (vm identifier) - """ - - # validate the request - if not self.validate_request(request, VM_SAVE_CONFIG_SCHEMA): - return - - # get the router instance - router = self.get_device_instance(request["id"], self._routers) - if not router: - return - - try: - router.save_configs() - except DynamipsError as e: - log.warn("could not save config to {}: {}".format(router.startup_config, e)) - - @IModule.route("dynamips.vm.export_config") - def vm_export_config(self, request): - """ - Export the config from a router - - Mandatory request parameters: - - id (vm identifier) - - Response parameters: - - startup_config_base64 (startup-config base64 encoded) - - private_config_base64 (private-config base64 encoded) - - False if no configuration can be extracted - """ - - # validate the request - if not self.validate_request(request, VM_EXPORT_CONFIG_SCHEMA): - return - - # get the router instance - router = self.get_device_instance(request["id"], self._routers) - if not router: - return - - response = {} - try: - startup_config_base64, private_config_base64 = router.extract_config() - if startup_config_base64: - response["startup_config_base64"] = startup_config_base64 - if private_config_base64: - response["private_config_base64"] = private_config_base64 - except DynamipsError: - self.send_custom_error("unable to extract configs from the NVRAM") - return - - if not response: - self.send_response(False) - else: - self.send_response(response) - - @IModule.route("dynamips.vm.idlepcs") - def vm_idlepcs(self, request): - """ - Get Idle-PC proposals. - - Mandatory request parameters: - - id (vm identifier) - - Optional request parameters: - - compute (returns previously compute Idle-PC values if False) - - Response parameters: - - id (vm identifier) - - idlepcs (Idle-PC values in an array) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VM_IDLEPCS_SCHEMA): - return - - # get the router instance - router = self.get_device_instance(request["id"], self._routers) - if not router: - return - - try: - if "compute" in request and request["compute"] == False: - idlepcs = router.show_idle_pc_prop() - else: - # reset the current Idle-PC value before calculating a new one - router.idlepc = "0x0" - idlepcs = router.get_idle_pc_prop() - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - response = {"id": router.id, - "idlepcs": idlepcs} - self.send_response(response) - - @IModule.route("dynamips.vm.auto_idlepc") - def vm_auto_idlepc(self, request): - """ - Auto Idle-PC calculation. - - Mandatory request parameters: - - id (vm identifier) - - Response parameters: - - id (vm identifier) - - logs (logs for the calculation) - - idlepc (Idle-PC value) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VM_AUTO_IDLEPC_SCHEMA): - return - - # get the router instance - router = self.get_device_instance(request["id"], self._routers) - if not router: - return - - try: - router.idlepc = "0x0" # reset the current Idle-PC value before calculating a new one - was_auto_started = False - if router.get_status() != "running": - router.start() - was_auto_started = True - time.sleep(20) # leave time to the router to boot - - logs = [] - validated_idlepc = "0x0" - idlepcs = router.get_idle_pc_prop() - if not idlepcs: - logs.append("No Idle-PC values found") - - for idlepc in idlepcs: - router.idlepc = idlepc.split()[0] - logs.append("Trying Idle-PC value {}".format(router.idlepc)) - start_time = time.time() - initial_cpu_usage = router.get_cpu_usage() - logs.append("Initial CPU usage = {}%".format(initial_cpu_usage)) - time.sleep(4) # wait 4 seconds to probe the cpu again - elapsed_time = time.time() - start_time - cpu_elapsed_usage = router.get_cpu_usage() - initial_cpu_usage - cpu_usage = abs(cpu_elapsed_usage * 100.0 / elapsed_time) - logs.append("CPU usage after {:.2} seconds = {:.2}%".format(elapsed_time, cpu_usage)) - if cpu_usage > 100: - cpu_usage = 100 - if cpu_usage < 70: - validated_idlepc = router.idlepc - logs.append("Idle-PC value {} has been validated".format(validated_idlepc)) - break - except DynamipsError as e: - self.send_custom_error(str(e)) - return - finally: - if was_auto_started: - router.stop() - - response = {"id": router.id, - "logs": logs, - "idlepc": validated_idlepc} - - self.send_response(response) - - @IModule.route("dynamips.vm.allocate_udp_port") - def vm_allocate_udp_port(self, request): - """ - Allocates a UDP port in order to create an UDP NIO. - - Mandatory request parameters: - - id (vm 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, VM_ALLOCATE_UDP_PORT_SCHEMA): - return - - # get the router instance - router = self.get_device_instance(request["id"], self._routers) - if not router: - return - - try: - # allocate a new UDP port - response = self.allocate_udp_port(router) - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - response["port_id"] = request["port_id"] - self.send_response(response) - - @IModule.route("dynamips.vm.add_nio") - def vm_add_nio(self, request): - """ - Adds an NIO (Network Input/Output) for a VM (router). - - Mandatory request parameters: - - id (vm identifier) - - slot (slot number) - - 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_generic_ethernet" - - ethernet_device (Ethernet device name e.g. eth0) - - type "nio_linux_ethernet" - - ethernet_device (Ethernet device name e.g. eth0) - - type "nio_tap" - - tap_device (TAP device name e.g. tap0) - - type "nio_unix" - - local_file (path to UNIX socket file) - - remote_file (path to UNIX socket file) - - type "nio_vde" - - control_file (path to VDE control file) - - local_file (path to VDE local file) - - type "nio_null" - - Response parameters: - - port_id (unique port identifier) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VM_ADD_NIO_SCHEMA): - return - - # get the router instance - router = self.get_device_instance(request["id"], self._routers) - if not router: - return - - slot = request["slot"] - port = request["port"] - try: - nio = self.create_nio(router, request) - if not nio: - raise DynamipsError("Requested NIO doesn't exist: {}".format(request["nio"])) - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - try: - router.slot_add_nio_binding(slot, port, nio) - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - self.send_response({"port_id": request["port_id"]}) - - @IModule.route("dynamips.vm.delete_nio") - def vm_delete_nio(self, request): - """ - Deletes an NIO (Network Input/Output). - - Mandatory request parameters: - - id (vm identifier) - - slot (slot identifier) - - port (port identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VM_DELETE_NIO_SCHEMA): - return - - # get the router instance - router = self.get_device_instance(request["id"], self._routers) - if not router: - return - - slot = request["slot"] - port = request["port"] - try: - nio = router.slot_remove_nio_binding(slot, port) - nio.delete() - except DynamipsError as e: - self.send_custom_error(str(e)) - return - - self.send_response(True) diff --git a/gns3server/old_modules/dynamips/hypervisor_manager.py b/gns3server/old_modules/dynamips/hypervisor_manager.py deleted file mode 100644 index a9be6ed0..00000000 --- a/gns3server/old_modules/dynamips/hypervisor_manager.py +++ /dev/null @@ -1,655 +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 . - -""" -Manages Dynamips hypervisors (load-balancing etc.) -""" - -from gns3server.config import Config -from .hypervisor import Hypervisor -from .dynamips_error import DynamipsError -from ..attic import find_unused_port -from ..attic import wait_socket_is_ready -from pkg_resources import parse_version - -import os -import time -import logging - -log = logging.getLogger(__name__) - - -class HypervisorManager(object): - - """ - Manages Dynamips hypervisors. - - :param path: path to the Dynamips executable - :param working_dir: path to a working directory - :param host: host/address for hypervisors to listen to - :param console_host: IP address to bind for console connections - """ - - def __init__(self, path, working_dir, host='127.0.0.1', console_host='0.0.0.0'): - - self._hypervisors = [] - self._path = path - self._working_dir = working_dir - self._console_host = console_host - self._host = console_host # FIXME: Dynamips must be patched to bind on a different address than the one used by the hypervisor. - - config = Config.instance() - dynamips_config = config.get_section_config("DYNAMIPS") - self._hypervisor_start_port_range = dynamips_config.get("hypervisor_start_port_range", 7200) - self._hypervisor_end_port_range = dynamips_config.get("hypervisor_end_port_range", 7700) - self._console_start_port_range = dynamips_config.get("console_start_port_range", 2001) - self._console_end_port_range = dynamips_config.get("console_end_port_range", 2500) - self._aux_start_port_range = dynamips_config.get("aux_start_port_range", 2501) - self._aux_end_port_range = dynamips_config.get("aux_end_port_range", 3000) - self._udp_start_port_range = dynamips_config.get("udp_start_port_range", 10001) - self._udp_end_port_range = dynamips_config.get("udp_end_port_range", 20000) - self._ghost_ios_support = dynamips_config.get("ghost_ios_support", True) - self._mmap_support = dynamips_config.get("mmap_support", True) - self._jit_sharing_support = dynamips_config.get("jit_sharing_support", False) - self._sparse_memory_support = dynamips_config.get("sparse_memory_support", True) - self._allocate_hypervisor_per_device = dynamips_config.get("allocate_hypervisor_per_device", True) - self._memory_usage_limit_per_hypervisor = dynamips_config.get("memory_usage_limit_per_hypervisor", 1024) - self._allocate_hypervisor_per_ios_image = dynamips_config.get("allocate_hypervisor_per_ios_image", True) - - def __del__(self): - """ - Shutdowns all started hypervisors - """ - - self.stop_all_hypervisors() - - @property - def hypervisors(self): - """ - Returns all hypervisor instances. - - :returns: list of hypervisor instances - """ - - return self._hypervisors - - @property - def path(self): - """ - Returns the Dynamips path. - - :returns: path to Dynamips - """ - - return self._path - - @path.setter - def path(self, path): - """ - Set a new Dynamips path. - - :param path: path to Dynamips - """ - - self._path = path - log.info("Dynamips path set to {}".format(self._path)) - - @property - def working_dir(self): - """ - Returns the Dynamips working directory path. - - :returns: path to Dynamips working directory - """ - - return self._working_dir - - @working_dir.setter - def working_dir(self, working_dir): - """ - Sets a new path to the Dynamips working directory. - - :param working_dir: path to Dynamips working directory - """ - - self._working_dir = os.path.join(working_dir, "dynamips") - log.info("working directory set to {}".format(self._working_dir)) - - # update all existing hypervisors with the new working directory - for hypervisor in self._hypervisors: - hypervisor.working_dir = self._working_dir - - @property - def hypervisor_start_port_range(self): - """ - Returns the hypervisor start port range value - - :returns: hypervisor start port range value (integer) - """ - - return self._hypervisor_start_port_range - - @hypervisor_start_port_range.setter - def hypervisor_start_port_range(self, hypervisor_start_port_range): - """ - Sets a new hypervisor start port range value - - :param hypervisor_start_port_range: hypervisor start port range value (integer) - """ - - if self._hypervisor_start_port_range != hypervisor_start_port_range: - self._hypervisor_start_port_range = hypervisor_start_port_range - log.info("hypervisor start port range value set to {}".format(self._hypervisor_start_port_range)) - - @property - def hypervisor_end_port_range(self): - """ - Returns the hypervisor end port range value - - :returns: hypervisor end port range value (integer) - """ - - return self._hypervisor_end_port_range - - @hypervisor_end_port_range.setter - def hypervisor_end_port_range(self, hypervisor_end_port_range): - """ - Sets a new hypervisor end port range value - - :param hypervisor_end_port_range: hypervisor end port range value (integer) - """ - - if self._hypervisor_end_port_range != hypervisor_end_port_range: - self._hypervisor_end_port_range = hypervisor_end_port_range - log.info("hypervisor end port range value set to {}".format(self._hypervisor_end_port_range)) - - @property - def console_start_port_range(self): - """ - Returns the console start port range value - - :returns: console start port range value (integer) - """ - - return self._console_start_port_range - - @console_start_port_range.setter - def console_start_port_range(self, console_start_port_range): - """ - Sets a new console start port range value - - :param console_start_port_range: console start port range value (integer) - """ - - if self._console_start_port_range != console_start_port_range: - self._console_start_port_range = console_start_port_range - log.info("console start port range value set to {}".format(self._console_start_port_range)) - - # update all existing hypervisors with the new value - for hypervisor in self._hypervisors: - hypervisor.console_start_port_range = console_start_port_range - - @property - def console_end_port_range(self): - """ - Returns the console end port range value - - :returns: console end port range value (integer) - """ - - return self._console_end_port_range - - @console_end_port_range.setter - def console_end_port_range(self, console_end_port_range): - """ - Sets a new console end port range value - - :param console_end_port_range: console end port range value (integer) - """ - - if self._console_end_port_range != console_end_port_range: - self._console_end_port_range = console_end_port_range - log.info("console end port range value set to {}".format(self._console_end_port_range)) - - # update all existing hypervisors with the new value - for hypervisor in self._hypervisors: - hypervisor.console_end_port_range = console_end_port_range - - @property - def aux_start_port_range(self): - """ - Returns the auxiliary console start port range value - - :returns: auxiliary console start port range value (integer) - """ - - return self._aux_start_port_range - - @aux_start_port_range.setter - def aux_start_port_range(self, aux_start_port_range): - """ - Sets a new auxiliary console start port range value - - :param aux_start_port_range: auxiliary console start port range value (integer) - """ - - if self._aux_start_port_range != aux_start_port_range: - self._aux_start_port_range = aux_start_port_range - log.info("auxiliary console start port range value set to {}".format(self._aux_start_port_range)) - - # update all existing hypervisors with the new value - for hypervisor in self._hypervisors: - hypervisor.aux_start_port_range = aux_start_port_range - - @property - def aux_end_port_range(self): - """ - Returns the auxiliary console end port range value - - :returns: auxiliary console end port range value (integer) - """ - - return self._aux_end_port_range - - @aux_end_port_range.setter - def aux_end_port_range(self, aux_end_port_range): - """ - Sets a new auxiliary console end port range value - - :param aux_end_port_range: auxiliary console end port range value (integer) - """ - - if self._aux_end_port_range != aux_end_port_range: - self._aux_end_port_range = aux_end_port_range - log.info("auxiliary console end port range value set to {}".format(self._aux_end_port_range)) - - # update all existing hypervisors with the new value - for hypervisor in self._hypervisors: - hypervisor.aux_end_port_range = aux_end_port_range - - @property - def udp_start_port_range(self): - """ - Returns the UDP start port range value - - :returns: UDP start port range value (integer) - """ - - return self._udp_start_port_range - - @udp_start_port_range.setter - def udp_start_port_range(self, udp_start_port_range): - """ - Sets a new UDP start port range value - - :param udp_start_port_range: UDP start port range value (integer) - """ - - if self._udp_start_port_range != udp_start_port_range: - self._udp_start_port_range = udp_start_port_range - log.info("UDP start port range value set to {}".format(self._udp_start_port_range)) - - # update all existing hypervisors with the new value - for hypervisor in self._hypervisors: - hypervisor.udp_start_port_range = udp_start_port_range - - @property - def udp_end_port_range(self): - """ - Returns the UDP end port range value - - :returns: UDP end port range value (integer) - """ - - return self._udp_end_port_range - - @udp_end_port_range.setter - def udp_end_port_range(self, udp_end_port_range): - """ - Sets a new UDP end port range value - - :param udp_end_port_range: UDP end port range value (integer) - """ - - if self._udp_end_port_range != udp_end_port_range: - self._udp_end_port_range = udp_end_port_range - log.info("UDP end port range value set to {}".format(self._udp_end_port_range)) - - # update all existing hypervisors with the new value - for hypervisor in self._hypervisors: - hypervisor.udp_end_port_range = udp_end_port_range - - @property - def ghost_ios_support(self): - """ - Returns either ghost IOS is activated or not. - - :returns: boolean - """ - - return self._ghost_ios_support - - @ghost_ios_support.setter - def ghost_ios_support(self, ghost_ios_support): - """ - Sets ghost IOS support. - - :param ghost_ios_support: boolean - """ - - if self._ghost_ios_support != ghost_ios_support: - self._ghost_ios_support = ghost_ios_support - if ghost_ios_support: - log.info("ghost IOS support enabled") - else: - log.info("ghost IOS support disabled") - - @property - def mmap_support(self): - """ - Returns either mmap is activated or not. - - :returns: boolean - """ - - return self._mmap_support - - @mmap_support.setter - def mmap_support(self, mmap_support): - """ - Sets mmap support. - - :param mmap_support: boolean - """ - - if self._mmap_support != mmap_support: - self._mmap_support = mmap_support - if mmap_support: - log.info("mmap support enabled") - else: - log.info("mmap support disabled") - - @property - def sparse_memory_support(self): - """ - Returns either sparse memory is activated or not. - - :returns: boolean - """ - - return self._sparse_memory_support - - @sparse_memory_support.setter - def sparse_memory_support(self, sparse_memory_support): - """ - Sets sparse memory support. - - :param sparse_memory_support: boolean - """ - - if self._sparse_memory_support != sparse_memory_support: - self._sparse_memory_support = sparse_memory_support - if sparse_memory_support: - log.info("sparse memory support enabled") - else: - log.info("sparse memory support disabled") - - @property - def jit_sharing_support(self): - """ - Returns either JIT sharing is activated or not. - - :returns: boolean - """ - - return self._jit_sharing_support - - @jit_sharing_support.setter - def jit_sharing_support(self, jit_sharing_support): - """ - Sets JIT sharing support. - - :param jit_sharing_support: boolean - """ - - if self._jit_sharing_support != jit_sharing_support: - self._jit_sharing_support = jit_sharing_support - if jit_sharing_support: - log.info("JIT sharing support enabled") - else: - log.info("JIT sharing support disabled") - - @property - def allocate_hypervisor_per_device(self): - """ - Returns either an hypervisor is created for each device. - - :returns: True or False - """ - - return self._allocate_hypervisor_per_device - - @allocate_hypervisor_per_device.setter - def allocate_hypervisor_per_device(self, value): - """ - Sets if an hypervisor is created for each device. - - :param value: True or False - """ - - if self._allocate_hypervisor_per_device != value: - self._allocate_hypervisor_per_device = value - if value: - log.info("allocating an hypervisor per device enabled") - else: - log.info("allocating an hypervisor per device disabled") - - @property - def memory_usage_limit_per_hypervisor(self): - """ - Returns the memory usage limit per hypervisor - - :returns: limit value (integer) - """ - - return self._memory_usage_limit_per_hypervisor - - @memory_usage_limit_per_hypervisor.setter - def memory_usage_limit_per_hypervisor(self, memory_limit): - """ - Sets the memory usage limit per hypervisor - - :param memory_limit: memory limit value (integer) - """ - - if self._memory_usage_limit_per_hypervisor != memory_limit: - self._memory_usage_limit_per_hypervisor = memory_limit - log.info("memory usage limit per hypervisor set to {}".format(memory_limit)) - - @property - def allocate_hypervisor_per_ios_image(self): - """ - Returns if router are grouped per hypervisor - based on their IOS image. - - :returns: True or False - """ - - return self._allocate_hypervisor_per_ios_image - - @allocate_hypervisor_per_ios_image.setter - def allocate_hypervisor_per_ios_image(self, value): - """ - Sets if routers are grouped per hypervisor - based on their IOS image. - - :param value: True or False - """ - - if self._allocate_hypervisor_per_ios_image != value: - self._allocate_hypervisor_per_ios_image = value - if value: - log.info("allocating an hypervisor per IOS image enabled") - else: - log.info("allocating an hypervisor per IOS image disabled") - - def wait_for_hypervisor(self, host, port): - """ - Waits for an hypervisor to be started (accepting a socket connection) - - :param host: host/address to connect to the hypervisor - :param port: port to connect to the hypervisor - """ - - begin = time.time() - # wait for the socket for a maximum of 10 seconds. - connection_success, last_exception = wait_socket_is_ready(host, port, wait=10.0) - - if not connection_success: - # FIXME: throw exception here - log.critical("Couldn't connect to hypervisor on {}:{} :{}".format(host, port, - last_exception)) - else: - log.info("Dynamips server ready after {:.4f} seconds".format(time.time() - begin)) - - def start_new_hypervisor(self): - """ - Creates a new Dynamips process and start it. - - :returns: the new hypervisor instance - """ - - try: - port = find_unused_port(self._hypervisor_start_port_range, self._hypervisor_end_port_range, self._host) - except Exception as e: - raise DynamipsError(e) - - hypervisor = Hypervisor(self._path, - self._working_dir, - self._host, - port) - - log.info("creating new hypervisor {}:{} with working directory {}".format(hypervisor.host, hypervisor.port, self._working_dir)) - hypervisor.start() - - self.wait_for_hypervisor(self._host, port) - log.info("hypervisor {}:{} has successfully started".format(hypervisor.host, hypervisor.port)) - - hypervisor.connect() - if parse_version(hypervisor.version) < parse_version('0.2.11'): - raise DynamipsError("Dynamips version must be >= 0.2.11, detected version is {}".format(hypervisor.version)) - - hypervisor.console_start_port_range = self._console_start_port_range - hypervisor.console_end_port_range = self._console_end_port_range - hypervisor.aux_start_port_range = self._aux_start_port_range - hypervisor.aux_end_port_range = self._aux_end_port_range - hypervisor.udp_start_port_range = self._udp_start_port_range - hypervisor.udp_end_port_range = self._udp_end_port_range - self._hypervisors.append(hypervisor) - return hypervisor - - def allocate_hypervisor_for_router(self, router_ios_image, router_ram): - """ - Allocates a Dynamips hypervisor for a specific router - (new or existing depending on the RAM amount and IOS image) - - :param router_ios_image: IOS image name - :param router_ram: amount of RAM (integer) - - :returns: the allocated hypervisor instance - """ - - # allocate an hypervisor for each router by default - if not self._allocate_hypervisor_per_device: - for hypervisor in self._hypervisors: - if self._allocate_hypervisor_per_ios_image: - if not hypervisor.image_ref: - hypervisor.image_ref = router_ios_image - elif hypervisor.image_ref != router_ios_image: - continue - if (hypervisor.memory_load + router_ram) <= self._memory_usage_limit_per_hypervisor: - current_memory_load = hypervisor.memory_load - hypervisor.increase_memory_load(router_ram) - log.info("allocating existing hypervisor {}:{}, RAM={}+{}".format(hypervisor.host, - hypervisor.port, - current_memory_load, - router_ram)) - return hypervisor - - hypervisor = self.start_new_hypervisor() - hypervisor.image_ref = router_ios_image - hypervisor.increase_memory_load(router_ram) - return hypervisor - - def unallocate_hypervisor_for_router(self, router): - """ - Unallocates a Dynamips hypervisor for a specific router. - - :param router: Router instance - """ - - hypervisor = router.hypervisor - hypervisor.decrease_memory_load(router.ram) - - if hypervisor.memory_load < 0: - log.warn("hypervisor {}:{} has a memory load below 0 ({})".format(hypervisor.host, - hypervisor.port, - hypervisor.memory_load)) - #hypervisor.memory_load = 0 - - # memory load at 0MB and no devices managed anymore... - # let's stop this hypervisor - if hypervisor.memory_load == 0 and not hypervisor.devices: - hypervisor.stop() - self._hypervisors.remove(hypervisor) - - def allocate_hypervisor_for_simulated_device(self): - """ - Allocates a Dynamips hypervisor for a specific Dynamips simulated device. - - :returns: the allocated hypervisor instance - """ - - # For now always allocate the first hypervisor available, - # in the future we could randomly allocate. - - if self._hypervisors: - return self._hypervisors[0] - - # no hypervisor, let's start one! - return self.start_new_hypervisor() - - def unallocate_hypervisor_for_simulated_device(self, device): - """ - Unallocates a Dynamips hypervisor for a specific Dynamips simulated device. - - :param device: device instance - """ - - hypervisor = device.hypervisor - if not hypervisor.devices: - hypervisor.stop() - self._hypervisors.remove(hypervisor) - - def stop_all_hypervisors(self): - """ - Stops all hypervisors. - """ - - for hypervisor in self._hypervisors: - hypervisor.stop() - self._hypervisors = [] diff --git a/gns3server/old_modules/dynamips/nodes/__init__.py b/gns3server/old_modules/dynamips/nodes/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/gns3server/old_modules/dynamips/nodes/atm_bridge.py b/gns3server/old_modules/dynamips/nodes/atm_bridge.py deleted file mode 100644 index bfed0b78..00000000 --- a/gns3server/old_modules/dynamips/nodes/atm_bridge.py +++ /dev/null @@ -1,172 +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 . - -""" -Interface for Dynamips virtual ATM bridge module ("atm_bridge"). -http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L622 -""" - -from ..dynamips_error import DynamipsError - - -class ATMBridge(object): - - """ - Dynamips bridge switch. - - :param hypervisor: Dynamips hypervisor instance - :param name: name for this switch - """ - - def __init__(self, hypervisor, name): - - # FIXME: instance tracking - self._hypervisor = hypervisor - self._name = '"' + name + '"' # put name into quotes to protect spaces - self._hypervisor.send("atm_bridge create {}".format(self._name)) - self._hypervisor.devices.append(self) - self._nios = {} - self._mapping = {} - - @property - def name(self): - """ - Returns the current name of this ATM bridge. - - :returns: ATM bridge name - """ - - return self._name[1:-1] # remove quotes - - @name.setter - def name(self, new_name): - """ - Renames this ATM bridge. - - :param new_name: New name for this bridge - """ - - new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces - self._hypervisor.send("atm_bridge rename {name} {new_name}".format(name=self._name, - new_name=new_name)) - self._name = new_name - - @property - def hypervisor(self): - """ - Returns the current hypervisor. - - :returns: hypervisor instance - """ - - return self._hypervisor - - def list(self): - """ - Returns all ATM bridge instances. - - :returns: list of all ATM bridges - """ - - return self._hypervisor.send("atm_bridge list") - - @property - def nios(self): - """ - Returns all the NIOs member of this ATM bridge. - - :returns: nio list - """ - - return self._nios - - @property - def mapping(self): - """ - Returns port mapping - - :returns: mapping list - """ - - return self._mapping - - def delete(self): - """ - Deletes this ATM bridge. - """ - - self._hypervisor.send("atm_bridge delete {}".format(self._name)) - self._hypervisor.devices.remove(self) - - def add_nio(self, nio, port): - """ - Adds a NIO as new port on ATM bridge. - - :param nio: NIO instance to add - :param port: port to allocate for the NIO - """ - - if port in self._nios: - raise DynamipsError("Port {} isn't free".format(port)) - - self._nios[port] = nio - - def remove_nio(self, port): - """ - Removes the specified NIO as member of this ATM switch. - - :param port: allocated port - """ - - if port not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port)) - - del self._nios[port] - - def configure(self, eth_port, atm_port, atm_vpi, atm_vci): - """ - Configures this ATM bridge. - - :param eth_port: Ethernet port - :param atm_port: ATM port - :param atm_vpi: ATM VPI - :param atm_vci: ATM VCI - """ - - if eth_port not in self._nios: - raise DynamipsError("Ethernet port {} is not allocated".format(eth_port)) - - if atm_port not in self._nios: - raise DynamipsError("ATM port {} is not allocated".format(atm_port)) - - eth_nio = self._nios[eth_port] - atm_nio = self._nios[atm_port] - - self._hypervisor.send("atm_bridge configure {name} {eth_nio} {atm_nio} {vpi} {vci}".format(name=self._name, - eth_nio=eth_nio, - atm_nio=atm_nio, - vpi=atm_vpi, - vci=atm_vci)) - self._mapping[eth_port] = (atm_port, atm_vpi, atm_vci) - - def unconfigure(self): - """ - Unconfigures this ATM bridge. - """ - - self._hypervisor.send("atm_bridge unconfigure {}".format(self._name)) - del self._mapping diff --git a/gns3server/old_modules/dynamips/nodes/atm_switch.py b/gns3server/old_modules/dynamips/nodes/atm_switch.py deleted file mode 100644 index aa0dba3e..00000000 --- a/gns3server/old_modules/dynamips/nodes/atm_switch.py +++ /dev/null @@ -1,406 +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 . - -""" -Interface for Dynamips virtual ATM switch module ("atmsw"). -http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L593 -""" - -import os -from ..dynamips_error import DynamipsError - -import logging -log = logging.getLogger(__name__) - - -class ATMSwitch(object): - - """ - Dynamips ATM switch. - - :param hypervisor: Dynamips hypervisor instance - :param name: name for this switch - """ - - _instances = [] - - def __init__(self, hypervisor, name): - - # find an instance identifier (0 < id <= 4096) - self._id = 0 - for identifier in range(1, 4097): - if identifier not in self._instances: - self._id = identifier - self._instances.append(self._id) - break - - if self._id == 0: - raise DynamipsError("Maximum number of instances reached") - - self._hypervisor = hypervisor - self._name = '"' + name + '"' # put name into quotes to protect spaces - self._hypervisor.send("atmsw create {}".format(self._name)) - - log.info("ATM switch {name} [id={id}] has been created".format(name=self._name, - id=self._id)) - - self._hypervisor.devices.append(self) - self._nios = {} - self._mapping = {} - - @classmethod - def reset(cls): - """ - Resets the instance count and the allocated instances list. - """ - - cls._instances.clear() - - @property - def id(self): - """ - Returns the unique ID for this ATM switch. - - :returns: id (integer) - """ - - return self._id - - @property - def name(self): - """ - Returns the current name of this ATM switch. - - :returns: ATM switch name - """ - - return self._name[1:-1] # remove quotes - - @name.setter - def name(self, new_name): - """ - Renames this ATM switch. - - :param new_name: New name for this switch - """ - - new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces - self._hypervisor.send("atmsw rename {name} {new_name}".format(name=self._name, - new_name=new_name)) - - log.info("ATM switch {name} [id={id}]: renamed to {new_name}".format(name=self._name, - id=self._id, - new_name=new_name)) - - self._name = new_name - - @property - def hypervisor(self): - """ - Returns the current hypervisor. - - :returns: hypervisor instance - """ - - return self._hypervisor - - def list(self): - """ - Returns all ATM switches instances. - - :returns: list of all ATM switches - """ - - return self._hypervisor.send("atmsw list") - - @property - def nios(self): - """ - Returns all the NIOs member of this ATM switch. - - :returns: nio list - """ - - return self._nios - - @property - def mapping(self): - """ - Returns port mapping - - :returns: mapping list - """ - - return self._mapping - - def delete(self): - """ - Deletes this ATM switch. - """ - - self._hypervisor.send("atmsw delete {}".format(self._name)) - - log.info("ATM switch {name} [id={id}] has been deleted".format(name=self._name, - id=self._id)) - self._hypervisor.devices.remove(self) - self._instances.remove(self._id) - - def has_port(self, port): - """ - Checks if a port exists on this ATM switch. - - :returns: boolean - """ - - if port in self._nios: - return True - return False - - def add_nio(self, nio, port): - """ - Adds a NIO as new port on ATM switch. - - :param nio: NIO instance to add - :param port: port to allocate for the NIO - """ - - if port in self._nios: - raise DynamipsError("Port {} isn't free".format(port)) - - log.info("ATM switch {name} [id={id}]: NIO {nio} bound to port {port}".format(name=self._name, - id=self._id, - nio=nio, - port=port)) - - self._nios[port] = nio - - def remove_nio(self, port): - """ - Removes the specified NIO as member of this ATM switch. - - :param port: allocated port - """ - - if port not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port)) - - nio = self._nios[port] - log.info("ATM switch {name} [id={id}]: NIO {nio} removed from port {port}".format(name=self._name, - id=self._id, - nio=nio, - port=port)) - - del self._nios[port] - return nio - - def map_vp(self, port1, vpi1, port2, vpi2): - """ - Creates a new Virtual Path connection. - - :param port1: input port - :param vpi1: input vpi - :param port2: output port - :param vpi2: output vpi - """ - - if port1 not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port1)) - - if port2 not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port2)) - - nio1 = self._nios[port1] - nio2 = self._nios[port2] - - self._hypervisor.send("atmsw create_vpc {name} {input_nio} {input_vpi} {output_nio} {output_vpi}".format(name=self._name, - input_nio=nio1, - input_vpi=vpi1, - output_nio=nio2, - output_vpi=vpi2)) - - log.info("ATM switch {name} [id={id}]: VPC from port {port1} VPI {vpi1} to port {port2} VPI {vpi2} created".format(name=self._name, - id=self._id, - port1=port1, - vpi1=vpi1, - port2=port2, - vpi2=vpi2)) - - self._mapping[(port1, vpi1)] = (port2, vpi2) - - def unmap_vp(self, port1, vpi1, port2, vpi2): - """ - Deletes a new Virtual Path connection. - - :param port1: input port - :param vpi1: input vpi - :param port2: output port - :param vpi2: output vpi - """ - - if port1 not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port1)) - - if port2 not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port2)) - - nio1 = self._nios[port1] - nio2 = self._nios[port2] - - self._hypervisor.send("atmsw delete_vpc {name} {input_nio} {input_vpi} {output_nio} {output_vpi}".format(name=self._name, - input_nio=nio1, - input_vpi=vpi1, - output_nio=nio2, - output_vpi=vpi2)) - - log.info("ATM switch {name} [id={id}]: VPC from port {port1} VPI {vpi1} to port {port2} VPI {vpi2} deleted".format(name=self._name, - id=self._id, - port1=port1, - vpi1=vpi1, - port2=port2, - vpi2=vpi2)) - - del self._mapping[(port1, vpi1)] - - def map_pvc(self, port1, vpi1, vci1, port2, vpi2, vci2): - """ - Creates a new Virtual Channel connection (unidirectional). - - :param port1: input port - :param vpi1: input vpi - :param vci1: input vci - :param port2: output port - :param vpi2: output vpi - :param vci2: output vci - """ - - if port1 not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port1)) - - if port2 not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port2)) - - nio1 = self._nios[port1] - nio2 = self._nios[port2] - - self._hypervisor.send("atmsw create_vcc {name} {input_nio} {input_vpi} {input_vci} {output_nio} {output_vpi} {output_vci}".format(name=self._name, - input_nio=nio1, - input_vpi=vpi1, - input_vci=vci1, - output_nio=nio2, - output_vpi=vpi2, - output_vci=vci2)) - - log.info("ATM switch {name} [id={id}]: VCC from port {port1} VPI {vpi1} VCI {vci1} to port {port2} VPI {vpi2} VCI {vci2} created".format(name=self._name, - id=self._id, - port1=port1, - vpi1=vpi1, - vci1=vci1, - port2=port2, - vpi2=vpi2, - vci2=vci2)) - - self._mapping[(port1, vpi1, vci1)] = (port2, vpi2, vci2) - - def unmap_pvc(self, port1, vpi1, vci1, port2, vpi2, vci2): - """ - Deletes a new Virtual Channel connection (unidirectional). - - :param port1: input port - :param vpi1: input vpi - :param vci1: input vci - :param port2: output port - :param vpi2: output vpi - :param vci2: output vci - """ - - if port1 not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port1)) - - if port2 not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port2)) - - nio1 = self._nios[port1] - nio2 = self._nios[port2] - - self._hypervisor.send("atmsw delete_vcc {name} {input_nio} {input_vpi} {input_vci} {output_nio} {output_vpi} {output_vci}".format(name=self._name, - input_nio=nio1, - input_vpi=vpi1, - input_vci=vci1, - output_nio=nio2, - output_vpi=vpi2, - output_vci=vci2)) - - log.info("ATM switch {name} [id={id}]: VCC from port {port1} VPI {vpi1} VCI {vci1} to port {port2} VPI {vpi2} VCI {vci2} deleted".format(name=self._name, - id=self._id, - port1=port1, - vpi1=vpi1, - vci1=vci1, - port2=port2, - vpi2=vpi2, - vci2=vci2)) - del self._mapping[(port1, vpi1, vci1)] - - def start_capture(self, port, output_file, data_link_type="DLT_ATM_RFC1483"): - """ - Starts a packet capture. - - :param port: allocated port - :param output_file: PCAP destination file for the capture - :param data_link_type: PCAP data link type (DLT_*), default is DLT_ATM_RFC1483 - """ - - if port not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port)) - - nio = self._nios[port] - - data_link_type = data_link_type.lower() - if data_link_type.startswith("dlt_"): - data_link_type = data_link_type[4:] - - if nio.input_filter[0] is not None and nio.output_filter[0] is not None: - raise DynamipsError("Port {} has already a filter applied".format(port)) - - try: - os.makedirs(os.path.dirname(output_file)) - except FileExistsError: - pass - except OSError as e: - raise DynamipsError("Could not create captures directory {}".format(e)) - - nio.bind_filter("both", "capture") - nio.setup_filter("both", "{} {}".format(data_link_type, output_file)) - - log.info("ATM switch {name} [id={id}]: starting packet capture on {port}".format(name=self._name, - id=self._id, - port=port)) - - def stop_capture(self, port): - """ - Stops a packet capture. - - :param port: allocated port - """ - - if port not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port)) - - nio = self._nios[port] - nio.unbind_filter("both") - log.info("ATM switch {name} [id={id}]: stopping packet capture on {port}".format(name=self._name, - id=self._id, - port=port)) diff --git a/gns3server/old_modules/dynamips/nodes/bridge.py b/gns3server/old_modules/dynamips/nodes/bridge.py deleted file mode 100644 index fcac17b9..00000000 --- a/gns3server/old_modules/dynamips/nodes/bridge.py +++ /dev/null @@ -1,122 +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 . - -""" -Interface for Dynamips NIO bridge module ("nio_bridge"). -http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L538 -""" - - -class Bridge(object): - - """ - Dynamips bridge. - - :param hypervisor: Dynamips hypervisor instance - :param name: name for this bridge - """ - - def __init__(self, hypervisor, name): - - self._hypervisor = hypervisor - self._name = '"' + name + '"' # put name into quotes to protect spaces - self._hypervisor.send("nio_bridge create {}".format(self._name)) - self._hypervisor.devices.append(self) - self._nios = [] - - @property - def name(self): - """ - Returns the current name of this bridge. - - :returns: bridge name - """ - - return self._name[1:-1] # remove quotes - - @name.setter - def name(self, new_name): - """ - Renames this bridge. - - :param new_name: New name for this bridge - """ - - new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces - self._hypervisor.send("nio_bridge rename {name} {new_name}".format(name=self._name, - new_name=new_name)) - - self._name = new_name - - @property - def hypervisor(self): - """ - Returns the current hypervisor. - - :returns: hypervisor instance - """ - - return self._hypervisor - - def list(self): - """ - Returns all bridge instances. - - :returns: list of all bridges - """ - - return self._hypervisor.send("nio_bridge list") - - @property - def nios(self): - """ - Returns all the NIOs member of this bridge. - - :returns: nio list - """ - - return self._nios - - def delete(self): - """ - Deletes this bridge. - """ - - self._hypervisor.send("nio_bridge delete {}".format(self._name)) - self._hypervisor.devices.remove(self) - - def add_nio(self, nio): - """ - Adds a NIO as new port on this bridge. - - :param nio: NIO instance to add - """ - - self._hypervisor.send("nio_bridge add_nio {name} {nio}".format(name=self._name, - nio=nio)) - self._nios.append(nio) - - def remove_nio(self, nio): - """ - Removes the specified NIO as member of this bridge. - - :param nio: NIO instance to remove - """ - - self._hypervisor.send("nio_bridge remove_nio {name} {nio}".format(name=self._name, - nio=nio)) - self._nios.remove(nio) diff --git a/gns3server/old_modules/dynamips/nodes/ethernet_switch.py b/gns3server/old_modules/dynamips/nodes/ethernet_switch.py deleted file mode 100644 index a88346dc..00000000 --- a/gns3server/old_modules/dynamips/nodes/ethernet_switch.py +++ /dev/null @@ -1,342 +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 . - -""" -Interface for Dynamips virtual Ethernet switch module ("ethsw"). -http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L558 -""" - -import os -from ..dynamips_error import DynamipsError - -import logging -log = logging.getLogger(__name__) - - -class EthernetSwitch(object): - - """ - Dynamips Ethernet switch. - - :param hypervisor: Dynamips hypervisor instance - :param name: name for this switch - """ - - _instances = [] - - def __init__(self, hypervisor, name): - - # find an instance identifier (0 < id <= 4096) - self._id = 0 - for identifier in range(1, 4097): - if identifier not in self._instances: - self._id = identifier - self._instances.append(self._id) - break - - if self._id == 0: - raise DynamipsError("Maximum number of instances reached") - - self._hypervisor = hypervisor - self._name = '"' + name + '"' # put name into quotes to protect spaces - self._hypervisor.send("ethsw create {}".format(self._name)) - - log.info("Ethernet switch {name} [id={id}] has been created".format(name=self._name, - id=self._id)) - - self._hypervisor.devices.append(self) - self._nios = {} - self._mapping = {} - - @classmethod - def reset(cls): - """ - Resets the instance count and the allocated instances list. - """ - - cls._instances.clear() - - @property - def id(self): - """ - Returns the unique ID for this Ethernet switch. - - :returns: id (integer) - """ - - return self._id - - @property - def name(self): - """ - Returns the current name of this Ethernet switch. - - :returns: Ethernet switch name - """ - - return self._name[1:-1] # remove quotes - - @name.setter - def name(self, new_name): - """ - Renames this Ethernet switch. - - :param new_name: New name for this switch - """ - - new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces - self._hypervisor.send("ethsw rename {name} {new_name}".format(name=self._name, - new_name=new_name)) - - log.info("Ethernet switch {name} [id={id}]: renamed to {new_name}".format(name=self._name, - id=self._id, - new_name=new_name)) - - self._name = new_name - - @property - def hypervisor(self): - """ - Returns the current hypervisor. - - :returns: hypervisor instance - """ - - return self._hypervisor - - def list(self): - """ - Returns all Ethernet switches instances. - - :returns: list of all Ethernet switches - """ - - return self._hypervisor.send("ethsw list") - - @property - def nios(self): - """ - Returns all the NIOs member of this Ethernet switch. - - :returns: nio list - """ - - return self._nios - - @property - def mapping(self): - """ - Returns port mapping - - :returns: mapping list - """ - - return self._mapping - - def delete(self): - """ - Deletes this Ethernet switch. - """ - - self._hypervisor.send("ethsw delete {}".format(self._name)) - - log.info("Ethernet switch {name} [id={id}] has been deleted".format(name=self._name, - id=self._id)) - self._hypervisor.devices.remove(self) - self._instances.remove(self._id) - - def add_nio(self, nio, port): - """ - Adds a NIO as new port on Ethernet switch. - - :param nio: NIO instance to add - :param port: port to allocate for the NIO - """ - - if port in self._nios: - raise DynamipsError("Port {} isn't free".format(port)) - - self._hypervisor.send("ethsw add_nio {name} {nio}".format(name=self._name, - nio=nio)) - - log.info("Ethernet switch {name} [id={id}]: NIO {nio} bound to port {port}".format(name=self._name, - id=self._id, - nio=nio, - port=port)) - self._nios[port] = nio - - def remove_nio(self, port): - """ - Removes the specified NIO as member of this Ethernet switch. - - :param port: allocated port - - :returns: the NIO that was bound to the port - """ - - if port not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port)) - - nio = self._nios[port] - self._hypervisor.send("ethsw remove_nio {name} {nio}".format(name=self._name, - nio=nio)) - - log.info("Ethernet switch {name} [id={id}]: NIO {nio} removed from port {port}".format(name=self._name, - id=self._id, - nio=nio, - port=port)) - - del self._nios[port] - - if port in self._mapping: - del self._mapping[port] - - return nio - - def set_access_port(self, port, vlan_id): - """ - Sets the specified port as an ACCESS port. - - :param port: allocated port - :param vlan_id: VLAN number membership - """ - - if port not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port)) - - nio = self._nios[port] - self._hypervisor.send("ethsw set_access_port {name} {nio} {vlan_id}".format(name=self._name, - nio=nio, - vlan_id=vlan_id)) - - log.info("Ethernet switch {name} [id={id}]: port {port} set as an access port in VLAN {vlan_id}".format(name=self._name, - id=self._id, - port=port, - vlan_id=vlan_id)) - self._mapping[port] = ("access", vlan_id) - - def set_dot1q_port(self, port, native_vlan): - """ - Sets the specified port as a 802.1Q trunk port. - - :param port: allocated port - :param native_vlan: native VLAN for this trunk port - """ - - if port not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port)) - - nio = self._nios[port] - self._hypervisor.send("ethsw set_dot1q_port {name} {nio} {native_vlan}".format(name=self._name, - nio=nio, - native_vlan=native_vlan)) - - log.info("Ethernet switch {name} [id={id}]: port {port} set as a 802.1Q port with native VLAN {vlan_id}".format(name=self._name, - id=self._id, - port=port, - vlan_id=native_vlan)) - - self._mapping[port] = ("dot1q", native_vlan) - - def set_qinq_port(self, port, outer_vlan): - """ - Sets the specified port as a trunk (QinQ) port. - - :param port: allocated port - :param outer_vlan: outer VLAN (transport VLAN) for this QinQ port - """ - - if port not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port)) - - nio = self._nios[port] - self._hypervisor.send("ethsw set_qinq_port {name} {nio} {outer_vlan}".format(name=self._name, - nio=nio, - outer_vlan=outer_vlan)) - - log.info("Ethernet switch {name} [id={id}]: port {port} set as a QinQ port with outer VLAN {vlan_id}".format(name=self._name, - id=self._id, - port=port, - vlan_id=outer_vlan)) - self._mapping[port] = ("qinq", outer_vlan) - - def get_mac_addr_table(self): - """ - Returns the MAC address table for this Ethernet switch. - - :returns: list of entries (Ethernet address, VLAN, NIO) - """ - - return self._hypervisor.send("ethsw show_mac_addr_table {}".format(self._name)) - - def clear_mac_addr_table(self): - """ - Clears the MAC address table for this Ethernet switch. - """ - - self._hypervisor.send("ethsw clear_mac_addr_table {}".format(self._name)) - - def start_capture(self, port, output_file, data_link_type="DLT_EN10MB"): - """ - Starts a packet capture. - - :param port: allocated port - :param output_file: PCAP destination file for the capture - :param data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB - """ - - if port not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port)) - - nio = self._nios[port] - - data_link_type = data_link_type.lower() - if data_link_type.startswith("dlt_"): - data_link_type = data_link_type[4:] - - if nio.input_filter[0] is not None and nio.output_filter[0] is not None: - raise DynamipsError("Port {} has already a filter applied".format(port)) - - try: - os.makedirs(os.path.dirname(output_file)) - except FileExistsError: - pass - except OSError as e: - raise DynamipsError("Could not create captures directory {}".format(e)) - - nio.bind_filter("both", "capture") - nio.setup_filter("both", "{} {}".format(data_link_type, output_file)) - - log.info("Ethernet switch {name} [id={id}]: starting packet capture on {port}".format(name=self._name, - id=self._id, - port=port)) - - def stop_capture(self, port): - """ - Stops a packet capture. - - :param port: allocated port - """ - - if port not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port)) - - nio = self._nios[port] - nio.unbind_filter("both") - log.info("Ethernet switch {name} [id={id}]: stopping packet capture on {port}".format(name=self._name, - id=self._id, - port=port)) diff --git a/gns3server/old_modules/dynamips/nodes/frame_relay_switch.py b/gns3server/old_modules/dynamips/nodes/frame_relay_switch.py deleted file mode 100644 index 8a309301..00000000 --- a/gns3server/old_modules/dynamips/nodes/frame_relay_switch.py +++ /dev/null @@ -1,328 +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 . - -""" -Interface for Dynamips virtual Frame-Relay switch module. -http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L642 -""" - -import os -from ..dynamips_error import DynamipsError - -import logging -log = logging.getLogger(__name__) - - -class FrameRelaySwitch(object): - - """ - Dynamips Frame Relay switch. - - :param hypervisor: Dynamips hypervisor instance - :param name: name for this switch - """ - - _instances = [] - - def __init__(self, hypervisor, name): - - # find an instance identifier (0 < id <= 4096) - self._id = 0 - for identifier in range(1, 4097): - if identifier not in self._instances: - self._id = identifier - self._instances.append(self._id) - break - - if self._id == 0: - raise DynamipsError("Maximum number of instances reached") - - self._hypervisor = hypervisor - self._name = '"' + name + '"' # put name into quotes to protect spaces - self._hypervisor.send("frsw create {}".format(self._name)) - - log.info("Frame Relay switch {name} [id={id}] has been created".format(name=self._name, - id=self._id)) - - self._hypervisor.devices.append(self) - self._nios = {} - self._mapping = {} - - @classmethod - def reset(cls): - """ - Resets the instance count and the allocated instances list. - """ - - cls._instances.clear() - - @property - def id(self): - """ - Returns the unique ID for this Frame Relay switch. - - :returns: id (integer) - """ - - return self._id - - @property - def name(self): - """ - Returns the current name of this Frame Relay switch. - - :returns: Frame Relay switch name - """ - - return self._name[1:-1] # remove quotes - - @name.setter - def name(self, new_name): - """ - Renames this Frame Relay switch. - - :param new_name: New name for this switch - """ - - new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces - self._hypervisor.send("frsw rename {name} {new_name}".format(name=self._name, - new_name=new_name)) - - log.info("Frame Relay switch {name} [id={id}]: renamed to {new_name}".format(name=self._name, - id=self._id, - new_name=new_name)) - - self._name = new_name - - @property - def hypervisor(self): - """ - Returns the current hypervisor. - - :returns: hypervisor instance - """ - - return self._hypervisor - - def list(self): - """ - Returns all Frame Relay switches instances. - - :returns: list of all Frame Relay switches - """ - - return self._hypervisor.send("frsw list") - - @property - def nios(self): - """ - Returns all the NIOs member of this Frame Relay switch. - - :returns: nio list - """ - - return self._nios - - @property - def mapping(self): - """ - Returns port mapping - - :returns: mapping list - """ - - return self._mapping - - def delete(self): - """ - Deletes this Frame Relay switch. - """ - - self._hypervisor.send("frsw delete {}".format(self._name)) - - log.info("Frame Relay switch {name} [id={id}] has been deleted".format(name=self._name, - id=self._id)) - self._hypervisor.devices.remove(self) - self._instances.remove(self._id) - - def has_port(self, port): - """ - Checks if a port exists on this Frame Relay switch. - - :returns: boolean - """ - - if port in self._nios: - return True - return False - - def add_nio(self, nio, port): - """ - Adds a NIO as new port on Frame Relay switch. - - :param nio: NIO instance to add - :param port: port to allocate for the NIO - """ - - if port in self._nios: - raise DynamipsError("Port {} isn't free".format(port)) - - log.info("Frame Relay switch {name} [id={id}]: NIO {nio} bound to port {port}".format(name=self._name, - id=self._id, - nio=nio, - port=port)) - - self._nios[port] = nio - - def remove_nio(self, port): - """ - Removes the specified NIO as member of this Frame Relay switch. - - :param port: allocated port - - :returns: the NIO that was bound to the allocated port - """ - - if port not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port)) - - nio = self._nios[port] - log.info("Frame Relay switch {name} [id={id}]: NIO {nio} removed from port {port}".format(name=self._name, - id=self._id, - nio=nio, - port=port)) - - del self._nios[port] - return nio - - def map_vc(self, port1, dlci1, port2, dlci2): - """ - Creates a new Virtual Circuit connection (unidirectional). - - :param port1: input port - :param dlci1: input DLCI - :param port2: output port - :param dlci2: output DLCI - """ - - if port1 not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port1)) - - if port2 not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port2)) - - nio1 = self._nios[port1] - nio2 = self._nios[port2] - - self._hypervisor.send("frsw create_vc {name} {input_nio} {input_dlci} {output_nio} {output_dlci}".format(name=self._name, - input_nio=nio1, - input_dlci=dlci1, - output_nio=nio2, - output_dlci=dlci2)) - - log.info("Frame Relay switch {name} [id={id}]: VC from port {port1} DLCI {dlci1} to port {port2} DLCI {dlci2} created".format(name=self._name, - id=self._id, - port1=port1, - dlci1=dlci1, - port2=port2, - dlci2=dlci2)) - - self._mapping[(port1, dlci1)] = (port2, dlci2) - - def unmap_vc(self, port1, dlci1, port2, dlci2): - """ - Deletes a Virtual Circuit connection (unidirectional). - - :param port1: input port - :param dlci1: input DLCI - :param port2: output port - :param dlci2: output DLCI - """ - - if port1 not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port1)) - - if port2 not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port2)) - - nio1 = self._nios[port1] - nio2 = self._nios[port2] - - self._hypervisor.send("frsw delete_vc {name} {input_nio} {input_dlci} {output_nio} {output_dlci}".format(name=self._name, - input_nio=nio1, - input_dlci=dlci1, - output_nio=nio2, - output_dlci=dlci2)) - - log.info("Frame Relay switch {name} [id={id}]: VC from port {port1} DLCI {dlci1} to port {port2} DLCI {dlci2} deleted".format(name=self._name, - id=self._id, - port1=port1, - dlci1=dlci1, - port2=port2, - dlci2=dlci2)) - del self._mapping[(port1, dlci1)] - - def start_capture(self, port, output_file, data_link_type="DLT_FRELAY"): - """ - Starts a packet capture. - - :param port: allocated port - :param output_file: PCAP destination file for the capture - :param data_link_type: PCAP data link type (DLT_*), default is DLT_FRELAY - """ - - if port not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port)) - - nio = self._nios[port] - - data_link_type = data_link_type.lower() - if data_link_type.startswith("dlt_"): - data_link_type = data_link_type[4:] - - if nio.input_filter[0] is not None and nio.output_filter[0] is not None: - raise DynamipsError("Port {} has already a filter applied".format(port)) - - try: - os.makedirs(os.path.dirname(output_file)) - except FileExistsError: - pass - except OSError as e: - raise DynamipsError("Could not create captures directory {}".format(e)) - - nio.bind_filter("both", "capture") - nio.setup_filter("both", "{} {}".format(data_link_type, output_file)) - - log.info("Frame relay switch {name} [id={id}]: starting packet capture on {port}".format(name=self._name, - id=self._id, - port=port)) - - def stop_capture(self, port): - """ - Stops a packet capture. - - :param port: allocated port - """ - - if port not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port)) - - nio = self._nios[port] - nio.unbind_filter("both") - log.info("Frame relay switch {name} [id={id}]: stopping packet capture on {port}".format(name=self._name, - id=self._id, - port=port)) diff --git a/gns3server/old_modules/dynamips/nodes/hub.py b/gns3server/old_modules/dynamips/nodes/hub.py deleted file mode 100644 index 18cedbe1..00000000 --- a/gns3server/old_modules/dynamips/nodes/hub.py +++ /dev/null @@ -1,189 +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 . - -""" -Hub object that uses the Bridge interface to create a hub with ports. -""" - -import os -from .bridge import Bridge -from ..dynamips_error import DynamipsError - -import logging -log = logging.getLogger(__name__) - - -class Hub(Bridge): - - """ - Dynamips hub (based on Bridge) - - :param hypervisor: Dynamips hypervisor instance - :param name: name for this hub - """ - - _instances = [] - - def __init__(self, hypervisor, name): - - # find an instance identifier (0 < id <= 4096) - self._id = 0 - for identifier in range(1, 4097): - if identifier not in self._instances: - self._id = identifier - self._instances.append(self._id) - break - - if self._id == 0: - raise DynamipsError("Maximum number of instances reached") - - self._mapping = {} - Bridge.__init__(self, hypervisor, name) - - log.info("Ethernet hub {name} [id={id}] has been created".format(name=self._name, - id=self._id)) - - @classmethod - def reset(cls): - """ - Resets the instance count and the allocated instances list. - """ - - cls._instances.clear() - - @property - def id(self): - """ - Returns the unique ID for this Ethernet switch. - - :returns: id (integer) - """ - - return self._id - - @property - def mapping(self): - """ - Returns port mapping - - :returns: mapping list - """ - - return self._mapping - - def delete(self): - """ - Deletes this hub. - """ - - Bridge.delete(self) - log.info("Ethernet hub {name} [id={id}] has been deleted".format(name=self._name, - id=self._id)) - self._instances.remove(self._id) - - def add_nio(self, nio, port): - """ - Adds a NIO as new port on this hub. - - :param nio: NIO instance to add - :param port: port to allocate for the NIO - """ - - if port in self._mapping: - raise DynamipsError("Port {} isn't free".format(port)) - - Bridge.add_nio(self, nio) - - log.info("Ethernet hub {name} [id={id}]: NIO {nio} bound to port {port}".format(name=self._name, - id=self._id, - nio=nio, - port=port)) - self._mapping[port] = nio - - def remove_nio(self, port): - """ - Removes the specified NIO as member of this hub. - - :param port: allocated port - - :returns: the NIO that was bound to the allocated port - """ - - if port not in self._mapping: - raise DynamipsError("Port {} is not allocated".format(port)) - - nio = self._mapping[port] - Bridge.remove_nio(self, nio) - - log.info("Ethernet switch {name} [id={id}]: NIO {nio} removed from port {port}".format(name=self._name, - id=self._id, - nio=nio, - port=port)) - - del self._mapping[port] - return nio - - def start_capture(self, port, output_file, data_link_type="DLT_EN10MB"): - """ - Starts a packet capture. - - :param port: allocated port - :param output_file: PCAP destination file for the capture - :param data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB - """ - - if port not in self._mapping: - raise DynamipsError("Port {} is not allocated".format(port)) - - nio = self._mapping[port] - - data_link_type = data_link_type.lower() - if data_link_type.startswith("dlt_"): - data_link_type = data_link_type[4:] - - if nio.input_filter[0] is not None and nio.output_filter[0] is not None: - raise DynamipsError("Port {} has already a filter applied".format(port)) - - try: - os.makedirs(os.path.dirname(output_file)) - except FileExistsError: - pass - except OSError as e: - raise DynamipsError("Could not create captures directory {}".format(e)) - - nio.bind_filter("both", "capture") - nio.setup_filter("both", "{} {}".format(data_link_type, output_file)) - - log.info("Ethernet hub {name} [id={id}]: starting packet capture on {port}".format(name=self._name, - id=self._id, - port=port)) - - def stop_capture(self, port): - """ - Stops a packet capture. - - :param port: allocated port - """ - - if port not in self._mapping: - raise DynamipsError("Port {} is not allocated".format(port)) - - nio = self._mapping[port] - nio.unbind_filter("both") - log.info("Ethernet hub {name} [id={id}]: stopping packet capture on {port}".format(name=self._name, - id=self._id, - port=port)) diff --git a/gns3server/old_modules/dynamips/nodes/router.py b/gns3server/old_modules/dynamips/nodes/router.py deleted file mode 100644 index 18d8db8d..00000000 --- a/gns3server/old_modules/dynamips/nodes/router.py +++ /dev/null @@ -1,1676 +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 . - -""" -Interface for Dynamips virtual Machine module ("vm") -http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L77 -""" - -from ..dynamips_error import DynamipsError -from ...attic import find_unused_port - -import time -import sys -import os -import base64 - -import logging -log = logging.getLogger(__name__) - - -class Router(object): - - """ - Dynamips router implementation. - - :param hypervisor: Dynamips hypervisor instance - :param name: name for this router - :param router_id: router instance ID - :param platform: c7200, c3745, c3725, c3600, c2691, c2600 or c1700 - :param ghost_flag: used when creating a ghost IOS. - """ - - _instances = [] - _allocated_console_ports = [] - _allocated_aux_ports = [] - _status = {0: "inactive", - 1: "shutting down", - 2: "running", - 3: "suspended"} - - def __init__(self, hypervisor, name, router_id=None, platform="c7200", ghost_flag=False): - - if not ghost_flag: - - if not router_id: - # find an instance identifier if none is provided (0 < id <= 4096) - self._id = 0 - for identifier in range(1, 4097): - if identifier not in self._instances: - self._id = identifier - self._instances.append(self._id) - break - - if self._id == 0: - raise DynamipsError("Maximum number of instances reached") - else: - if router_id in self._instances: - raise DynamipsError("Router identifier {} is already used by another router".format(router_id)) - self._id = router_id - self._instances.append(self._id) - - else: - log.info("creating a new ghost IOS file") - self._id = 0 - name = "Ghost" - - self._hypervisor = hypervisor - self._name = '"' + name + '"' # put name into quotes to protect spaces - self._platform = platform - self._image = "" - self._startup_config = "" - self._private_config = "" - self._ram = 128 # Megabytes - self._nvram = 128 # Kilobytes - self._mmap = True - self._sparsemem = True - self._clock_divisor = 8 - self._idlepc = "" - self._idlemax = 500 - self._idlesleep = 30 - self._ghost_file = "" - self._ghost_status = 0 - if sys.platform.startswith("win"): - self._exec_area = 16 # 16 MB by default on Windows (Cygwin) - else: - self._exec_area = 64 # 64 MB on other systems - self._jit_sharing_group = None - self._disk0 = 0 # Megabytes - self._disk1 = 0 # Megabytes - self._confreg = "0x2102" - self._console = None - self._aux = None - self._mac_addr = None - self._system_id = "FTX0945W0MY" # processor board ID in IOS - self._slots = [] - - self._hypervisor.send("vm create {name} {id} {platform}".format(name=self._name, - id=self._id, - platform=self._platform)) - - if not ghost_flag: - log.info("router {platform} {name} [id={id}] has been created".format(name=self._name, - platform=platform, - id=self._id)) - - try: - # allocate a console port - self._console = find_unused_port(self._hypervisor.console_start_port_range, - self._hypervisor.console_end_port_range, - self._hypervisor.host, - ignore_ports=self._allocated_console_ports) - - self._hypervisor.send("vm set_con_tcp_port {name} {console}".format(name=self._name, - console=self._console)) - self._allocated_console_ports.append(self._console) - - # allocate a auxiliary console port - self._aux = find_unused_port(self._hypervisor.aux_start_port_range, - self._hypervisor.aux_end_port_range, - self._hypervisor.host, - ignore_ports=self._allocated_aux_ports) - - self._hypervisor.send("vm set_aux_tcp_port {name} {aux}".format(name=self._name, - aux=self._aux)) - - self._allocated_aux_ports.append(self._aux) - except Exception as e: - raise DynamipsError(e) - - # get the default base MAC address - self._mac_addr = self._hypervisor.send("{platform} get_mac_addr {name}".format(platform=self._platform, - name=self._name))[0] - - self._hypervisor.devices.append(self) - - @classmethod - def reset(cls): - """ - Resets the instance count and the allocated instances list. - """ - - cls._instances.clear() - cls._allocated_console_ports.clear() - cls._allocated_aux_ports.clear() - - def defaults(self): - """ - Returns all the default base attribute values for routers. - - :returns: default values (dictionary) - """ - - router_defaults = {"platform": self._platform, - "image": self._image, - "startup_config": self._startup_config, - "private_config": self._private_config, - "ram": self._ram, - "nvram": self._nvram, - "mmap": self._mmap, - "sparsemem": self._sparsemem, - "clock_divisor": self._clock_divisor, - "idlepc": self._idlepc, - "idlemax": self._idlemax, - "idlesleep": self._idlesleep, - "exec_area": self._exec_area, - "jit_sharing_group": self._jit_sharing_group, - "disk0": self._disk0, - "disk1": self._disk1, - "confreg": self._confreg, - "console": self._console, - "aux": self._aux, - "mac_addr": self._mac_addr, - "system_id": self._system_id} - - slot_id = 0 - for slot in self._slots: - if slot: - slot = str(slot) - router_defaults["slot" + str(slot_id)] = slot - slot_id += 1 - - if self._slots[0] and self._slots[0].wics: - for wic_slot_id in range(0, len(self._slots[0].wics)): - router_defaults["wic" + str(wic_slot_id)] = None - - return router_defaults - - @property - def id(self): - """ - Returns the unique ID for this router. - - :returns: id (integer) - """ - - return self._id - - @property - def name(self): - """ - Returns the name of this router. - - :returns: name (string) - """ - - return self._name[1:-1] # remove quotes - - @name.setter - def name(self, new_name): - """ - Renames this router. - - :param new_name: new name string - """ - - if self._startup_config: - # change the hostname in the startup-config - startup_config_path = os.path.join(self.hypervisor.working_dir, "configs", "i{}_startup-config.cfg".format(self.id)) - if os.path.isfile(startup_config_path): - try: - with open(startup_config_path, "r+", errors="replace") as f: - old_config = f.read() - new_config = old_config.replace(self.name, new_name) - f.seek(0) - f.write(new_config) - except OSError as e: - raise DynamipsError("Could not amend the configuration {}: {}".format(startup_config_path, e)) - - if self._private_config: - # change the hostname in the private-config - private_config_path = os.path.join(self.hypervisor.working_dir, "configs", "i{}_private-config.cfg".format(self.id)) - if os.path.isfile(private_config_path): - try: - with open(private_config_path, "r+", errors="replace") as f: - old_config = f.read() - new_config = old_config.replace(self.name, new_name) - f.seek(0) - f.write(new_config) - except OSError as e: - raise DynamipsError("Could not amend the configuration {}: {}".format(private_config_path, e)) - - new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces - self._hypervisor.send("vm rename {name} {new_name}".format(name=self._name, - new_name=new_name)) - - log.info("router {name} [id={id}]: renamed to {new_name}".format(name=self._name, - id=self._id, - new_name=new_name)) - self._name = new_name - - @property - def platform(self): - """ - Returns the platform of this router. - - :returns: platform name (string): - c7200, c3745, c3725, c3600, c2691, c2600 or c1700 - """ - - return self._platform - - @property - def hypervisor(self): - """ - Returns the current hypervisor. - - :returns: hypervisor instance - """ - - return self._hypervisor - - def list(self): - """ - Returns all VM instances - - :returns: list of all VM instances - """ - - return self._hypervisor.send("vm list") - - def list_con_ports(self): - """ - Returns all VM console TCP ports - - :returns: list of port numbers - """ - - return self._hypervisor.send("vm list_con_ports") - - def delete(self): - """ - Deletes this router. - """ - - self._hypervisor.send("vm delete {}".format(self._name)) - self._hypervisor.devices.remove(self) - log.info("router {name} [id={id}] has been deleted".format(name=self._name, id=self._id)) - if self._id in self._instances: - self._instances.remove(self._id) - if self.console: - self._allocated_console_ports.remove(self.console) - if self.aux: - self._allocated_aux_ports.remove(self.aux) - - def clean_delete(self): - """ - Deletes this router & associated files (nvram, disks etc.) - """ - - self._hypervisor.send("vm clean_delete {}".format(self._name)) - self._hypervisor.devices.remove(self) - - if self._startup_config: - # delete the startup-config - startup_config_path = os.path.join(self.hypervisor.working_dir, "configs", "{}.cfg".format(self.name)) - if os.path.isfile(startup_config_path): - os.remove(startup_config_path) - - if self._private_config: - # delete the private-config - private_config_path = os.path.join(self.hypervisor.working_dir, "configs", "{}-private.cfg".format(self.name)) - if os.path.isfile(private_config_path): - os.remove(private_config_path) - - log.info("router {name} [id={id}] has been deleted (including associated files)".format(name=self._name, id=self._id)) - if self._id in self._instances: - self._instances.remove(self._id) - if self.console: - self._allocated_console_ports.remove(self.console) - if self.aux: - self._allocated_aux_ports.remove(self.aux) - - def start(self): - """ - Starts this router. - At least the IOS image must be set before starting it. - """ - - status = self.get_status() - if status == "suspended": - self.resume() - elif status == "inactive": - - if not os.path.isfile(self._image) or not os.path.exists(self._image): - if os.path.islink(self._image): - raise DynamipsError("IOS image '{}' linked to '{}' is not accessible".format(self._image, os.path.realpath(self._image))) - else: - raise DynamipsError("IOS image '{}' is not accessible".format(self._image)) - - try: - with open(self._image, "rb") as f: - # read the first 7 bytes of the file. - elf_header_start = f.read(7) - except OSError as e: - raise DynamipsError("Cannot read ELF header for IOS image {}: {}".format(self._image, e)) - - # IOS images must start with the ELF magic number, be 32-bit, big endian and have an ELF version of 1 - if elf_header_start != b'\x7fELF\x01\x02\x01': - raise DynamipsError("'{}' is not a valid IOS image".format(self._image)) - - self._hypervisor.send("vm start {}".format(self._name)) - log.info("router {name} [id={id}] has been started".format(name=self._name, id=self._id)) - - def stop(self): - """ - Stops this router. - The settings are kept. - """ - - if self.get_status() != "inactive": - self._hypervisor.send("vm stop {}".format(self._name)) - log.info("router {name} [id={id}] has been stopped".format(name=self._name, id=self._id)) - - def suspend(self): - """ - Suspends this router - """ - - if self.get_status() == "running": - self._hypervisor.send("vm suspend {}".format(self._name)) - log.info("router {name} [id={id}] has been suspended".format(name=self._name, id=self._id)) - - def resume(self): - """ - Resumes this suspended router - """ - - self._hypervisor.send("vm resume {}".format(self._name)) - log.info("router {name} [id={id}] has been resumed".format(name=self._name, id=self._id)) - - def get_status(self): - """ - Returns the status of this router - - :returns: inactive, shutting down, running or suspended. - """ - - status_id = int(self._hypervisor.send("vm get_status {}".format(self._name))[0]) - return self._status[status_id] - - def is_running(self): - """ - Checks if this router is running. - - :returns: True if running, False otherwise - """ - - if self.get_status() == "running": - return True - return False - - @property - def jit_sharing_group(self): - """ - Returns the JIT sharing group for this router. - - :returns: translation sharing group ID - """ - - return self._jit_sharing_group - - @jit_sharing_group.setter - def jit_sharing_group(self, group_id): - """ - Sets the translation sharing group (unstable). - - :param group_id: translation sharing group ID - """ - - if not self._image: - raise DynamipsError("Register an IOS image fist") - - try: - self._hypervisor.send("vm set_tsg {name} {group_id}".format(name=self._name, - group_id=group_id)) - except DynamipsError: - raise DynamipsError("JIT sharing is only supported in Dynamips >= 0.2.8-RC3 unstable") - - log.info("router {name} [id={id}]: set in JIT sharing group {group_id}".format(name=self._name, - id=self._id, - group_id=group_id)) - - self._jit_sharing_group = group_id - self._hypervisor.add_jitsharing_group(os.path.basename(self._image), group_id) - - def set_debug_level(self, level): - """ - Sets the debug level for this router (default is 0). - - :param level: level number - """ - - self._hypervisor.send("vm set_debug_level {name} {level}".format(name=self._name, - level=level)) - - @property - def image(self): - """ - Returns this IOS image for this router. - - :returns: path to IOS image file - """ - - return self._image - - @image.setter - def image(self, image): - """ - Sets the IOS image for this router. - There is no default. - - :param image: path to IOS image file - """ - - # encase image in quotes to protect spaces in the path - self._hypervisor.send("vm set_ios {name} {image}".format(name=self._name, - image='"' + image + '"')) - - log.info("router {name} [id={id}]: has a new IOS image set: {image}".format(name=self._name, - id=self._id, - image='"' + image + '"')) - - self._image = image - - @property - def startup_config(self): - """ - Returns the startup-config for this router. - - :returns: path to startup-config file - """ - - return self._startup_config - - @startup_config.setter - def startup_config(self, startup_config): - """ - Sets the startup-config for this router. - - :param startup_config: path to startup-config file - """ - - self._startup_config = startup_config - - @property - def private_config(self): - """ - Returns the private-config for this router. - - :returns: path to private-config file - """ - - return self._private_config - - @private_config.setter - def private_config(self, private_config): - """ - Sets the private-config for this router. - - :param private_config: path to private-config file - """ - - self._private_config = private_config - - def set_config(self, startup_config, private_config=''): - """ - Sets the config files that are pushed to startup-config and - private-config in NVRAM when the instance is started. - - :param startup_config: path to statup-config file - :param private_config: path to private-config file - (keep existing data when if an empty string) - """ - - if self._startup_config != startup_config or self._private_config != private_config: - - self._hypervisor.send("vm set_config {name} {startup} {private}".format(name=self._name, - startup='"' + startup_config + '"', - private='"' + private_config + '"')) - - log.info("router {name} [id={id}]: has a startup-config set: {startup}".format(name=self._name, - id=self._id, - startup='"' + startup_config + '"')) - - self._startup_config = startup_config - - if private_config: - log.info("router {name} [id={id}]: has a private-config set: {private}".format(name=self._name, - id=self._id, - private='"' + private_config + '"')) - - self._private_config = private_config - - def extract_config(self): - """ - Gets the contents of the config files - startup-config and private-config from NVRAM. - - :returns: tuple (startup-config, private-config) base64 encoded - """ - - try: - reply = self._hypervisor.send("vm extract_config {}".format(self._name))[0].rsplit(' ', 2)[-2:] - except IOError: - # for some reason Dynamips gets frozen when it does not find the magic number in the NVRAM file. - return None, None - startup_config = reply[0][1:-1] # get statup-config and remove single quotes - private_config = reply[1][1:-1] # get private-config and remove single quotes - return startup_config, private_config - - def push_config(self, startup_config, private_config='(keep)'): - """ - Pushes configuration to the config files startup-config and private-config in NVRAM. - The data is a Base64 encoded string, or '(keep)' to keep existing data. - - :param startup_config: statup-config string base64 encoded - :param private_config: private-config string base64 encoded - (keep existing data when if the value is ('keep')) - """ - - self._hypervisor.send("vm push_config {name} {startup} {private}".format(name=self._name, - startup=startup_config, - private=private_config)) - - log.info("router {name} [id={id}]: new startup-config pushed".format(name=self._name, - id=self._id)) - - if private_config != '(keep)': - log.info("router {name} [id={id}]: new private-config pushed".format(name=self._name, - id=self._id)) - - def save_configs(self): - """ - Saves the startup-config and private-config to files. - """ - - if self.startup_config or self.private_config: - startup_config_base64, private_config_base64 = self.extract_config() - if startup_config_base64: - try: - config = base64.decodebytes(startup_config_base64.encode("utf-8")).decode("utf-8") - config = "!\n" + config.replace("\r", "") - config_path = os.path.join(self.hypervisor.working_dir, self.startup_config) - with open(config_path, "w") as f: - log.info("saving startup-config to {}".format(self.startup_config)) - f.write(config) - except OSError as e: - raise DynamipsError("Could not save the startup configuration {}: {}".format(config_path, e)) - - if private_config_base64: - try: - config = base64.decodebytes(private_config_base64.encode("utf-8")).decode("utf-8") - config = "!\n" + config.replace("\r", "") - config_path = os.path.join(self.hypervisor.working_dir, self.private_config) - with open(config_path, "w") as f: - log.info("saving private-config to {}".format(self.private_config)) - f.write(config) - except OSError as e: - raise DynamipsError("Could not save the private configuration {}: {}".format(config_path, e)) - - @property - def ram(self): - """ - Returns the amount of RAM allocated to this router. - - :returns: amount of RAM in Mbytes (integer) - """ - - return self._ram - - @ram.setter - def ram(self, ram): - """ - Sets amount of RAM allocated to this router - - :param ram: amount of RAM in Mbytes (integer) - """ - - if self._ram == ram: - return - - self._hypervisor.send("vm set_ram {name} {ram}".format(name=self._name, - ram=ram)) - - log.info("router {name} [id={id}]: RAM updated from {old_ram}MB to {new_ram}MB".format(name=self._name, - id=self._id, - old_ram=self._ram, - new_ram=ram)) - - self._hypervisor.decrease_memory_load(self._ram) - self._ram = ram - self._hypervisor.increase_memory_load(ram) - - @property - def nvram(self): - """ - Returns the mount of NVRAM allocated to this router. - - :returns: amount of NVRAM in Kbytes (integer) - """ - - return self._nvram - - @nvram.setter - def nvram(self, nvram): - """ - Sets amount of NVRAM allocated to this router - - :param nvram: amount of NVRAM in Kbytes (integer) - """ - - if self._nvram == nvram: - return - - self._hypervisor.send("vm set_nvram {name} {nvram}".format(name=self._name, - nvram=nvram)) - - log.info("router {name} [id={id}]: NVRAM updated from {old_nvram}KB to {new_nvram}KB".format(name=self._name, - id=self._id, - old_nvram=self._nvram, - new_nvram=nvram)) - self._nvram = nvram - - @property - def mmap(self): - """ - Returns True if a mapped file is used to simulate this router memory. - - :returns: boolean either mmap is activated or not - """ - - return self._mmap - - @mmap.setter - def mmap(self, mmap): - """ - Enable/Disable use of a mapped file to simulate router memory. - By default, a mapped file is used. This is a bit slower, but requires less memory. - - :param mmap: activate/deactivate mmap (boolean) - """ - - if mmap: - flag = 1 - else: - flag = 0 - self._hypervisor.send("vm set_ram_mmap {name} {mmap}".format(name=self._name, - mmap=flag)) - - if mmap: - log.info("router {name} [id={id}]: mmap enabled".format(name=self._name, - id=self._id)) - else: - log.info("router {name} [id={id}]: mmap disabled".format(name=self._name, - id=self._id)) - self._mmap = mmap - - @property - def sparsemem(self): - """ - Returns True if sparse memory is used on this router. - - :returns: boolean either mmap is activated or not - """ - - return self._sparsemem - - @sparsemem.setter - def sparsemem(self, sparsemem): - """ - Enable/disable use of sparse memory - - :param sparsemem: activate/deactivate sparsemem (boolean) - """ - - if sparsemem: - flag = 1 - else: - flag = 0 - self._hypervisor.send("vm set_sparse_mem {name} {sparsemem}".format(name=self._name, - sparsemem=flag)) - - if sparsemem: - log.info("router {name} [id={id}]: sparse memory enabled".format(name=self._name, - id=self._id)) - else: - log.info("router {name} [id={id}]: sparse memory disabled".format(name=self._name, - id=self._id)) - self._sparsemem = sparsemem - - @property - def clock_divisor(self): - """ - Returns the clock divisor value for this router. - - :returns: clock divisor value (integer) - """ - - return self._clock_divisor - - @clock_divisor.setter - def clock_divisor(self, clock_divisor): - """ - Sets the clock divisor value. The higher is the value, the faster is the clock in the - virtual machine. The default is 4, but it is often required to adjust it. - - :param clock_divisor: clock divisor value (integer) - """ - - self._hypervisor.send("vm set_clock_divisor {name} {clock}".format(name=self._name, - clock=clock_divisor)) - - log.info("router {name} [id={id}]: clock divisor updated from {old_clock} to {new_clock}".format(name=self._name, - id=self._id, - old_clock=self._clock_divisor, - new_clock=clock_divisor)) - self._clock_divisor = clock_divisor - - @property - def idlepc(self): - """ - Returns the idle Pointer Counter (PC). - - :returns: idlepc value (string) - """ - - return self._idlepc - - @idlepc.setter - def idlepc(self, idlepc): - """ - Sets the idle Pointer Counter (PC) - - :param idlepc: idlepc value (string) - """ - - if not idlepc: - idlepc = "0x0" - - if not self.is_running(): - # router is not running - self._hypervisor.send("vm set_idle_pc {name} {idlepc}".format(name=self._name, - idlepc=idlepc)) - else: - self._hypervisor.send("vm set_idle_pc_online {name} 0 {idlepc}".format(name=self._name, - idlepc=idlepc)) - - log.info("router {name} [id={id}]: idle-PC set to {idlepc}".format(name=self._name, - id=self._id, - idlepc=idlepc)) - - self._idlepc = idlepc - - def get_idle_pc_prop(self): - """ - Gets the idle PC proposals. - Takes 1000 measurements and records up to 10 idle PC proposals. - There is a 10ms wait between each measurement. - - :returns: list of idle PC proposal - """ - - if not self.is_running(): - # router is not running - raise DynamipsError("router {name} is not running".format(name=self._name)) - - log.info("router {name} [id={id}] has started calculating Idle-PC values".format(name=self._name, id=self._id)) - begin = time.time() - idlepcs = self._hypervisor.send("vm get_idle_pc_prop {} 0".format(self._name)) - log.info("router {name} [id={id}] has finished calculating Idle-PC values after {time:.4f} seconds".format(name=self._name, - id=self._id, - time=time.time() - begin)) - return idlepcs - - def show_idle_pc_prop(self): - """ - Dumps the idle PC proposals (previously generated). - - :returns: list of idle PC proposal - """ - - if not self.is_running(): - # router is not running - raise DynamipsError("router {name} is not running".format(name=self._name)) - - return self._hypervisor.send("vm show_idle_pc_prop {} 0".format(self._name)) - - @property - def idlemax(self): - """ - Returns CPU idle max value. - - :returns: idle max (integer) - """ - - return self._idlemax - - @idlemax.setter - def idlemax(self, idlemax): - """ - Sets CPU idle max value - - :param idlemax: idle max value (integer) - """ - - if self.is_running(): # router is running - self._hypervisor.send("vm set_idle_max {name} 0 {idlemax}".format(name=self._name, - idlemax=idlemax)) - - log.info("router {name} [id={id}]: idlemax updated from {old_idlemax} to {new_idlemax}".format(name=self._name, - id=self._id, - old_idlemax=self._idlemax, - new_idlemax=idlemax)) - - self._idlemax = idlemax - - @property - def idlesleep(self): - """ - Returns CPU idle sleep time value. - - :returns: idle sleep (integer) - """ - - return self._idlesleep - - @idlesleep.setter - def idlesleep(self, idlesleep): - """ - Sets CPU idle sleep time value. - - :param idlesleep: idle sleep value (integer) - """ - - if self.is_running(): # router is running - self._hypervisor.send("vm set_idle_sleep_time {name} 0 {idlesleep}".format(name=self._name, - idlesleep=idlesleep)) - - log.info("router {name} [id={id}]: idlesleep updated from {old_idlesleep} to {new_idlesleep}".format(name=self._name, - id=self._id, - old_idlesleep=self._idlesleep, - new_idlesleep=idlesleep)) - - self._idlesleep = idlesleep - - def show_timer_drift(self): - """ - Shows info about potential timer drift. - - :returns: timer drift info. - """ - - return self._hypervisor.send("vm show_timer_drift {} 0".format(self._name)) - - @property - def ghost_file(self): - """ - Returns ghost RAM file. - - :returns: path to ghost file - """ - - return self._ghost_file - - @ghost_file.setter - def ghost_file(self, ghost_file): - """ - Sets ghost RAM file - - :ghost_file: path to ghost file - """ - - self._hypervisor.send("vm set_ghost_file {name} {ghost_file}".format(name=self._name, - ghost_file=ghost_file)) - - log.info("router {name} [id={id}]: ghost file set to {ghost_file}".format(name=self._name, - id=self._id, - ghost_file=ghost_file)) - - self._ghost_file = ghost_file - - # if this is a ghost instance, track this as a hosted ghost instance by this hypervisor - if self.ghost_status == 1: - self._hypervisor.add_ghost(ghost_file, self) - - def formatted_ghost_file(self): - """ - Returns a properly formatted ghost file name. - - :returns: formatted ghost_file name (string) - """ - - # replace specials characters in 'drive:\filename' in Linux and Dynamips in MS Windows or viceversa. - ghost_file = "{}-{}.ghost".format(os.path.basename(self._image), self._ram) - ghost_file = ghost_file.replace('\\', '-').replace('/', '-').replace(':', '-') - return ghost_file - - @property - def ghost_status(self): - """Returns ghost RAM status - - :returns: ghost status (integer) - """ - - return self._ghost_status - - @ghost_status.setter - def ghost_status(self, ghost_status): - """ - Sets ghost RAM status - - :param ghost_status: state flag indicating status - 0 => Do not use IOS ghosting - 1 => This is a ghost instance - 2 => Use an existing ghost instance - """ - - self._hypervisor.send("vm set_ghost_status {name} {ghost_status}".format(name=self._name, - ghost_status=ghost_status)) - - log.info("router {name} [id={id}]: ghost status set to {ghost_status}".format(name=self._name, - id=self._id, - ghost_status=ghost_status)) - self._ghost_status = ghost_status - - @property - def exec_area(self): - """ - Returns the exec area value. - - :returns: exec area value (integer) - """ - - return self._exec_area - - @exec_area.setter - def exec_area(self, exec_area): - """ - Sets the exec area value. - The exec area is a pool of host memory used to store pages - translated by the JIT (they contain the native code - corresponding to MIPS code pages). - - :param exec_area: exec area value (integer) - """ - - self._hypervisor.send("vm set_exec_area {name} {exec_area}".format(name=self._name, - exec_area=exec_area)) - - log.info("router {name} [id={id}]: exec area updated from {old_exec}MB to {new_exec}MB".format(name=self._name, - id=self._id, - old_exec=self._exec_area, - new_exec=exec_area)) - self._exec_area = exec_area - - @property - def disk0(self): - """ - Returns the size (MB) for PCMCIA disk0. - - :returns: disk0 size (integer) - """ - - return self._disk0 - - @disk0.setter - def disk0(self, disk0): - """ - Sets the size (MB) for PCMCIA disk0. - - :param disk0: disk0 size (integer) - """ - - self._hypervisor.send("vm set_disk0 {name} {disk0}".format(name=self._name, - disk0=disk0)) - - log.info("router {name} [id={id}]: disk0 updated from {old_disk0}MB to {new_disk0}MB".format(name=self._name, - id=self._id, - old_disk0=self._disk0, - new_disk0=disk0)) - self._disk0 = disk0 - - @property - def disk1(self): - """ - Returns the size (MB) for PCMCIA disk1. - - :returns: disk1 size (integer) - """ - - return self._disk1 - - @disk1.setter - def disk1(self, disk1): - """ - Sets the size (MB) for PCMCIA disk1. - - :param disk1: disk1 size (integer) - """ - - self._hypervisor.send("vm set_disk1 {name} {disk1}".format(name=self._name, - disk1=disk1)) - - log.info("router {name} [id={id}]: disk1 updated from {old_disk1}MB to {new_disk1}MB".format(name=self._name, - id=self._id, - old_disk1=self._disk1, - new_disk1=disk1)) - self._disk1 = disk1 - - @property - def confreg(self): - """ - Returns the configuration register. - The default is 0x2102. - - :returns: configuration register value (string) - """ - - return self._confreg - - @confreg.setter - def confreg(self, confreg): - """ - Sets the configuration register. - - :param confreg: configuration register value (string) - """ - - self._hypervisor.send("vm set_conf_reg {name} {confreg}".format(name=self._name, - confreg=confreg)) - - log.info("router {name} [id={id}]: confreg updated from {old_confreg} to {new_confreg}".format(name=self._name, - id=self._id, - old_confreg=self._confreg, - new_confreg=confreg)) - self._confreg = confreg - - @property - def console(self): - """ - Returns the TCP console port. - - :returns: console port (integer) - """ - - return self._console - - @console.setter - def console(self, console): - """ - Sets the TCP console port. - - :param console: console port (integer) - """ - - if console == self._console: - return - - if console in self._allocated_console_ports: - raise DynamipsError("Console port {} is already used by another router".format(console)) - - self._hypervisor.send("vm set_con_tcp_port {name} {console}".format(name=self._name, - console=console)) - - log.info("router {name} [id={id}]: console port updated from {old_console} to {new_console}".format(name=self._name, - id=self._id, - old_console=self._console, - new_console=console)) - self._allocated_console_ports.remove(self._console) - self._console = console - self._allocated_console_ports.append(self._console) - - @property - def aux(self): - """ - Returns the TCP auxiliary port. - - :returns: console auxiliary port (integer) - """ - - return self._aux - - @aux.setter - def aux(self, aux): - """ - Sets the TCP auxiliary port. - - :param aux: console auxiliary port (integer) - """ - - if aux == self._aux: - return - - if aux in self._allocated_aux_ports: - raise DynamipsError("Auxiliary console port {} is already used by another router".format(aux)) - - self._hypervisor.send("vm set_aux_tcp_port {name} {aux}".format(name=self._name, - aux=aux)) - - log.info("router {name} [id={id}]: aux port updated from {old_aux} to {new_aux}".format(name=self._name, - id=self._id, - old_aux=self._aux, - new_aux=aux)) - - self._allocated_aux_ports.remove(self._aux) - self._aux = aux - self._allocated_aux_ports.append(self._aux) - - def get_cpu_info(self, cpu_id=0): - """ - Shows info about the CPU identified by cpu_id. - The boot CPU (which is typically the only CPU) has ID 0. - - :returns: ? (could not test) - """ - - # FIXME: nothing returned by Dynamips. - return self._hypervisor.send("vm cpu_info {name} {cpu_id}".format(name=self._name, - cpu_id=cpu_id)) - - def get_cpu_usage(self, cpu_id=0): - """ - Shows cpu usage in seconds, "cpu_id" is ignored. - - :returns: cpu usage in seconds - """ - - return int(self._hypervisor.send("vm cpu_usage {name} {cpu_id}".format(name=self._name, - cpu_id=cpu_id))[0]) - - def send_console_msg(self, message): - """ - Sends a message to the console. - - :param message: message to send to the console - """ - - self._hypervisor.send("vm send_con_msg {name} {message}".format(name=self._name, - message=message)) - - def send_aux_msg(self, message): - """ - Sends a message to the auxiliary console. - - :param message: message to send to the auxiliary console - """ - - self._hypervisor.send("vm send_aux_msg {name} {message}".format(name=self._name, - message=message)) - - @property - def mac_addr(self): - """ - Returns the MAC address. - - :returns: the MAC address (hexadecimal format: hh:hh:hh:hh:hh:hh) - """ - - return self._mac_addr - - @mac_addr.setter - def mac_addr(self, mac_addr): - """ - Sets the MAC address. - - :param mac_addr: a MAC address (hexadecimal format: hh:hh:hh:hh:hh:hh) - """ - - self._hypervisor.send("{platform} set_mac_addr {name} {mac_addr}".format(platform=self._platform, - name=self._name, - mac_addr=mac_addr)) - - log.info("router {name} [id={id}]: MAC address updated from {old_mac} to {new_mac}".format(name=self._name, - id=self._id, - old_mac=self._mac_addr, - new_mac=mac_addr)) - self._mac_addr = mac_addr - - @property - def system_id(self): - """ - Returns the system ID. - - :returns: the system ID (also called board processor ID) - """ - - return self._system_id - - @system_id.setter - def system_id(self, system_id): - """ - Sets the system ID. - - :param system_id: a system ID (also called board processor ID) - """ - - self._hypervisor.send("{platform} set_system_id {name} {system_id}".format(platform=self._platform, - name=self._name, - system_id=system_id)) - - log.info("router {name} [id={id}]: system ID updated from {old_id} to {new_id}".format(name=self._name, - id=self._id, - old_id=self._system_id, - new_id=system_id)) - self._system_id = system_id - - def get_hardware_info(self): - """ - Get some hardware info about this router. - - :returns: ? (could not test) - """ - - # FIXME: nothing returned by Dynamips. - return (self._hypervisor.send("{platform} show_hardware {name}".format(platform=self._platform, - name=self._name))) - - def get_cpu_usage(self): - """ - Returns the CPU usage. - - :return: CPU usage in percent - """ - - return int(self._hypervisor.send("vm cpu_usage {name} 0".format(name=self._name))[0]) - - def get_slot_bindings(self): - """ - Returns slot bindings. - - :returns: slot bindings (adapter names) list - """ - - return self._hypervisor.send("vm slot_bindings {}".format(self._name)) - - def slot_add_binding(self, slot_id, adapter): - """ - Adds a slot binding (a module into a slot). - - :param slot_id: slot ID - :param adapter: device to add in the corresponding slot - """ - - try: - slot = self._slots[slot_id] - except IndexError: - raise DynamipsError("Slot {slot_id} doesn't exist on router {name}".format(name=self._name, - slot_id=slot_id)) - - if slot is not None: - current_adapter = slot - raise DynamipsError("Slot {slot_id} is already occupied by adapter {adapter} on router {name}".format(name=self._name, - slot_id=slot_id, - adapter=current_adapter)) - - # Only c7200, c3600 and c3745 (NM-4T only) support new adapter while running - if self.is_running() and not ((self._platform == 'c7200' and not str(adapter).startswith('C7200')) - and not (self._platform == 'c3600' and self.chassis == '3660') - and not (self._platform == 'c3745' and adapter == 'NM-4T')): - raise DynamipsError("Adapter {adapter} cannot be added while router {name} is running".format(adapter=adapter, - name=self._name)) - - self._hypervisor.send("vm slot_add_binding {name} {slot_id} 0 {adapter}".format(name=self._name, - slot_id=slot_id, - adapter=adapter)) - - log.info("router {name} [id={id}]: adapter {adapter} inserted into slot {slot_id}".format(name=self._name, - id=self._id, - adapter=adapter, - slot_id=slot_id)) - - self._slots[slot_id] = adapter - - # Generate an OIR event if the router is running - if self.is_running(): - - self._hypervisor.send("vm slot_oir_start {name} {slot_id} 0".format(name=self._name, - slot_id=slot_id)) - - log.info("router {name} [id={id}]: OIR start event sent to slot {slot_id}".format(name=self._name, - id=self._id, - slot_id=slot_id)) - - def slot_remove_binding(self, slot_id): - """ - Removes a slot binding (a module from a slot). - - :param slot_id: slot ID - """ - - try: - adapter = self._slots[slot_id] - except IndexError: - raise DynamipsError("Slot {slot_id} doesn't exist on router {name}".format(name=self._name, - slot_id=slot_id)) - - if adapter is None: - raise DynamipsError("No adapter in slot {slot_id} on router {name}".format(name=self._name, - slot_id=slot_id)) - - # Only c7200, c3600 and c3745 (NM-4T only) support to remove adapter while running - if self.is_running() and not ((self._platform == 'c7200' and not str(adapter).startswith('C7200')) - and not (self._platform == 'c3600' and self.chassis == '3660') - and not (self._platform == 'c3745' and adapter == 'NM-4T')): - raise DynamipsError("Adapter {adapter} cannot be removed while router {name} is running".format(adapter=adapter, - name=self._name)) - - # Generate an OIR event if the router is running - if self.is_running(): - - self._hypervisor.send("vm slot_oir_stop {name} {slot_id} 0".format(name=self._name, - slot_id=slot_id)) - - log.info("router {name} [id={id}]: OIR stop event sent to slot {slot_id}".format(name=self._name, - id=self._id, - slot_id=slot_id)) - - self._hypervisor.send("vm slot_remove_binding {name} {slot_id} 0".format(name=self._name, - slot_id=slot_id)) - - log.info("router {name} [id={id}]: adapter {adapter} removed from slot {slot_id}".format(name=self._name, - id=self._id, - adapter=adapter, - slot_id=slot_id)) - self._slots[slot_id] = None - - def install_wic(self, wic_slot_id, wic): - """ - Installs a WIC adapter into this router. - - :param wic_slot_id: WIC slot ID - :param wic: WIC to be installed - """ - - # WICs are always installed on adapters in slot 0 - slot_id = 0 - - # Do not check if slot has an adapter because adapters with WICs interfaces - # must be inserted by default in the router and cannot be removed. - adapter = self._slots[slot_id] - - if wic_slot_id > len(adapter.wics) - 1: - raise DynamipsError("WIC slot {wic_slot_id} doesn't exist".format(name=self._name, - wic_slot_id=wic_slot_id)) - - if not adapter.wic_slot_available(wic_slot_id): - raise DynamipsError("WIC slot {wic_slot_id} is already occupied by another WIC".format(name=self._name, - wic_slot_id=wic_slot_id)) - - # Dynamips WICs slot IDs start on a multiple of 16 - # WIC1 = 16, WIC2 = 32 and WIC3 = 48 - internal_wic_slot_id = 16 * (wic_slot_id + 1) - self._hypervisor.send("vm slot_add_binding {name} {slot_id} {wic_slot_id} {wic}".format(name=self._name, - slot_id=slot_id, - wic_slot_id=internal_wic_slot_id, - wic=wic)) - - log.info("router {name} [id={id}]: {wic} inserted into WIC slot {wic_slot_id}".format(name=self._name, - id=self._id, - wic=wic, - wic_slot_id=wic_slot_id)) - - adapter.install_wic(wic_slot_id, wic) - - def uninstall_wic(self, wic_slot_id): - """ - Uninstalls a WIC adapter from this router. - - :param wic_slot_id: WIC slot ID - """ - - # WICs are always installed on adapters in slot 0 - slot_id = 0 - - # Do not check if slot has an adapter because adapters with WICs interfaces - # must be inserted by default in the router and cannot be removed. - adapter = self._slots[slot_id] - - if wic_slot_id > len(adapter.wics) - 1: - raise DynamipsError("WIC slot {wic_slot_id} doesn't exist".format(name=self._name, - wic_slot_id=wic_slot_id)) - - if adapter.wic_slot_available(wic_slot_id): - raise DynamipsError("No WIC is installed in WIC slot {wic_slot_id}".format(name=self._name, - wic_slot_id=wic_slot_id)) - # Dynamips WICs slot IDs start on a multiple of 16 - # WIC1 = 16, WIC2 = 32 and WIC3 = 48 - internal_wic_slot_id = 16 * (wic_slot_id + 1) - self._hypervisor.send("vm slot_remove_binding {name} {slot_id} {wic_slot_id}".format(name=self._name, - slot_id=slot_id, - wic_slot_id=internal_wic_slot_id)) - - log.info("router {name} [id={id}]: {wic} removed from WIC slot {wic_slot_id}".format(name=self._name, - id=self._id, - wic=adapter.wics[wic_slot_id], - wic_slot_id=wic_slot_id)) - adapter.uninstall_wic(wic_slot_id) - - def get_slot_nio_bindings(self, slot_id): - """ - Returns slot NIO bindings. - - :param slot_id: slot ID - - :returns: list of NIO bindings - """ - - return (self._hypervisor.send("vm slot_nio_bindings {name} {slot_id}".format(name=self._name, - slot_id=slot_id))) - - def slot_add_nio_binding(self, slot_id, port_id, nio): - """ - Adds a slot NIO binding. - - :param slot_id: slot ID - :param port_id: port ID - :param nio: NIO instance to add to the slot/port - """ - - try: - adapter = self._slots[slot_id] - except IndexError: - raise DynamipsError("Slot {slot_id} doesn't exist on router {name}".format(name=self._name, - slot_id=slot_id)) - if not adapter.port_exists(port_id): - raise DynamipsError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, - port_id=port_id)) - - self._hypervisor.send("vm slot_add_nio_binding {name} {slot_id} {port_id} {nio}".format(name=self._name, - slot_id=slot_id, - port_id=port_id, - nio=nio)) - - log.info("router {name} [id={id}]: NIO {nio_name} bound to port {slot_id}/{port_id}".format(name=self._name, - id=self._id, - nio_name=nio.name, - slot_id=slot_id, - port_id=port_id)) - - self.slot_enable_nio(slot_id, port_id) - adapter.add_nio(port_id, nio) - - def slot_remove_nio_binding(self, slot_id, port_id): - """ - Removes a slot NIO binding. - - :param slot_id: slot ID - :param port_id: port ID - - :returns: removed NIO instance - """ - - try: - adapter = self._slots[slot_id] - except IndexError: - raise DynamipsError("Slot {slot_id} doesn't exist on router {name}".format(name=self._name, - slot_id=slot_id)) - if not adapter.port_exists(port_id): - raise DynamipsError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, - port_id=port_id)) - - self.slot_disable_nio(slot_id, port_id) - self._hypervisor.send("vm slot_remove_nio_binding {name} {slot_id} {port_id}".format(name=self._name, - slot_id=slot_id, - port_id=port_id)) - - nio = adapter.get_nio(port_id) - adapter.remove_nio(port_id) - - log.info("router {name} [id={id}]: NIO {nio_name} removed from port {slot_id}/{port_id}".format(name=self._name, - id=self._id, - nio_name=nio.name, - slot_id=slot_id, - port_id=port_id)) - - return nio - - def slot_enable_nio(self, slot_id, port_id): - """ - Enables a slot NIO binding. - - :param slot_id: slot ID - :param port_id: port ID - """ - - if self.is_running(): # running router - self._hypervisor.send("vm slot_enable_nio {name} {slot_id} {port_id}".format(name=self._name, - slot_id=slot_id, - port_id=port_id)) - - log.info("router {name} [id={id}]: NIO enabled on port {slot_id}/{port_id}".format(name=self._name, - id=self._id, - slot_id=slot_id, - port_id=port_id)) - - def slot_disable_nio(self, slot_id, port_id): - """ - Disables a slot NIO binding. - - :param slot_id: slot ID - :param port_id: port ID - """ - - if self.is_running(): # running router - self._hypervisor.send("vm slot_disable_nio {name} {slot_id} {port_id}".format(name=self._name, - slot_id=slot_id, - port_id=port_id)) - - log.info("router {name} [id={id}]: NIO disabled on port {slot_id}/{port_id}".format(name=self._name, - id=self._id, - slot_id=slot_id, - port_id=port_id)) - - def start_capture(self, slot_id, port_id, output_file, data_link_type="DLT_EN10MB"): - """ - Starts a packet capture. - - :param slot_id: slot ID - :param port_id: port ID - :param output_file: PCAP destination file for the capture - :param data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB - """ - - try: - adapter = self._slots[slot_id] - except IndexError: - raise DynamipsError("Slot {slot_id} doesn't exist on router {name}".format(name=self._name, - slot_id=slot_id)) - if not adapter.port_exists(port_id): - raise DynamipsError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, - port_id=port_id)) - - data_link_type = data_link_type.lower() - if data_link_type.startswith("dlt_"): - data_link_type = data_link_type[4:] - - nio = adapter.get_nio(port_id) - - if nio.input_filter[0] is not None and nio.output_filter[0] is not None: - raise DynamipsError("Port {port_id} has already a filter applied on {adapter}".format(adapter=adapter, - port_id=port_id)) - - try: - os.makedirs(os.path.dirname(output_file)) - except FileExistsError: - pass - except OSError as e: - raise DynamipsError("Could not create captures directory {}".format(e)) - - nio.bind_filter("both", "capture") - nio.setup_filter("both", '{} "{}"'.format(data_link_type, output_file)) - - log.info("router {name} [id={id}]: starting packet capture on port {slot_id}/{port_id}".format(name=self._name, - id=self._id, - nio_name=nio.name, - slot_id=slot_id, - port_id=port_id)) - - def stop_capture(self, slot_id, port_id): - """ - Stops a packet capture. - - :param slot_id: slot ID - :param port_id: port ID - """ - - try: - adapter = self._slots[slot_id] - except IndexError: - raise DynamipsError("Slot {slot_id} doesn't exist on router {name}".format(name=self._name, - slot_id=slot_id)) - if not adapter.port_exists(port_id): - raise DynamipsError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, - port_id=port_id)) - - nio = adapter.get_nio(port_id) - nio.unbind_filter("both") - - log.info("router {name} [id={id}]: stopping packet capture on port {slot_id}/{port_id}".format(name=self._name, - id=self._id, - nio_name=nio.name, - slot_id=slot_id, - port_id=port_id)) - - def _create_slots(self, numslots): - """ - Creates the appropriate number of slots for this router. - - :param numslots: number of slots to create - """ - - self._slots = numslots * [None] - - @property - def slots(self): - """ - Returns the slots for this router. - - :return: slot list - """ - - return self._slots diff --git a/gns3server/old_modules/dynamips/schemas/__init__.py b/gns3server/old_modules/dynamips/schemas/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/gns3server/old_modules/dynamips/schemas/atmsw.py b/gns3server/old_modules/dynamips/schemas/atmsw.py deleted file mode 100644 index 37669478..00000000 --- a/gns3server/old_modules/dynamips/schemas/atmsw.py +++ /dev/null @@ -1,322 +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 . - -ATMSW_CREATE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to create a new ATM switch instance", - "type": "object", - "properties": { - "name": { - "description": "ATM switch name", - "type": "string", - "minLength": 1, - }, - }, - "additionalProperties": False, - "required": ["name"] -} - -ATMSW_DELETE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to delete an ATM switch instance", - "type": "object", - "properties": { - "id": { - "description": "ATM switch instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -ATMSW_UPDATE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to update an ATM switch instance", - "type": "object", - "properties": { - "id": { - "description": "ATM switch instance ID", - "type": "integer" - }, - "name": { - "description": "ATM switch name", - "type": "string", - "minLength": 1, - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -ATMSW_ALLOCATE_UDP_PORT_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to allocate an UDP port for an ATM switch instance", - "type": "object", - "properties": { - "id": { - "description": "ATM switch instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the ATM switch instance", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id", "port_id"] -} - -ATMSW_ADD_NIO_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to add a NIO for an ATM switch 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": "ATM switch instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the ATM switch instance", - "type": "integer" - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 1, - }, - "mappings": { - "type": "object", - }, - "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", "port_id", "mappings", "nio"], -} - -ATMSW_DELETE_NIO_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to delete a NIO for an ATM switch instance", - "type": "object", - "properties": { - "id": { - "description": "ATM switch instance ID", - "type": "integer" - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 1, - }, - }, - "additionalProperties": False, - "required": ["id", "port"] -} - -ATMSW_START_CAPTURE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to start a packet capture on an ATM switch instance port", - "type": "object", - "properties": { - "id": { - "description": "ATM switch instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the ATM switch instance", - "type": "integer" - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 1, - }, - "capture_file_name": { - "description": "Capture file name", - "type": "string", - "minLength": 1, - }, - "data_link_type": { - "description": "PCAP data link type", - "type": "string", - "minLength": 1, - }, - }, - "additionalProperties": False, - "required": ["id", "port_id", "port", "capture_file_name"] -} - -ATMSW_STOP_CAPTURE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to stop a packet capture on an ATM switch instance port", - "type": "object", - "properties": { - "id": { - "description": "ATM switch instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the ATM switch instance", - "type": "integer" - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 1, - }, - }, - "additionalProperties": False, - "required": ["id", "port_id", "port"] -} diff --git a/gns3server/old_modules/dynamips/schemas/ethhub.py b/gns3server/old_modules/dynamips/schemas/ethhub.py deleted file mode 100644 index 1002a696..00000000 --- a/gns3server/old_modules/dynamips/schemas/ethhub.py +++ /dev/null @@ -1,319 +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 . - -ETHHUB_CREATE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to create a new Ethernet hub instance", - "type": "object", - "properties": { - "name": { - "description": "Ethernet hub name", - "type": "string", - "minLength": 1, - }, - }, - "additionalProperties": False, - "required": ["name"] -} - -ETHHUB_DELETE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to delete an Ethernet hub instance", - "type": "object", - "properties": { - "id": { - "description": "Ethernet hub instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -ETHHUB_UPDATE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to update an Ethernet hub instance", - "type": "object", - "properties": { - "id": { - "description": "Ethernet hub instance ID", - "type": "integer" - }, - "name": { - "description": "Ethernet hub name", - "type": "string", - "minLength": 1, - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -ETHHUB_ALLOCATE_UDP_PORT_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to allocate an UDP port for an Ethernet hub instance", - "type": "object", - "properties": { - "id": { - "description": "Ethernet hub instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the Ethernet hub instance", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id", "port_id"] -} - -ETHHUB_ADD_NIO_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to add a NIO for an Ethernet hub 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": "Ethernet hub instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the Ethernet hub instance", - "type": "integer" - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 1, - }, - "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"] -} - -ETHHUB_DELETE_NIO_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to delete a NIO for an Ethernet hub instance", - "type": "object", - "properties": { - "id": { - "description": "Ethernet hub instance ID", - "type": "integer" - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 1, - }, - }, - "additionalProperties": False, - "required": ["id", "port"] -} - -ETHHUB_START_CAPTURE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to start a packet capture on an Ethernet hub instance port", - "type": "object", - "properties": { - "id": { - "description": "Ethernet hub instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the Ethernet hub instance", - "type": "integer" - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 1, - }, - "capture_file_name": { - "description": "Capture file name", - "type": "string", - "minLength": 1, - }, - "data_link_type": { - "description": "PCAP data link type", - "type": "string", - "minLength": 1, - }, - }, - "additionalProperties": False, - "required": ["id", "port_id", "port", "capture_file_name"] -} - -ETHHUB_STOP_CAPTURE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to stop a packet capture on an Ethernet hub instance port", - "type": "object", - "properties": { - "id": { - "description": "Ethernet hub instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the Ethernet hub instance", - "type": "integer" - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 1, - }, - }, - "additionalProperties": False, - "required": ["id", "port_id", "port"] -} diff --git a/gns3server/old_modules/dynamips/schemas/ethsw.py b/gns3server/old_modules/dynamips/schemas/ethsw.py deleted file mode 100644 index 33559f28..00000000 --- a/gns3server/old_modules/dynamips/schemas/ethsw.py +++ /dev/null @@ -1,348 +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 . - -ETHSW_CREATE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to create a new Ethernet switch instance", - "type": "object", - "properties": { - "name": { - "description": "Ethernet switch name", - "type": "string", - "minLength": 1, - }, - }, - "additionalProperties": False, - "required": ["name"] -} - -ETHSW_DELETE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to delete an Ethernet switch instance", - "type": "object", - "properties": { - "id": { - "description": "Ethernet switch instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -# TODO: ports {'1': {'vlan': 1, 'type': 'qinq'} -ETHSW_UPDATE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to update an Ethernet switch instance", - "type": "object", - "properties": { - "id": { - "description": "Ethernet switch instance ID", - "type": "integer" - }, - "name": { - "description": "Ethernet switch name", - "type": "string", - "minLength": 1, - }, - # "ports": { - # "type": "object", - # "properties": { - # "type": { - # "description": "Port type", - # "enum": ["access", "dot1q", "qinq"], - # }, - # "vlan": { - # "description": "VLAN number", - # "type": "integer", - # "minimum": 1 - # }, - # }, - # }, - }, - #"additionalProperties": False, - "required": ["id"] -} - -ETHSW_ALLOCATE_UDP_PORT_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to allocate an UDP port for an Ethernet switch instance", - "type": "object", - "properties": { - "id": { - "description": "Ethernet switch instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the Ethernet switch instance", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id", "port_id"] -} - -ETHSW_ADD_NIO_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to add a NIO for an Ethernet switch 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": "Ethernet switch instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the Ethernet switch instance", - "type": "integer" - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 1, - }, - "port_type": { - "description": "Port type", - "enum": ["access", "dot1q", "qinq"], - }, - "vlan": { - "description": "VLAN number", - "type": "integer", - "minimum": 1 - }, - "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", "port_type", "vlan", "nio"], - - "dependencies": { - "port_type": ["vlan"], - "vlan": ["port_type"] - } -} - -ETHSW_DELETE_NIO_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to delete a NIO for an Ethernet switch instance", - "type": "object", - "properties": { - "id": { - "description": "Ethernet switch instance ID", - "type": "integer" - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 1, - }, - }, - "additionalProperties": False, - "required": ["id", "port"] -} - -ETHSW_START_CAPTURE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to start a packet capture on an Ethernet switch instance port", - "type": "object", - "properties": { - "id": { - "description": "Ethernet switch instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the Ethernet switch instance", - "type": "integer" - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 1, - }, - "capture_file_name": { - "description": "Capture file name", - "type": "string", - "minLength": 1, - }, - "data_link_type": { - "description": "PCAP data link type", - "type": "string", - "minLength": 1, - }, - }, - "additionalProperties": False, - "required": ["id", "port_id", "port", "capture_file_name"] -} - -ETHSW_STOP_CAPTURE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to stop a packet capture on an Ethernet switch instance port", - "type": "object", - "properties": { - "id": { - "description": "Ethernet switch instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the Ethernet switch instance", - "type": "integer" - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 1, - }, - }, - "additionalProperties": False, - "required": ["id", "port_id", "port"] -} diff --git a/gns3server/old_modules/dynamips/schemas/frsw.py b/gns3server/old_modules/dynamips/schemas/frsw.py deleted file mode 100644 index 835e47a7..00000000 --- a/gns3server/old_modules/dynamips/schemas/frsw.py +++ /dev/null @@ -1,322 +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 . - -FRSW_CREATE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to create a new Frame relay switch instance", - "type": "object", - "properties": { - "name": { - "description": "Frame relay switch name", - "type": "string", - "minLength": 1, - }, - }, - "additionalProperties": False, - "required": ["name"] -} - -FRSW_DELETE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to delete a Frame relay switch instance", - "type": "object", - "properties": { - "id": { - "description": "Frame relay switch instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -FRSW_UPDATE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to update a Frame relay switch instance", - "type": "object", - "properties": { - "id": { - "description": "Frame relay switch instance ID", - "type": "integer" - }, - "name": { - "description": "Frame relay switch name", - "type": "string", - "minLength": 1, - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -FRSW_ALLOCATE_UDP_PORT_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to allocate an UDP port for a Frame relay switch instance", - "type": "object", - "properties": { - "id": { - "description": "Frame relay switch instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the Frame relay switch instance", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id", "port_id"] -} - -FRSW_ADD_NIO_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to add a NIO for a Frame relay switch 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": "Frame relay switch instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the Frame relay switch instance", - "type": "integer" - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 1, - }, - "mappings": { - "type": "object", - }, - "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", "port_id", "mappings", "nio"], -} - -FRSW_DELETE_NIO_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to delete a NIO for a Frame relay switch instance", - "type": "object", - "properties": { - "id": { - "description": "Frame relay switch instance ID", - "type": "integer" - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 1, - }, - }, - "additionalProperties": False, - "required": ["id", "port"] -} - -FRSW_START_CAPTURE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to start a packet capture on a Frame relay switch instance port", - "type": "object", - "properties": { - "id": { - "description": "Frame relay switch instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the Frame relay instance", - "type": "integer" - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 1, - }, - "capture_file_name": { - "description": "Capture file name", - "type": "string", - "minLength": 1, - }, - "data_link_type": { - "description": "PCAP data link type", - "type": "string", - "minLength": 1, - }, - }, - "additionalProperties": False, - "required": ["id", "port_id", "port", "capture_file_name"] -} - -FRSW_STOP_CAPTURE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to stop a packet capture on a Frame relay switch instance port", - "type": "object", - "properties": { - "id": { - "description": "Frame relay switch instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the Frame relay instance", - "type": "integer" - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 1, - }, - }, - "additionalProperties": False, - "required": ["id", "port_id", "port"] -} diff --git a/gns3server/old_modules/dynamips/schemas/vm.py b/gns3server/old_modules/dynamips/schemas/vm.py deleted file mode 100644 index adb380e4..00000000 --- a/gns3server/old_modules/dynamips/schemas/vm.py +++ /dev/null @@ -1,719 +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 . - -VM_CREATE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to create a new VM instance", - "type": "object", - "properties": { - "name": { - "description": "Router name", - "type": "string", - "minLength": 1, - }, - "router_id": { - "description": "VM/router instance ID", - "type": "integer" - }, - "platform": { - "description": "router platform", - "type": "string", - "minLength": 1, - "pattern": "^c[0-9]{4}$" - }, - "chassis": { - "description": "router chassis model", - "type": "string", - "minLength": 1, - "pattern": "^[0-9]{4}(XM)?$" - }, - "image": { - "description": "path to the IOS image file", - "type": "string", - "minLength": 1 - }, - "ram": { - "description": "amount of RAM in MB", - "type": "integer" - }, - "console": { - "description": "console TCP port", - "type": "integer", - "minimum": 1, - "maximum": 65535 - }, - "aux": { - "description": "auxiliary console TCP port", - "type": "integer", - "minimum": 1, - "maximum": 65535 - }, - "mac_addr": { - "description": "base MAC address", - "type": "string", - "minLength": 1, - "pattern": "^([0-9a-fA-F]{4}\\.){2}[0-9a-fA-F]{4}$" - }, - "cloud_path": { - "description": "Path to the image in the cloud object store", - "type": "string", - } - }, - "additionalProperties": False, - "required": ["name", "platform", "image", "ram"] -} - -VM_DELETE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to delete a VM instance", - "type": "object", - "properties": { - "id": { - "description": "VM instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -VM_START_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to start a VM instance", - "type": "object", - "properties": { - "id": { - "description": "VM instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -VM_STOP_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to stop a VM instance", - "type": "object", - "properties": { - "id": { - "description": "VM instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -VM_SUSPEND_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to suspend a VM instance", - "type": "object", - "properties": { - "id": { - "description": "VM instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -VM_RELOAD_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to reload a VM instance", - "type": "object", - "properties": { - "id": { - "description": "VM instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -# TODO: improve platform specific properties (dependencies?) -VM_UPDATE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to update a VM instance", - "type": "object", - "properties": { - "id": { - "description": "VM instance ID", - "type": "integer" - }, - "name": { - "description": "Router name", - "type": "string", - "minLength": 1, - }, - "platform": { - "description": "platform", - "type": "string", - "minLength": 1, - "pattern": "^c[0-9]{4}$" - }, - "image": { - "description": "path to the IOS image", - "type": "string", - "minLength": 1, - }, - "startup_config": { - "description": "path to the IOS startup configuration file", - "type": "string", - "minLength": 1, - }, - "private_config": { - "description": "path to the IOS private configuration file", - "type": "string", - "minLength": 1, - }, - "ram": { - "description": "amount of RAM in MB", - "type": "integer" - }, - "nvram": { - "description": "amount of NVRAM in KB", - "type": "integer" - }, - "mmap": { - "description": "MMAP feature", - "type": "boolean" - }, - "sparsemem": { - "description": "sparse memory feature", - "type": "boolean" - }, - "clock_divisor": { - "description": "clock divisor", - "type": "integer" - }, - "idlepc": { - "description": "Idle-PC value", - "type": "string", - "pattern": "^(0x[0-9a-fA-F]+)?$" - }, - "idlemax": { - "description": "idlemax value", - "type": "integer", - }, - "idlesleep": { - "description": "idlesleep value", - "type": "integer", - }, - "exec_area": { - "description": "exec area value", - "type": "integer", - }, - "jit_sharing_group": { - "description": "JIT sharing group", - "type": "integer", - }, - "disk0": { - "description": "disk0 size in MB", - "type": "integer" - }, - "disk1": { - "description": "disk1 size in MB", - "type": "integer" - }, - "confreg": { - "description": "configuration register", - "type": "string", - "minLength": 1, - "pattern": "^0x[0-9a-fA-F]{4}$" - }, - "console": { - "description": "console TCP port", - "type": "integer", - "minimum": 1, - "maximum": 65535 - }, - "aux": { - "description": "auxiliary console TCP port", - "type": "integer", - "minimum": 1, - "maximum": 65535 - }, - "mac_addr": { - "description": "base MAC address", - "type": "string", - "minLength": 1, - "pattern": "^([0-9a-fA-F]{4}\\.){2}[0-9a-fA-F]{4}$" - }, - "system_id": { - "description": "system ID", - "type": "string", - "minLength": 1, - }, - "slot0": { - "description": "Network module slot 0", - "oneOf": [ - {"type": "string"}, - {"type": "null"} - ] - }, - "slot1": { - "description": "Network module slot 1", - "oneOf": [ - {"type": "string"}, - {"type": "null"} - ] - }, - "slot2": { - "description": "Network module slot 2", - "oneOf": [ - {"type": "string"}, - {"type": "null"} - ] - }, - "slot3": { - "description": "Network module slot 3", - "oneOf": [ - {"type": "string"}, - {"type": "null"} - ] - }, - "slot4": { - "description": "Network module slot 4", - "oneOf": [ - {"type": "string"}, - {"type": "null"} - ] - }, - "slot5": { - "description": "Network module slot 5", - "oneOf": [ - {"type": "string"}, - {"type": "null"} - ] - }, - "slot6": { - "description": "Network module slot 6", - "oneOf": [ - {"type": "string"}, - {"type": "null"} - ] - }, - "wic0": { - "description": "Network module WIC slot 0", - "oneOf": [ - {"type": "string"}, - {"type": "null"} - ] - }, - "wic1": { - "description": "Network module WIC slot 0", - "oneOf": [ - {"type": "string"}, - {"type": "null"} - ] - }, - "wic2": { - "description": "Network module WIC slot 0", - "oneOf": [ - {"type": "string"}, - {"type": "null"} - ] - }, - "startup_config_base64": { - "description": "startup configuration base64 encoded", - "type": "string" - }, - "private_config_base64": { - "description": "private configuration base64 encoded", - "type": "string" - }, - # C7200 properties - "npe": { - "description": "NPE model", - "enum": ["npe-100", - "npe-150", - "npe-175", - "npe-200", - "npe-225", - "npe-300", - "npe-400", - "npe-g2"] - }, - "midplane": { - "description": "Midplane model", - "enum": ["std", "vxr"] - }, - "sensors": { - "description": "Temperature sensors", - "type": "array" - }, - "power_supplies": { - "description": "Power supplies status", - "type": "array" - }, - # I/O memory property for all platforms but C7200 - "iomem": { - "description": "I/O memory percentage", - "type": "integer", - "minimum": 0, - "maximum": 100 - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -VM_START_CAPTURE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to start a packet capture on a VM instance port", - "type": "object", - "properties": { - "id": { - "description": "VM instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the VM instance", - "type": "integer" - }, - "slot": { - "description": "Slot number", - "type": "integer", - "minimum": 0, - "maximum": 6 - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 0, - "maximum": 49 # maximum is 16 for regular port numbers, WICs port numbers start at 16, 32 or 48 - }, - "capture_file_name": { - "description": "Capture file name", - "type": "string", - "minLength": 1, - }, - "data_link_type": { - "description": "PCAP data link type", - "type": "string", - "minLength": 1, - }, - }, - "additionalProperties": False, - "required": ["id", "port_id", "slot", "port", "capture_file_name"] -} - -VM_STOP_CAPTURE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to stop a packet capture on a VM instance port", - "type": "object", - "properties": { - "id": { - "description": "VM instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the VM instance", - "type": "integer" - }, - "slot": { - "description": "Slot number", - "type": "integer", - "minimum": 0, - "maximum": 6 - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 0, - "maximum": 49 # maximum is 16 for regular port numbers, WICs port numbers start at 16, 32 or 48 - }, - }, - "additionalProperties": False, - "required": ["id", "port_id", "slot", "port"] -} - -VM_SAVE_CONFIG_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to save the configs for VM instance", - "type": "object", - "properties": { - "id": { - "description": "VM instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -VM_EXPORT_CONFIG_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to export the configs for VM instance", - "type": "object", - "properties": { - "id": { - "description": "VM instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -VM_IDLEPCS_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to calculate or show Idle-PCs for VM instance", - "type": "object", - "properties": { - "id": { - "description": "VM instance ID", - "type": "integer" - }, - "compute": { - "description": "indicates to compute new Idle-PC values", - "type": "boolean" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -VM_AUTO_IDLEPC_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request an auto Idle-PC calculation for this VM instance", - "type": "object", - "properties": { - "id": { - "description": "VM instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -VM_ALLOCATE_UDP_PORT_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to allocate an UDP port for a VM instance", - "type": "object", - "properties": { - "id": { - "description": "VM instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the VM instance", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id", "port_id"] -} - -VM_ADD_NIO_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to add a NIO for a VM 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": "VM instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the VM instance", - "type": "integer" - }, - "slot": { - "description": "Slot number", - "type": "integer", - "minimum": 0, - "maximum": 6 - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 0, - "maximum": 49 # maximum is 16 for regular port numbers, WICs port numbers start at 16, 32 or 48 - }, - "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", "slot", "port", "nio"] -} - -VM_DELETE_NIO_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to delete a NIO for a VM instance", - "type": "object", - "properties": { - "id": { - "description": "VM instance ID", - "type": "integer" - }, - "slot": { - "description": "Slot number", - "type": "integer", - "minimum": 0, - "maximum": 6 - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 0, - "maximum": 49 # maximum is 16 for regular port numbers, WICs port numbers start at 16, 32 or 48 - }, - }, - "additionalProperties": False, - "required": ["id", "slot", "port"] -} diff --git a/gns3server/schemas/dynamips.py b/gns3server/schemas/dynamips.py new file mode 100644 index 00000000..a707d0cc --- /dev/null +++ b/gns3server/schemas/dynamips.py @@ -0,0 +1,108 @@ +# -*- 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 . + + +ROUTER_CREATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to create a new Dynamips router instance", + "type": "object", + "properties": { + "name": { + "description": "Router name", + "type": "string", + "minLength": 1, + }, + "router_id": { + "description": "VM/router instance ID", + "type": "integer" + }, + "platform": { + "description": "router platform", + "type": "string", + "minLength": 1, + "pattern": "^c[0-9]{4}$" + }, + "chassis": { + "description": "router chassis model", + "type": "string", + "minLength": 1, + "pattern": "^[0-9]{4}(XM)?$" + }, + "image": { + "description": "path to the IOS image file", + "type": "string", + "minLength": 1 + }, + "ram": { + "description": "amount of RAM in MB", + "type": "integer" + }, + "console": { + "description": "console TCP port", + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "aux": { + "description": "auxiliary console TCP port", + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "mac_addr": { + "description": "base MAC address", + "type": "string", + "minLength": 1, + "pattern": "^([0-9a-fA-F]{4}\\.){2}[0-9a-fA-F]{4}$" + }, + "cloud_path": { + "description": "Path to the image in the cloud object store", + "type": "string", + } + }, + "additionalProperties": False, + "required": ["name", "platform", "image", "ram"] +} + +ROUTER_OBJECT_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Dynamips router instance", + "type": "object", + "properties": { + "name": { + "description": "Dynamips router instance name", + "type": "string", + "minLength": 1, + }, + "vm_id": { + "description": "Dynamips router instance UUID", + "type": "string", + "minLength": 36, + "maxLength": 36, + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + }, + "project_id": { + "description": "Project UUID", + "type": "string", + "minLength": 36, + "maxLength": 36, + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + }, + }, + "additionalProperties": False, + "required": ["name", "vm_id", "project_id"] +} From a6da2406a0990f6c1c9d7dc2ede28bf2eac97cb2 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 10 Feb 2015 17:24:38 +0100 Subject: [PATCH 216/485] Fix tests --- tests/modules/vpcs/test_vpcs_vm.py | 12 ++++++++++++ tests/test_main.py | 5 ++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/modules/vpcs/test_vpcs_vm.py b/tests/modules/vpcs/test_vpcs_vm.py index 14a1e84b..2848139f 100644 --- a/tests/modules/vpcs/test_vpcs_vm.py +++ b/tests/modules/vpcs/test_vpcs_vm.py @@ -79,6 +79,12 @@ def test_start(loop, vm): def test_stop(loop, vm): process = MagicMock() + + # Wait process kill success + future = asyncio.Future() + future.set_result(True) + process.wait.return_value = future + with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM._check_requirements", return_value=True): with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): nio = VPCS.instance().create_nio(vm.vpcs_path, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) @@ -92,6 +98,12 @@ def test_stop(loop, vm): def test_reload(loop, vm): process = MagicMock() + + # Wait process kill success + future = asyncio.Future() + future.set_result(True) + process.wait.return_value = future + with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM._check_requirements", return_value=True): with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): nio = VPCS.instance().create_nio(vm.vpcs_path, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) diff --git a/tests/test_main.py b/tests/test_main.py index ed9006b3..645fd87a 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -27,7 +27,10 @@ from gns3server.version import __version__ def test_locale_check(): - locale.setlocale(locale.LC_ALL, ("fr_FR")) + try: + locale.setlocale(locale.LC_ALL, ("fr_FR")) + except: # Locale is not available on the server + return main.locale_check() assert locale.getlocale() == ('fr_FR', 'UTF-8') From f0add73d801dfe1c82af5e6e7f7f1462dacb664f Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 10 Feb 2015 17:27:54 +0100 Subject: [PATCH 217/485] Cleanup --- old_tests/iou/test_iou_device.py | 41 -------------------------------- tests/test_main.py | 2 +- 2 files changed, 1 insertion(+), 42 deletions(-) delete mode 100644 old_tests/iou/test_iou_device.py diff --git a/old_tests/iou/test_iou_device.py b/old_tests/iou/test_iou_device.py deleted file mode 100644 index 58581de9..00000000 --- a/old_tests/iou/test_iou_device.py +++ /dev/null @@ -1,41 +0,0 @@ -from gns3server.modules.iou import IOUDevice -import os -import pytest - - -def no_iou(): - cwd = os.path.dirname(os.path.abspath(__file__)) - iou_path = os.path.join(cwd, "i86bi_linux-ipbase-ms-12.4.bin") - - if os.path.isfile(iou_path): - return False - else: - return True - - -@pytest.fixture(scope="session") -def iou(request): - - cwd = os.path.dirname(os.path.abspath(__file__)) - iou_path = os.path.join(cwd, "i86bi_linux-ipbase-ms-12.4.bin") - iou_device = IOUDevice("IOU1", iou_path, "/tmp") - iou_device.start() - request.addfinalizer(iou_device.delete) - return iou_device - - -@pytest.mark.skipif(no_iou(), reason="IOU Image not available") -def test_iou_is_started(iou): - - print(iou.command()) - assert iou.id == 1 # we should have only one IOU running! - assert iou.is_running() - - -@pytest.mark.skipif(no_iou(), reason="IOU Image not available") -def test_iou_restart(iou): - - iou.stop() - assert not iou.is_running() - iou.start() - assert iou.is_running() diff --git a/tests/test_main.py b/tests/test_main.py index 645fd87a..cdf71631 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -29,7 +29,7 @@ def test_locale_check(): try: locale.setlocale(locale.LC_ALL, ("fr_FR")) - except: # Locale is not available on the server + except: # Locale is not available on the server return main.locale_check() assert locale.getlocale() == ('fr_FR', 'UTF-8') From 37945585b961df17a9fb7047315b5ff04a34dc8a Mon Sep 17 00:00:00 2001 From: grossmj Date: Tue, 10 Feb 2015 21:50:02 -0700 Subject: [PATCH 218/485] New Dynamips integration part 2 --- gns3server/handlers/dynamips_handler.py | 3 +- gns3server/modules/dynamips/__init__.py | 5 +- gns3server/modules/dynamips/dynamips_vm.py | 50 ++++ gns3server/modules/dynamips/nodes/c1700.py | 82 +++---- gns3server/modules/dynamips/nodes/c2600.py | 82 +++---- gns3server/modules/dynamips/nodes/c2691.py | 58 ++--- gns3server/modules/dynamips/nodes/c3600.py | 82 +++---- gns3server/modules/dynamips/nodes/c3725.py | 58 ++--- gns3server/modules/dynamips/nodes/c3745.py | 58 ++--- gns3server/modules/dynamips/nodes/c7200.py | 141 ++++++------ gns3server/modules/dynamips/nodes/router.py | 238 ++++++++++---------- gns3server/modules/vpcs/vpcs_vm.py | 4 +- 12 files changed, 397 insertions(+), 464 deletions(-) create mode 100644 gns3server/modules/dynamips/dynamips_vm.py diff --git a/gns3server/handlers/dynamips_handler.py b/gns3server/handlers/dynamips_handler.py index f7929708..85d79cf6 100644 --- a/gns3server/handlers/dynamips_handler.py +++ b/gns3server/handlers/dynamips_handler.py @@ -48,7 +48,8 @@ class DynamipsHandler: dynamips_manager = Dynamips.instance() vm = yield from dynamips_manager.create_vm(request.json.pop("name"), request.match_info["project_id"], - request.json.get("vm_id")) + request.json.get("vm_id"), + request.json.pop("platform")) #for name, value in request.json.items(): # if hasattr(vm, name) and getattr(vm, name) != value: diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index 9e9246bd..4b401384 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -99,11 +99,12 @@ from ..base_manager import BaseManager from .dynamips_error import DynamipsError from .hypervisor import Hypervisor from .nodes.router import Router +from .dynamips_vm import DynamipsVM class Dynamips(BaseManager): - _VM_CLASS = Router + _VM_CLASS = DynamipsVM def __init__(self): @@ -112,7 +113,7 @@ class Dynamips(BaseManager): # FIXME: temporary self._working_dir = "/tmp" - self._dynamips_path = "/usr/local/bin/dynamips" + self._dynamips_path = "/usr/bin/dynamips" def find_dynamips(self): diff --git a/gns3server/modules/dynamips/dynamips_vm.py b/gns3server/modules/dynamips/dynamips_vm.py new file mode 100644 index 00000000..3beed541 --- /dev/null +++ b/gns3server/modules/dynamips/dynamips_vm.py @@ -0,0 +1,50 @@ +# -*- 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 .dynamips_error import DynamipsError +from .nodes.c1700 import C1700 +from .nodes.c2600 import C2600 +from .nodes.c2691 import C2691 +from .nodes.c3600 import C3600 +from .nodes.c3725 import C3725 +from .nodes.c3745 import C3745 +from .nodes.c7200 import C7200 + +import logging +log = logging.getLogger(__name__) + +PLATFORMS = {'c1700': C1700, + 'c2600': C2600, + 'c2691': C2691, + 'c3725': C3725, + 'c3745': C3745, + 'c3600': C3600, + 'c7200': C7200} + + +class DynamipsVM: + """ + Factory to create an Router object based on the correct platform. + """ + + def __new__(cls, name, vm_id, project, manager, platform, **kwargs): + + if platform not in PLATFORMS: + raise DynamipsError("Unknown router platform: {}".format(platform)) + + return PLATFORMS[platform](name, vm_id, project, manager, **kwargs) diff --git a/gns3server/modules/dynamips/nodes/c1700.py b/gns3server/modules/dynamips/nodes/c1700.py index 906abe3e..6bfe30e0 100644 --- a/gns3server/modules/dynamips/nodes/c1700.py +++ b/gns3server/modules/dynamips/nodes/c1700.py @@ -20,6 +20,7 @@ Interface for Dynamips virtual Cisco 1700 instances module ("c1700") http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L428 """ +import asyncio from .router import Router from ..adapters.c1700_mb_1fe import C1700_MB_1FE from ..adapters.c1700_mb_wic1 import C1700_MB_WIC1 @@ -32,16 +33,17 @@ class C1700(Router): """ Dynamips c1700 router. - :param hypervisor: Dynamips hypervisor instance - :param name: name for this router - :param router_id: router instance ID + :param name: The name of this router + :param vm_id: Router instance identifier + :param project: Project instance + :param manager: Parent VM Manager :param chassis: chassis for this router: 1720, 1721, 1750, 1751 or 1760 (default = 1720). 1710 is not supported. """ - def __init__(self, hypervisor, name, router_id=None, chassis="1720"): - Router.__init__(self, hypervisor, name, router_id, platform="c1700") + def __init__(self, name, vm_id, project, manager, chassis="1720"): + Router.__init__(self, name, vm_id, project, manager, platform="c1700") # Set default values for this platform self._ram = 128 @@ -51,43 +53,25 @@ class C1700(Router): self._chassis = chassis self._iomem = 15 # percentage self._clock_divisor = 8 - self._sparsemem = False + self._sparsemem = False # never activate sparsemem for c1700 (unstable) - if chassis != "1720": - self.chassis = chassis + def __json__(self): - self._setup_chassis() - - def defaults(self): - """ - Returns all the default attribute values for this platform. - - :returns: default values (dictionary) - """ - - router_defaults = Router.defaults(self) - - platform_defaults = {"ram": self._ram, - "nvram": self._nvram, - "disk0": self._disk0, - "disk1": self._disk1, + c1700_router_info = {"iomem": self._iomem, "chassis": self._chassis, - "iomem": self._iomem, - "clock_divisor": self._clock_divisor, "sparsemem": self._sparsemem} - # update the router defaults with the platform specific defaults - router_defaults.update(platform_defaults) - return router_defaults + router_info = Router.__json__(self) + router_info.update(c1700_router_info) + return router_info - def list(self): - """ - Returns all c1700 instances + @asyncio.coroutine + def create(self): - :returns: c1700 instance list - """ - - return self._hypervisor.send("c1700 list") + yield from Router.create(self) + if self._chassis != "1720": + yield from self.set_chassis(self._chassis) + self._setup_chassis() def _setup_chassis(self): """ @@ -114,8 +98,8 @@ class C1700(Router): return self._chassis - @chassis.setter - def chassis(self, chassis): + @asyncio.coroutine + def set_chassis(self, chassis): """ Sets the chassis. @@ -123,12 +107,11 @@ class C1700(Router): 1720, 1721, 1750, 1751 or 1760 """ - self._hypervisor.send("c1700 set_chassis {name} {chassis}".format(name=self._name, - chassis=chassis)) + yield from self._hypervisor.send('c1700 set_chassis "{name}" {chassis}'.format(name=self._name, chassis=chassis)) - log.info("router {name} [id={id}]: chassis set to {chassis}".format(name=self._name, - id=self._id, - chassis=chassis)) + log.info('Router "{name}" [{id}]: chassis set to {chassis}'.format(name=self._name, + id=self._id, + chassis=chassis)) self._chassis = chassis self._setup_chassis() @@ -143,19 +126,18 @@ class C1700(Router): return self._iomem - @iomem.setter - def iomem(self, iomem): + @asyncio.coroutine + def set_iomem(self, iomem): """ Sets I/O memory size for this router. :param iomem: I/O memory size """ - self._hypervisor.send("c1700 set_iomem {name} {size}".format(name=self._name, - size=iomem)) + yield from self._hypervisor.send('c1700 set_iomem "{name}" {size}'.format(name=self._name, size=iomem)) - log.info("router {name} [id={id}]: I/O memory updated from {old_iomem}% to {new_iomem}%".format(name=self._name, - id=self._id, - old_iomem=self._iomem, - new_iomem=iomem)) + log.info('Router "{name}" [{id}]: I/O memory updated from {old_iomem}% to {new_iomem}%'.format(name=self._name, + id=self._id, + old_iomem=self._iomem, + new_iomem=iomem)) self._iomem = iomem diff --git a/gns3server/modules/dynamips/nodes/c2600.py b/gns3server/modules/dynamips/nodes/c2600.py index b5e46e89..6f53ad14 100644 --- a/gns3server/modules/dynamips/nodes/c2600.py +++ b/gns3server/modules/dynamips/nodes/c2600.py @@ -20,6 +20,7 @@ Interface for Dynamips virtual Cisco 2600 instances module ("c2600") http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L404 """ +import asyncio from .router import Router from ..adapters.c2600_mb_1e import C2600_MB_1E from ..adapters.c2600_mb_2e import C2600_MB_2E @@ -34,9 +35,10 @@ class C2600(Router): """ Dynamips c2600 router. - :param hypervisor: Dynamips hypervisor instance - :param name: name for this router - :param router_id: router instance ID + :param name: The name of this router + :param vm_id: Router instance identifier + :param project: Project instance + :param manager: Parent VM Manager :param chassis: chassis for this router: 2610, 2611, 2620, 2621, 2610XM, 2611XM 2620XM, 2621XM, 2650XM or 2651XM (default = 2610). @@ -55,8 +57,8 @@ class C2600(Router): "2650XM": C2600_MB_1FE, "2651XM": C2600_MB_2FE} - def __init__(self, hypervisor, name, router_id=None, chassis="2610"): - Router.__init__(self, hypervisor, name, router_id, platform="c2600") + def __init__(self, name, vm_id, project, manager, chassis="2610"): + Router.__init__(self, name, vm_id, project, manager, platform="c2600") # Set default values for this platform self._ram = 128 @@ -66,43 +68,25 @@ class C2600(Router): self._chassis = chassis self._iomem = 15 # percentage self._clock_divisor = 8 - self._sparsemem = False + self._sparsemem = False # never activate sparsemem for c2600 (unstable) - if chassis != "2610": - self.chassis = chassis + def __json__(self): - self._setup_chassis() - - def defaults(self): - """ - Returns all the default attribute values for this platform. - - :returns: default values (dictionary) - """ - - router_defaults = Router.defaults(self) - - platform_defaults = {"ram": self._ram, - "nvram": self._nvram, - "disk0": self._disk0, - "disk1": self._disk1, - "iomem": self._iomem, + c2600_router_info = {"iomem": self._iomem, "chassis": self._chassis, - "clock_divisor": self._clock_divisor, "sparsemem": self._sparsemem} - # update the router defaults with the platform specific defaults - router_defaults.update(platform_defaults) - return router_defaults + router_info = Router.__json__(self) + router_info.update(c2600_router_info) + return router_info - def list(self): - """ - Returns all c2600 instances + @asyncio.coroutine + def create(self): - :returns: c2600 instance list - """ - - return self._hypervisor.send("c2600 list") + yield from Router.create(self) + if self._chassis != "2610": + yield from self.set_chassis(self._chassis) + self._setup_chassis() def _setup_chassis(self): """ @@ -123,8 +107,8 @@ class C2600(Router): return self._chassis - @chassis.setter - def chassis(self, chassis): + @asyncio.coroutine + def set_chassis(self, chassis): """ Sets the chassis. @@ -133,12 +117,11 @@ class C2600(Router): 2620XM, 2621XM, 2650XM or 2651XM """ - self._hypervisor.send("c2600 set_chassis {name} {chassis}".format(name=self._name, - chassis=chassis)) + yield from self._hypervisor.send('c2600 set_chassis "{name}" {chassis}'.format(name=self._name, chassis=chassis)) - log.info("router {name} [id={id}]: chassis set to {chassis}".format(name=self._name, - id=self._id, - chassis=chassis)) + log.info('Router "{name}" [{id}]: chassis set to {chassis}'.format(name=self._name, + id=self._id, + chassis=chassis)) self._chassis = chassis self._setup_chassis() @@ -152,19 +135,18 @@ class C2600(Router): return self._iomem - @iomem.setter - def iomem(self, iomem): + @asyncio.coroutine + def set_iomem(self, iomem): """ Sets I/O memory size for this router. :param iomem: I/O memory size """ - self._hypervisor.send("c2600 set_iomem {name} {size}".format(name=self._name, - size=iomem)) + yield from self._hypervisor.send('c2600 set_iomem "{name}" {size}'.format(name=self._name, size=iomem)) - log.info("router {name} [id={id}]: I/O memory updated from {old_iomem}% to {new_iomem}%".format(name=self._name, - id=self._id, - old_iomem=self._iomem, - new_iomem=iomem)) + log.info('Router "{name}" [{id}]: I/O memory updated from {old_iomem}% to {new_iomem}%'.format(name=self._name, + id=self._id, + old_iomem=self._iomem, + new_iomem=iomem)) self._iomem = iomem diff --git a/gns3server/modules/dynamips/nodes/c2691.py b/gns3server/modules/dynamips/nodes/c2691.py index 0dc0ef28..60489405 100644 --- a/gns3server/modules/dynamips/nodes/c2691.py +++ b/gns3server/modules/dynamips/nodes/c2691.py @@ -20,6 +20,7 @@ Interface for Dynamips virtual Cisco 2691 instances module ("c2691") http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L387 """ +import asyncio from .router import Router from ..adapters.gt96100_fe import GT96100_FE @@ -31,13 +32,14 @@ class C2691(Router): """ Dynamips c2691 router. - :param hypervisor: Dynamips hypervisor instance - :param name: name for this router - :param router_id: router instance ID + :param name: The name of this router + :param vm_id: Router instance identifier + :param project: Project instance + :param manager: Parent VM Manager """ - def __init__(self, hypervisor, name, router_id=None): - Router.__init__(self, hypervisor, name, router_id, platform="c2691") + def __init__(self, name, vm_id, project, manager): + Router.__init__(self, name, vm_id, project, manager, platform="c2691") # Set default values for this platform self._ram = 192 @@ -50,34 +52,13 @@ class C2691(Router): self._create_slots(2) self._slots[0] = GT96100_FE() - def defaults(self): - """ - Returns all the default attribute values for this platform. - - :returns: default values (dictionary) - """ - - router_defaults = Router.defaults(self) - - platform_defaults = {"ram": self._ram, - "nvram": self._nvram, - "disk0": self._disk0, - "disk1": self._disk1, - "iomem": self._iomem, - "clock_divisor": self._clock_divisor} + def __json__(self): - # update the router defaults with the platform specific defaults - router_defaults.update(platform_defaults) - return router_defaults - - def list(self): - """ - Returns all c2691 instances - - :returns: c2691 instance list - """ + c2691_router_info = {"iomem": self._iomem} - return self._hypervisor.send("c2691 list") + router_info = Router.__json__(self) + router_info.update(c2691_router_info) + return router_info @property def iomem(self): @@ -89,19 +70,18 @@ class C2691(Router): return self._iomem - @iomem.setter - def iomem(self, iomem): + @asyncio.coroutine + def set_iomem(self, iomem): """ Sets I/O memory size for this router. :param iomem: I/O memory size """ - self._hypervisor.send("c2691 set_iomem {name} {size}".format(name=self._name, - size=iomem)) + yield from self._hypervisor.send('c2691 set_iomem "{name}" {size}'.format(name=self._name, size=iomem)) - log.info("router {name} [id={id}]: I/O memory updated from {old_iomem}% to {new_iomem}%".format(name=self._name, - id=self._id, - old_iomem=self._iomem, - new_iomem=iomem)) + log.info('Router "{name}" [{id}]: I/O memory updated from {old_iomem}% to {new_iomem}%'.format(name=self._name, + id=self._id, + old_iomem=self._iomem, + new_iomem=iomem)) self._iomem = iomem diff --git a/gns3server/modules/dynamips/nodes/c3600.py b/gns3server/modules/dynamips/nodes/c3600.py index 32e2bbe7..42e2ea79 100644 --- a/gns3server/modules/dynamips/nodes/c3600.py +++ b/gns3server/modules/dynamips/nodes/c3600.py @@ -20,6 +20,7 @@ Interface for Dynamips virtual Cisco 3600 instances module ("c3600") http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L366 """ +import asyncio from .router import Router from ..adapters.leopard_2fe import Leopard_2FE @@ -31,15 +32,16 @@ class C3600(Router): """ Dynamips c3600 router. - :param hypervisor: Dynamips hypervisor instance - :param name: name for this router - :param router_id: router instance ID + :param name: The name of this router + :param vm_id: Router instance identifier + :param project: Project instance + :param manager: Parent VM Manager :param chassis: chassis for this router: 3620, 3640 or 3660 (default = 3640). """ - def __init__(self, hypervisor, name, router_id=None, chassis="3640"): - Router.__init__(self, hypervisor, name, router_id, platform="c3600") + def __init__(self, name, vm_id, project, manager, chassis="3640"): + Router.__init__(self, name, vm_id, project, manager, platform="c3600") # Set default values for this platform self._ram = 192 @@ -50,40 +52,22 @@ class C3600(Router): self._chassis = chassis self._clock_divisor = 4 - if chassis != "3640": - self.chassis = chassis + def __json__(self): - self._setup_chassis() - - def defaults(self): - """ - Returns all the default attribute values for this platform. - - :returns: default values (dictionary) - """ - - router_defaults = Router.defaults(self) + c3600_router_info = {"iomem": self._iomem, + "chassis": self._chassis} - platform_defaults = {"ram": self._ram, - "nvram": self._nvram, - "disk0": self._disk0, - "disk1": self._disk1, - "iomem": self._iomem, - "chassis": self._chassis, - "clock_divisor": self._clock_divisor} + router_info = Router.__json__(self) + router_info.update(c3600_router_info) + return router_info - # update the router defaults with the platform specific defaults - router_defaults.update(platform_defaults) - return router_defaults + @asyncio.coroutine + def create(self): - def list(self): - """ - Returns all c3600 instances - - :returns: c3600 instance list - """ - - return self._hypervisor.send("c3600 list") + yield from Router.create(self) + if self._chassis != "3640": + yield from self.set_chassis(self._chassis) + self._setup_chassis() def _setup_chassis(self): """ @@ -109,20 +93,19 @@ class C3600(Router): return self._chassis - @chassis.setter - def chassis(self, chassis): + @asyncio.coroutine + def set_chassis(self, chassis): """ Sets the chassis. :param: chassis string: 3620, 3640 or 3660 """ - self._hypervisor.send("c3600 set_chassis {name} {chassis}".format(name=self._name, - chassis=chassis)) + yield from self._hypervisor.send('c3600 set_chassis "{name}" {chassis}'.format(name=self._name, chassis=chassis)) - log.info("router {name} [id={id}]: chassis set to {chassis}".format(name=self._name, - id=self._id, - chassis=chassis)) + log.info('Router "{name}" [{id}]: chassis set to {chassis}'.format(name=self._name, + id=self._id, + chassis=chassis)) self._chassis = chassis self._setup_chassis() @@ -137,19 +120,18 @@ class C3600(Router): return self._iomem - @iomem.setter - def iomem(self, iomem): + @asyncio.coroutine + def set_iomem(self, iomem): """ Set I/O memory size for this router. :param iomem: I/O memory size """ - self._hypervisor.send("c3600 set_iomem {name} {size}".format(name=self._name, - size=iomem)) + yield from self._hypervisor.send('c3600 set_iomem "{name}" {size}'.format(name=self._name, size=iomem)) - log.info("router {name} [id={id}]: I/O memory updated from {old_iomem}% to {new_iomem}%".format(name=self._name, - id=self._id, - old_iomem=self._iomem, - new_iomem=iomem)) + log.info('Router "{name}" [{id}]: I/O memory updated from {old_iomem}% to {new_iomem}%'.format(name=self._name, + id=self._id, + old_iomem=self._iomem, + new_iomem=iomem)) self._iomem = iomem diff --git a/gns3server/modules/dynamips/nodes/c3725.py b/gns3server/modules/dynamips/nodes/c3725.py index 9317a393..cad3716f 100644 --- a/gns3server/modules/dynamips/nodes/c3725.py +++ b/gns3server/modules/dynamips/nodes/c3725.py @@ -20,6 +20,7 @@ Interface for Dynamips virtual Cisco 3725 instances module ("c3725") http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L346 """ +import asyncio from .router import Router from ..adapters.gt96100_fe import GT96100_FE @@ -31,13 +32,14 @@ class C3725(Router): """ Dynamips c3725 router. - :param hypervisor: Dynamips hypervisor instance - :param name: name for this router - :param router_id: router instance ID + :param name: The name of this router + :param vm_id: Router instance identifier + :param project: Project instance + :param manager: Parent VM Manager """ - def __init__(self, hypervisor, name, router_id=None): - Router.__init__(self, hypervisor, name, router_id, platform="c3725") + def __init__(self, name, vm_id, project, manager): + Router.__init__(self, name, vm_id, project, manager, platform="c3725") # Set default values for this platform self._ram = 128 @@ -50,34 +52,13 @@ class C3725(Router): self._create_slots(3) self._slots[0] = GT96100_FE() - def defaults(self): - """ - Returns all the default attribute values for this platform. - - :returns: default values (dictionary) - """ - - router_defaults = Router.defaults(self) - - platform_defaults = {"ram": self._ram, - "nvram": self._nvram, - "disk0": self._disk0, - "disk1": self._disk1, - "iomem": self._iomem, - "clock_divisor": self._clock_divisor} + def __json__(self): - # update the router defaults with the platform specific defaults - router_defaults.update(platform_defaults) - return router_defaults - - def list(self): - """ - Returns all c3725 instances. - - :returns: c3725 instance list - """ + c3725_router_info = {"iomem": self._iomem} - return self._hypervisor.send("c3725 list") + router_info = Router.__json__(self) + router_info.update(c3725_router_info) + return router_info @property def iomem(self): @@ -89,19 +70,18 @@ class C3725(Router): return self._iomem - @iomem.setter - def iomem(self, iomem): + @asyncio.coroutine + def set_iomem(self, iomem): """ Sets I/O memory size for this router. :param iomem: I/O memory size """ - self._hypervisor.send("c3725 set_iomem {name} {size}".format(name=self._name, - size=iomem)) + yield from self._hypervisor.send('c3725 set_iomem "{name}" {size}'.format(name=self._name, size=iomem)) - log.info("router {name} [id={id}]: I/O memory updated from {old_iomem}% to {new_iomem}%".format(name=self._name, - id=self._id, - old_iomem=self._iomem, - new_iomem=iomem)) + log.info('Router "{name}" [{id}]: I/O memory updated from {old_iomem}% to {new_iomem}%'.format(name=self._name, + id=self._id, + old_iomem=self._iomem, + new_iomem=iomem)) self._iomem = iomem diff --git a/gns3server/modules/dynamips/nodes/c3745.py b/gns3server/modules/dynamips/nodes/c3745.py index 8002909a..5ef49d11 100644 --- a/gns3server/modules/dynamips/nodes/c3745.py +++ b/gns3server/modules/dynamips/nodes/c3745.py @@ -20,6 +20,7 @@ Interface for Dynamips virtual Cisco 3745 instances module ("c3745") http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L326 """ +import asyncio from .router import Router from ..adapters.gt96100_fe import GT96100_FE @@ -31,13 +32,14 @@ class C3745(Router): """ Dynamips c3745 router. - :param hypervisor: Dynamips hypervisor instance - :param name: name for this router - :param router_id: router instance ID + :param name: The name of this router + :param vm_id: Router instance identifier + :param project: Project instance + :param manager: Parent VM Manager """ - def __init__(self, hypervisor, name, router_id=None): - Router.__init__(self, hypervisor, name, router_id, platform="c3745") + def __init__(self, name, vm_id, project, manager): + Router.__init__(self, name, vm_id, project, manager, platform="c3745") # Set default values for this platform self._ram = 256 @@ -50,34 +52,13 @@ class C3745(Router): self._create_slots(5) self._slots[0] = GT96100_FE() - def defaults(self): - """ - Returns all the default attribute values for this platform. - - :returns: default values (dictionary) - """ - - router_defaults = Router.defaults(self) - - platform_defaults = {"ram": self._ram, - "nvram": self._nvram, - "disk0": self._disk0, - "disk1": self._disk1, - "iomem": self._iomem, - "clock_divisor": self._clock_divisor} + def __json__(self): - # update the router defaults with the platform specific defaults - router_defaults.update(platform_defaults) - return router_defaults - - def list(self): - """ - Returns all c3745 instances. - - :returns: c3745 instance list - """ + c3745_router_info = {"iomem": self._iomem} - return self._hypervisor.send("c3745 list") + router_info = Router.__json__(self) + router_info.update(c3745_router_info) + return router_info @property def iomem(self): @@ -89,19 +70,18 @@ class C3745(Router): return self._iomem - @iomem.setter - def iomem(self, iomem): + @asyncio.coroutine + def set_iomem(self, iomem): """ Sets I/O memory size for this router. :param iomem: I/O memory size """ - self._hypervisor.send("c3745 set_iomem {name} {size}".format(name=self._name, - size=iomem)) + yield from self._hypervisor.send('c3745 set_iomem "{name}" {size}'.format(name=self._name, size=iomem)) - log.info("router {name} [id={id}]: I/O memory updated from {old_iomem}% to {new_iomem}%".format(name=self._name, - id=self._id, - old_iomem=self._iomem, - new_iomem=iomem)) + log.info('Router "{name}" [{id}]: I/O memory updated from {old_iomem}% to {new_iomem}%'.format(name=self._name, + id=self._id, + old_iomem=self._iomem, + new_iomem=iomem)) self._iomem = iomem diff --git a/gns3server/modules/dynamips/nodes/c7200.py b/gns3server/modules/dynamips/nodes/c7200.py index 21ab4aa6..777907bc 100644 --- a/gns3server/modules/dynamips/nodes/c7200.py +++ b/gns3server/modules/dynamips/nodes/c7200.py @@ -20,6 +20,8 @@ Interface for Dynamips virtual Cisco 7200 instances module ("c7200") http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L294 """ +import asyncio + from ..dynamips_error import DynamipsError from .router import Router from ..adapters.c7200_io_fe import C7200_IO_FE @@ -33,14 +35,15 @@ class C7200(Router): """ Dynamips c7200 router (model is 7206). - :param hypervisor: Dynamips hypervisor instance - :param name: name for this router - :param router_id: router instance ID - :param npe: default NPE + :param name: The name of this router + :param vm_id: Router instance identifier + :param project: Project instance + :param manager: Parent VM Manager + :param npe: Default NPE """ - def __init__(self, hypervisor, name, router_id=None, npe="npe-400"): - Router.__init__(self, hypervisor, name, router_id, platform="c7200") + def __init__(self, name, vm_id, project, manager, npe="npe-400"): + Router.__init__(self, name, vm_id, project, manager, platform="c7200") # Set default values for this platform self._ram = 512 @@ -50,9 +53,7 @@ class C7200(Router): self._npe = npe self._midplane = "vxr" self._clock_divisor = 4 - - if npe != "npe-400": - self.npe = npe + self._npe = npe # 4 sensors with a default temperature of 22C: # sensor 1 = I/0 controller inlet @@ -66,43 +67,30 @@ class C7200(Router): self._create_slots(7) - # first slot is a mandatory Input/Output controller (based on NPE type) - if npe == "npe-g2": - self.slot_add_binding(0, C7200_IO_GE_E()) - else: - self.slot_add_binding(0, C7200_IO_FE()) - - def defaults(self): - """ - Returns all the default attribute values for this platform. + def __json__(self): - :returns: default values (dictionary) - """ - - router_defaults = Router.defaults(self) - - platform_defaults = {"ram": self._ram, - "nvram": self._nvram, - "disk0": self._disk0, - "disk1": self._disk1, - "npe": self._npe, + c7200_router_info = {"npe": self._npe, "midplane": self._midplane, - "clock_divisor": self._clock_divisor, "sensors": self._sensors, "power_supplies": self._power_supplies} - # update the router defaults with the platform specific defaults - router_defaults.update(platform_defaults) - return router_defaults + router_info = Router.__json__(self) + router_info.update(c7200_router_info) + return router_info - def list(self): - """ - Returns all c7200 instances. + @asyncio.coroutine + def create(self): - :returns: c7200 instance list - """ + yield from Router.create(self) - return self._hypervisor.send("c7200 list") + if self._npe != "npe-400": + yield from self.set_npe(self._npe) + + # first slot is a mandatory Input/Output controller (based on NPE type) + if self.npe == "npe-g2": + yield from self.slot_add_binding(0, C7200_IO_GE_E()) + else: + yield from self.slot_add_binding(0, C7200_IO_FE()) @property def npe(self): @@ -114,8 +102,8 @@ class C7200(Router): return self._npe - @npe.setter - def npe(self, npe): + @asyncio.coroutine + def set_npe(self, npe): """ Sets the NPE model. @@ -127,13 +115,12 @@ class C7200(Router): if self.is_running(): raise DynamipsError("Cannot change NPE on running router") - self._hypervisor.send("c7200 set_npe {name} {npe}".format(name=self._name, - npe=npe)) + yield from self._hypervisor.send('c7200 set_npe "{name}" {npe}'.format(name=self._name, npe=npe)) - log.info("router {name} [id={id}]: NPE updated from {old_npe} to {new_npe}".format(name=self._name, - id=self._id, - old_npe=self._npe, - new_npe=npe)) + log.info('Router "{name}" [{id}]: NPE updated from {old_npe} to {new_npe}'.format(name=self._name, + id=self._id, + old_npe=self._npe, + new_npe=npe)) self._npe = npe @property @@ -146,21 +133,20 @@ class C7200(Router): return self._midplane - @midplane.setter - def midplane(self, midplane): + @asyncio.coroutine + def set_midplane(self, midplane): """ Sets the midplane model. :returns: midplane model string (e.g. "vxr" or "std") """ - self._hypervisor.send("c7200 set_midplane {name} {midplane}".format(name=self._name, - midplane=midplane)) + yield from self._hypervisor.send('c7200 set_midplane "{name}" {midplane}'.format(name=self._name, midplane=midplane)) - log.info("router {name} [id={id}]: midplane updated from {old_midplane} to {new_midplane}".format(name=self._name, - id=self._id, - old_midplane=self._midplane, - new_midplane=midplane)) + log.info('Router "{name}" [{id}]: midplane updated from {old_midplane} to {new_midplane}'.format(name=self._name, + id=self._id, + old_midplane=self._midplane, + new_midplane=midplane)) self._midplane = midplane @property @@ -173,8 +159,8 @@ class C7200(Router): return self._sensors - @sensors.setter - def sensors(self, sensors): + @asyncio.coroutine + def set_sensors(self, sensors): """ Sets the 4 sensors with temperature in degree Celcius. @@ -188,15 +174,15 @@ class C7200(Router): sensor_id = 0 for sensor in sensors: - self._hypervisor.send("c7200 set_temp_sensor {name} {sensor_id} {temp}".format(name=self._name, - sensor_id=sensor_id, - temp=sensor)) + yield from self._hypervisor.send('c7200 set_temp_sensor "{name}" {sensor_id} {temp}'.format(name=self._name, + sensor_id=sensor_id, + temp=sensor)) - log.info("router {name} [id={id}]: sensor {sensor_id} temperature updated from {old_temp}C to {new_temp}C".format(name=self._name, - id=self._id, - sensor_id=sensor_id, - old_temp=self._sensors[sensor_id], - new_temp=sensors[sensor_id])) + log.info('Router "{name}" [{id}]: sensor {sensor_id} temperature updated from {old_temp}C to {new_temp}C'.format(name=self._name, + id=self._id, + sensor_id=sensor_id, + old_temp=self._sensors[sensor_id], + new_temp=sensors[sensor_id])) sensor_id += 1 self._sensors = sensors @@ -211,8 +197,8 @@ class C7200(Router): return self._power_supplies - @power_supplies.setter - def power_supplies(self, power_supplies): + @asyncio.coroutine + def set_power_supplies(self, power_supplies): """ Sets the 2 power supplies with 0 = off, 1 = on. @@ -222,18 +208,19 @@ class C7200(Router): power_supply_id = 0 for power_supply in power_supplies: - self._hypervisor.send("c7200 set_power_supply {name} {power_supply_id} {powered_on}".format(name=self._name, - power_supply_id=power_supply_id, - powered_on=power_supply)) - - log.info("router {name} [id={id}]: power supply {power_supply_id} state updated to {powered_on}".format(name=self._name, - id=self._id, - power_supply_id=power_supply_id, - powered_on=power_supply)) + yield from self._hypervisor.send('c7200 set_power_supply "{name}" {power_supply_id} {powered_on}'.format(name=self._name, + power_supply_id=power_supply_id, + powered_on=power_supply)) + + log.info('Router "{name}" [{id}]: power supply {power_supply_id} state updated to {powered_on}'.format(name=self._name, + id=self._id, + power_supply_id=power_supply_id, + powered_on=power_supply)) power_supply_id += 1 self._power_supplies = power_supplies + @asyncio.coroutine def start(self): """ Starts this router. @@ -242,8 +229,8 @@ class C7200(Router): # trick: we must send sensors and power supplies info after starting the router # otherwise they are not taken into account (Dynamips bug?) - Router.start(self) + yield from Router.start(self) if self._sensors != [22, 22, 22, 22]: - self.sensors = self._sensors + yield from self.set_sensors(self._sensors) if self._power_supplies != [1, 1]: - self.power_supplies = self._power_supplies + yield from self.set_power_supplies(self._power_supplies) diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index 722100f4..b3324185 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -37,8 +37,15 @@ class Router(BaseVM): """ Dynamips router implementation. + + :param name: The name of this router + :param vm_id: Router instance identifier + :param project: Project instance + :param manager: Parent VM Manager + :param platform: Platform of this router """ + _instances = [] _status = {0: "inactive", 1: "shutting down", 2: "running", @@ -136,21 +143,22 @@ class Router(BaseVM): self._hypervisor = yield from self.manager.start_new_hypervisor() - yield from self._hypervisor.send("vm create '{name}' {id} {platform}".format(name=self._name, - id=self._id, + print("{} {} {}".format(self._name, self._id, self._platform)) + yield from self._hypervisor.send('vm create "{name}" {id} {platform}'.format(name=self._name, + id=1, #FIXME: instance ID! platform=self._platform)) if not self._ghost_flag: - log.info("Router {platform} '{name}' [{id}] has been created".format(name=self._name, + log.info('Router {platform} "{name}" [{id}] has been created'.format(name=self._name, platform=self._platform, id=self._id)) - yield from self._hypervisor.send("vm set_con_tcp_port '{name}' {console}".format(name=self._name, console=self._console)) - yield from self._hypervisor.send("vm set_aux_tcp_port '{name}' {aux}".format(name=self._name, aux=self._aux)) + yield from self._hypervisor.send('vm set_con_tcp_port "{name}" {console}'.format(name=self._name, console=self._console)) + yield from self._hypervisor.send('vm set_aux_tcp_port "{name}" {aux}'.format(name=self._name, aux=self._aux)) # get the default base MAC address - mac_addr = yield from self._hypervisor.send("{platform} get_mac_addr '{name}'".format(platform=self._platform, + mac_addr = yield from self._hypervisor.send('{platform} get_mac_addr "{name}"'.format(platform=self._platform, name=self._name)) self._mac_addr = mac_addr[0] @@ -164,7 +172,7 @@ class Router(BaseVM): :returns: inactive, shutting down, running or suspended. """ - status = yield from self._hypervisor.send("vm get_status '{name}'".format(name=self._name)) + status = yield from self._hypervisor.send('vm get_status "{name}"'.format(name=self._name)) return self._status[int(status[0])] @asyncio.coroutine @@ -181,23 +189,23 @@ class Router(BaseVM): if not os.path.isfile(self._image) or not os.path.exists(self._image): if os.path.islink(self._image): - raise DynamipsError("IOS image '{}' linked to '{}' is not accessible".format(self._image, os.path.realpath(self._image))) + raise DynamipsError('IOS image "{}" linked to "{}" is not accessible'.format(self._image, os.path.realpath(self._image))) else: - raise DynamipsError("IOS image '{}' is not accessible".format(self._image)) + raise DynamipsError('IOS image "{}" is not accessible'.format(self._image)) try: with open(self._image, "rb") as f: # read the first 7 bytes of the file. elf_header_start = f.read(7) except OSError as e: - raise DynamipsError("Cannot read ELF header for IOS image {}: {}".format(self._image, e)) + raise DynamipsError('Cannot read ELF header for IOS image "{}": {}'.format(self._image, e)) # IOS images must start with the ELF magic number, be 32-bit, big endian and have an ELF version of 1 if elf_header_start != b'\x7fELF\x01\x02\x01': - raise DynamipsError("'{}' is not a valid IOS image".format(self._image)) + raise DynamipsError('"{}" is not a valid IOS image'.format(self._image)) - yield from self._hypervisor.send("vm start '{}'".format(self._name)) - log.info("router '{name}' [{id}] has been started".format(name=self._name, id=self._id)) + yield from self._hypervisor.send('vm start "{}"'.format(self._name)) + log.info('router "{name}" [{id}] has been started'.format(name=self._name, id=self._id)) @asyncio.coroutine def stop(self): @@ -207,8 +215,8 @@ class Router(BaseVM): status = yield from self.get_status() if status != "inactive": - yield from self._hypervisor.send("vm stop '{name}'".format(self._name)) - log.info("Router '{name}' [{id}] has been stopped".format(name=self._name, id=self._id)) + yield from self._hypervisor.send('vm stop "{name}"'.format(self._name)) + log.info('Router "{name}" [{id}] has been stopped'.format(name=self._name, id=self._id)) @asyncio.coroutine def suspend(self): @@ -218,8 +226,8 @@ class Router(BaseVM): status = yield from self.get_status() if status == "running": - yield from self._hypervisor.send("vm suspend '{}'".format(self._name)) - log.info("Router '{name}' [{id}] has been suspended".format(name=self._name, id=self._id)) + yield from self._hypervisor.send('vm suspend "{}"'.format(self._name)) + log.info('Router "{name}" [{id}] has been suspended'.format(name=self._name, id=self._id)) @asyncio.coroutine def resume(self): @@ -227,8 +235,8 @@ class Router(BaseVM): Resumes this suspended router """ - yield from self._hypervisor.send("vm resume '{}'".format(self._name)) - log.info("Router '{name}' [{id}] has been resumed".format(name=self._name, id=self._id)) + yield from self._hypervisor.send('vm resume "{}"'.format(self._name)) + log.info('Router "{name}" [{id}] has been resumed'.format(name=self._name, id=self._id)) @asyncio.coroutine def is_running(self): @@ -271,9 +279,9 @@ class Router(BaseVM): """ yield from self.close() - yield from self._hypervisor.send("vm delete '{}'".format(self._name)) + yield from self._hypervisor.send('vm delete "{}"'.format(self._name)) self._hypervisor.devices.remove(self) - log.info("router '{name}' [{id}] has been deleted".format(name=self._name, id=self._id)) + log.info('Router "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id)) @property def platform(self): @@ -326,7 +334,7 @@ class Router(BaseVM): :param level: level number """ - yield from self._hypervisor.send("vm set_debug_level '{name}' {level}".format(name=self._name, level=level)) + yield from self._hypervisor.send('vm set_debug_level "{name}" {level}'.format(name=self._name, level=level)) @property def image(self): @@ -348,11 +356,11 @@ class Router(BaseVM): """ # encase image in quotes to protect spaces in the path - yield from self._hypervisor.send("vm set_ios {name} {image}".format(name=self._name, image='"' + image + '"')) + yield from self._hypervisor.send('vm set_ios "{name}" "{image}"'.format(name=self._name, image=image)) - log.info("Router '{name}' [{id}]: has a new IOS image set: {image}".format(name=self._name, - id=self._id, - image='"' + image + '"')) + log.info('Router "{name}" [{id}]: has a new IOS image set: "{image}"'.format(name=self._name, + id=self._id, + image=image)) self._image = image @@ -377,8 +385,8 @@ class Router(BaseVM): if self._ram == ram: return - yield from self._hypervisor.send("vm set_ram '{name}' {ram}".format(name=self._name, ram=ram)) - log.info("Router '{name}' [{id}]: RAM updated from {old_ram}MB to {new_ram}MB".format(name=self._name, + yield from self._hypervisor.send('vm set_ram "{name}" {ram}'.format(name=self._name, ram=ram)) + log.info('Router "{name}" [{id}]: RAM updated from {old_ram}MB to {new_ram}MB'.format(name=self._name, id=self._id, old_ram=self._ram, new_ram=ram)) @@ -405,8 +413,8 @@ class Router(BaseVM): if self._nvram == nvram: return - yield from self._hypervisor.send("vm set_nvram '{name}' {nvram}".format(name=self._name, nvram=nvram)) - log.info("Router '{name}' [{id}]: NVRAM updated from {old_nvram}KB to {new_nvram}KB".format(name=self._name, + yield from self._hypervisor.send('vm set_nvram "{name}" {nvram}'.format(name=self._name, nvram=nvram)) + log.info('Router "{name}" [{id}]: NVRAM updated from {old_nvram}KB to {new_nvram}KB'.format(name=self._name, id=self._id, old_nvram=self._nvram, new_nvram=nvram)) @@ -436,12 +444,12 @@ class Router(BaseVM): else: flag = 0 - yield from self._hypervisor.send("vm set_ram_mmap '{name}' {mmap}".format(name=self._name, mmap=flag)) + yield from self._hypervisor.send('vm set_ram_mmap "{name}" {mmap}'.format(name=self._name, mmap=flag)) if mmap: - log.info("Router '{name}' [{id}]: mmap enabled".format(name=self._name, id=self._id)) + log.info('Router "{name}" [{id}]: mmap enabled'.format(name=self._name, id=self._id)) else: - log.info("Router '{name}' [{id}]: mmap disabled".format(name=self._name, id=self._id)) + log.info('Router "{name}" [{id}]: mmap disabled'.format(name=self._name, id=self._id)) self._mmap = mmap @property @@ -466,12 +474,12 @@ class Router(BaseVM): flag = 1 else: flag = 0 - yield from self._hypervisor.send("vm set_sparse_mem '{name}' {sparsemem}".format(name=self._name, sparsemem=flag)) + yield from self._hypervisor.send('vm set_sparse_mem "{name}" {sparsemem}'.format(name=self._name, sparsemem=flag)) if sparsemem: - log.info("Router '{name}' [{id}]: sparse memory enabled".format(name=self._name, id=self._id)) + log.info('Router "{name}" [{id}]: sparse memory enabled'.format(name=self._name, id=self._id)) else: - log.info("Router '{name}' [{id}]: sparse memory disabled".format(name=self._name, id=self._id)) + log.info('Router "{name}" [{id}]: sparse memory disabled'.format(name=self._name, id=self._id)) self._sparsemem = sparsemem @property @@ -493,8 +501,8 @@ class Router(BaseVM): :param clock_divisor: clock divisor value (integer) """ - yield from self._hypervisor.send("vm set_clock_divisor '{name}' {clock}".format(name=self._name, clock=clock_divisor)) - log.info("Router '{name}' [{id}]: clock divisor updated from {old_clock} to {new_clock}".format(name=self._name, + yield from self._hypervisor.send('vm set_clock_divisor "{name}" {clock}'.format(name=self._name, clock=clock_divisor)) + log.info('Router "{name}" [{id}]: clock divisor updated from {old_clock} to {new_clock}'.format(name=self._name, id=self._id, old_clock=self._clock_divisor, new_clock=clock_divisor)) @@ -524,11 +532,11 @@ class Router(BaseVM): is_running = yield from self.is_running() if not is_running: # router is not running - yield from self._hypervisor.send("vm set_idle_pc '{name}' {idlepc}".format(name=self._name, idlepc=idlepc)) + yield from self._hypervisor.send('vm set_idle_pc "{name}" {idlepc}'.format(name=self._name, idlepc=idlepc)) else: - yield from self._hypervisor.send("vm set_idle_pc_online '{name}' 0 {idlepc}".format(name=self._name, idlepc=idlepc)) + yield from self._hypervisor.send('vm set_idle_pc_online "{name}" 0 {idlepc}'.format(name=self._name, idlepc=idlepc)) - log.info("Router '{name}' [{id}]: idle-PC set to {idlepc}".format(name=self._name, id=self._id, idlepc=idlepc)) + log.info('Router "{name}" [{id}]: idle-PC set to {idlepc}'.format(name=self._name, id=self._id, idlepc=idlepc)) self._idlepc = idlepc @asyncio.coroutine @@ -544,12 +552,12 @@ class Router(BaseVM): is_running = yield from self.is_running() if not is_running: # router is not running - raise DynamipsError("Router '{name}' is not running".format(name=self._name)) + raise DynamipsError('Router "{name}" is not running'.format(name=self._name)) - log.info("Router '{name}' [{id}] has started calculating Idle-PC values".format(name=self._name, id=self._id)) + log.info('Router "{name}" [{id}] has started calculating Idle-PC values'.format(name=self._name, id=self._id)) begin = time.time() - idlepcs = yield from self._hypervisor.send("vm get_idle_pc_prop '{}' 0".format(self._name)) - log.info("Router '{name}' [{id}] has finished calculating Idle-PC values after {time:.4f} seconds".format(name=self._name, + idlepcs = yield from self._hypervisor.send('vm get_idle_pc_prop "{}" 0'.format(self._name)) + log.info('Router "{name}" [{id}] has finished calculating Idle-PC values after {time:.4f} seconds'.format(name=self._name, id=self._id, time=time.time() - begin)) return idlepcs @@ -565,9 +573,9 @@ class Router(BaseVM): is_running = yield from self.is_running() if not is_running: # router is not running - raise DynamipsError("Router '{name}' is not running".format(name=self._name)) + raise DynamipsError('Router "{name}" is not running'.format(name=self._name)) - proposals = yield from self._hypervisor.send("vm show_idle_pc_prop '{}' 0".format(self._name)) + proposals = yield from self._hypervisor.send('vm show_idle_pc_prop "{}" 0'.format(self._name)) return proposals @property @@ -590,9 +598,9 @@ class Router(BaseVM): is_running = yield from self.is_running() if is_running: # router is running - yield from self._hypervisor.send("vm set_idle_max '{name}' 0 {idlemax}".format(name=self._name, idlemax=idlemax)) + yield from self._hypervisor.send('vm set_idle_max "{name}" 0 {idlemax}'.format(name=self._name, idlemax=idlemax)) - log.info("Router '{name}' [{id}]: idlemax updated from {old_idlemax} to {new_idlemax}".format(name=self._name, + log.info('Router "{name}" [{id}]: idlemax updated from {old_idlemax} to {new_idlemax}'.format(name=self._name, id=self._id, old_idlemax=self._idlemax, new_idlemax=idlemax)) @@ -619,10 +627,10 @@ class Router(BaseVM): is_running = yield from self.is_running() if is_running: # router is running - yield from self._hypervisor.send("vm set_idle_sleep_time '{name}' 0 {idlesleep}".format(name=self._name, + yield from self._hypervisor.send('vm set_idle_sleep_time "{name}" 0 {idlesleep}'.format(name=self._name, idlesleep=idlesleep)) - log.info("Router '{name}' [{id}]: idlesleep updated from {old_idlesleep} to {new_idlesleep}".format(name=self._name, + log.info('Router "{name}" [{id}]: idlesleep updated from {old_idlesleep} to {new_idlesleep}'.format(name=self._name, id=self._id, old_idlesleep=self._idlesleep, new_idlesleep=idlesleep)) @@ -647,10 +655,10 @@ class Router(BaseVM): :ghost_file: path to ghost file """ - yield from self._hypervisor.send("vm set_ghost_file '{name}' {ghost_file}".format(name=self._name, + yield from self._hypervisor.send('vm set_ghost_file "{name}" {ghost_file}'.format(name=self._name, ghost_file=ghost_file)) - log.info("Router '{name}' [{id}]: ghost file set to {ghost_file}".format(name=self._name, + log.info('Router "{name}" [{id}]: ghost file set to {ghost_file}'.format(name=self._name, id=self._id, ghost_file=ghost_file)) @@ -692,10 +700,10 @@ class Router(BaseVM): 2 => Use an existing ghost instance """ - yield from self._hypervisor.send("vm set_ghost_status '{name}' {ghost_status}".format(name=self._name, + yield from self._hypervisor.send('vm set_ghost_status "{name}" {ghost_status}'.format(name=self._name, ghost_status=ghost_status)) - log.info("Router '{name}' [{id}]: ghost status set to {ghost_status}".format(name=self._name, + log.info('Router "{name}" [{id}]: ghost status set to {ghost_status}'.format(name=self._name, id=self._id, ghost_status=ghost_status)) self._ghost_status = ghost_status @@ -721,10 +729,10 @@ class Router(BaseVM): :param exec_area: exec area value (integer) """ - yield from self._hypervisor.send("vm set_exec_area '{name}' {exec_area}".format(name=self._name, + yield from self._hypervisor.send('vm set_exec_area "{name}" {exec_area}'.format(name=self._name, exec_area=exec_area)) - log.info("Router '{name}' [{id}]: exec area updated from {old_exec}MB to {new_exec}MB".format(name=self._name, + log.info('Router "{name}" [{id}]: exec area updated from {old_exec}MB to {new_exec}MB'.format(name=self._name, id=self._id, old_exec=self._exec_area, new_exec=exec_area)) @@ -748,12 +756,12 @@ class Router(BaseVM): :param disk0: disk0 size (integer) """ - yield from self._hypervisor.send("vm set_disk0 '{name}' {disk0}".format(name=self._name, disk0=disk0)) + yield from self._hypervisor.send('vm set_disk0 "{name}" {disk0}'.format(name=self._name, disk0=disk0)) - log.info("Router {name} [{id}]: disk0 updated from {old_disk0}MB to {new_disk0}MB".format(name=self._name, - id=self._id, - old_disk0=self._disk0, - new_disk0=disk0)) + log.info('Router "{name}" [{id}]: disk0 updated from {old_disk0}MB to {new_disk0}MB'.format(name=self._name, + id=self._id, + old_disk0=self._disk0, + new_disk0=disk0)) self._disk0 = disk0 @property @@ -774,9 +782,9 @@ class Router(BaseVM): :param disk1: disk1 size (integer) """ - yield from self._hypervisor.send("vm set_disk1 '{name}' {disk1}".format(name=self._name, disk1=disk1)) + yield from self._hypervisor.send('vm set_disk1 "{name}" {disk1}'.format(name=self._name, disk1=disk1)) - log.info("Router '{name}' [{id}]: disk1 updated from {old_disk1}MB to {new_disk1}MB".format(name=self._name, + log.info('Router "{name}" [{id}]: disk1 updated from {old_disk1}MB to {new_disk1}MB'.format(name=self._name, id=self._id, old_disk1=self._disk1, new_disk1=disk1)) @@ -801,9 +809,9 @@ class Router(BaseVM): :param confreg: configuration register value (string) """ - yield from self._hypervisor.send("vm set_conf_reg '{name}' {confreg}".format(name=self._name, confreg=confreg)) + yield from self._hypervisor.send('vm set_conf_reg "{name}" {confreg}'.format(name=self._name, confreg=confreg)) - log.info("Router '{name}' [{id}]: confreg updated from {old_confreg} to {new_confreg}".format(name=self._name, + log.info('Router "{name}" [{id}]: confreg updated from {old_confreg} to {new_confreg}'.format(name=self._name, id=self._id, old_confreg=self._confreg, new_confreg=confreg)) @@ -827,9 +835,9 @@ class Router(BaseVM): :param console: console port (integer) """ - yield from self._hypervisor.send("vm set_con_tcp_port '{name}' {console}".format(name=self._name, console=console)) + yield from self._hypervisor.send('vm set_con_tcp_port "{name}" {console}'.format(name=self._name, console=console)) - log.info("Router '{name}' [{id}]: console port updated from {old_console} to {new_console}".format(name=self._name, + log.info('Router "{name}" [{id}]: console port updated from {old_console} to {new_console}'.format(name=self._name, id=self._id, old_console=self._console, new_console=console)) @@ -855,9 +863,9 @@ class Router(BaseVM): :param aux: console auxiliary port (integer) """ - yield from self._hypervisor.send("vm set_aux_tcp_port '{name}' {aux}".format(name=self._name, aux=aux)) + yield from self._hypervisor.send('vm set_aux_tcp_port "{name}" {aux}'.format(name=self._name, aux=aux)) - log.info("Router '{name}' [{id}]: aux port updated from {old_aux} to {new_aux}".format(name=self._name, + log.info('Router "{name}" [{id}]: aux port updated from {old_aux} to {new_aux}'.format(name=self._name, id=self._id, old_aux=self._aux, new_aux=aux)) @@ -873,7 +881,7 @@ class Router(BaseVM): :returns: cpu usage in seconds """ - cpu_usage = yield from self._hypervisor.send("vm cpu_usage '{name}' {cpu_id}".format(name=self._name, cpu_id=cpu_id)) + cpu_usage = yield from self._hypervisor.send('vm cpu_usage "{name}" {cpu_id}'.format(name=self._name, cpu_id=cpu_id)) return int(cpu_usage[0]) @property @@ -894,11 +902,11 @@ class Router(BaseVM): :param mac_addr: a MAC address (hexadecimal format: hh:hh:hh:hh:hh:hh) """ - yield from self._hypervisor.send("{platform} set_mac_addr '{name}' {mac_addr}".format(platform=self._platform, + yield from self._hypervisor.send('{platform} set_mac_addr "{name}" {mac_addr}'.format(platform=self._platform, name=self._name, mac_addr=mac_addr)) - log.info("Router '{name}' [{id}]: MAC address updated from {old_mac} to {new_mac}".format(name=self._name, + log.info('Router "{name}" [{id}]: MAC address updated from {old_mac} to {new_mac}'.format(name=self._name, id=self._id, old_mac=self._mac_addr, new_mac=mac_addr)) @@ -922,11 +930,11 @@ class Router(BaseVM): :param system_id: a system ID (also called board processor ID) """ - yield from self._hypervisor.send("{platform} set_system_id '{name}' {system_id}".format(platform=self._platform, + yield from self._hypervisor.send('{platform} set_system_id "{name}" {system_id}'.format(platform=self._platform, name=self._name, system_id=system_id)) - log.info("Router '{name'} [{id}]: system ID updated from {old_id} to {new_id}".format(name=self._name, + log.info('Router "{name}" [{id}]: system ID updated from {old_id} to {new_id}'.format(name=self._name, id=self._id, old_id=self._system_id, new_id=system_id)) @@ -940,7 +948,7 @@ class Router(BaseVM): :returns: slot bindings (adapter names) list """ - slot_bindings = yield from self._hypervisor.send("vm slot_bindings '{}'".format(self._name)) + slot_bindings = yield from self._hypervisor.send('vm slot_bindings "{}"'.format(self._name)) return slot_bindings @asyncio.coroutine @@ -955,11 +963,11 @@ class Router(BaseVM): try: slot = self._slots[slot_id] except IndexError: - raise DynamipsError("Slot {slot_id} doesn't exist on router '{name}'".format(name=self._name, slot_id=slot_id)) + raise DynamipsError('Slot {slot_id} does not exist on router "{name}"'.format(name=self._name, slot_id=slot_id)) if slot is not None: current_adapter = slot - raise DynamipsError("Slot {slot_id} is already occupied by adapter {adapter} on router '{name}'".format(name=self._name, + raise DynamipsError('Slot {slot_id} is already occupied by adapter {adapter} on router "{name}"'.format(name=self._name, slot_id=slot_id, adapter=current_adapter)) @@ -969,14 +977,14 @@ class Router(BaseVM): if is_running and not ((self._platform == 'c7200' and not str(adapter).startswith('C7200')) and not (self._platform == 'c3600' and self.chassis == '3660') and not (self._platform == 'c3745' and adapter == 'NM-4T')): - raise DynamipsError("Adapter {adapter} cannot be added while router '{name} 'is running".format(adapter=adapter, + raise DynamipsError('Adapter {adapter} cannot be added while router "{name}" is running'.format(adapter=adapter, name=self._name)) - yield from self._hypervisor.send("vm slot_add_binding '{name}' {slot_id} 0 {adapter}".format(name=self._name, + yield from self._hypervisor.send('vm slot_add_binding "{name}" {slot_id} 0 {adapter}'.format(name=self._name, slot_id=slot_id, adapter=adapter)) - log.info("Router '{name}' [{id}]: adapter {adapter} inserted into slot {slot_id}".format(name=self._name, + log.info('Router "{name}" [{id}]: adapter {adapter} inserted into slot {slot_id}'.format(name=self._name, id=self._id, adapter=adapter, slot_id=slot_id)) @@ -986,9 +994,9 @@ class Router(BaseVM): # Generate an OIR event if the router is running if is_running: - yield from self._hypervisor.send("vm slot_oir_start '{name}' {slot_id} 0".format(name=self._name, slot_id=slot_id)) + yield from self._hypervisor.send('vm slot_oir_start "{name}" {slot_id} 0'.format(name=self._name, slot_id=slot_id)) - log.info("Router '{name}' [{id}]: OIR start event sent to slot {slot_id}".format(name=self._name, + log.info('Router "{name}" [{id}]: OIR start event sent to slot {slot_id}'.format(name=self._name, id=self._id, slot_id=slot_id)) @@ -1003,10 +1011,10 @@ class Router(BaseVM): try: adapter = self._slots[slot_id] except IndexError: - raise DynamipsError("Slot {slot_id} doesn't exist on router '{name}'".format(name=self._name, slot_id=slot_id)) + raise DynamipsError('Slot {slot_id} does not exist on router "{name}"'.format(name=self._name, slot_id=slot_id)) if adapter is None: - raise DynamipsError("No adapter in slot {slot_id} on router '{name}'".format(name=self._name, + raise DynamipsError('No adapter in slot {slot_id} on router "{name}"'.format(name=self._name, slot_id=slot_id)) is_running = yield from self.is_running() @@ -1015,21 +1023,21 @@ class Router(BaseVM): if is_running and not ((self._platform == 'c7200' and not str(adapter).startswith('C7200')) and not (self._platform == 'c3600' and self.chassis == '3660') and not (self._platform == 'c3745' and adapter == 'NM-4T')): - raise DynamipsError("Adapter {adapter} cannot be removed while router '{name}' is running".format(adapter=adapter, + raise DynamipsError('Adapter {adapter} cannot be removed while router "{name}" is running'.format(adapter=adapter, name=self._name)) # Generate an OIR event if the router is running if is_running: - yield from self._hypervisor.send("vm slot_oir_stop '{name}' {slot_id} 0".format(name=self._name, slot_id=slot_id)) + yield from self._hypervisor.send('vm slot_oir_stop "{name}" {slot_id} 0'.format(name=self._name, slot_id=slot_id)) - log.info("router '{name}' [{id}]: OIR stop event sent to slot {slot_id}".format(name=self._name, + log.info('Router "{name}" [{id}]: OIR stop event sent to slot {slot_id}'.format(name=self._name, id=self._id, slot_id=slot_id)) - yield from self._hypervisor.send("vm slot_remove_binding '{name}' {slot_id} 0".format(name=self._name, slot_id=slot_id)) + yield from self._hypervisor.send('vm slot_remove_binding "{name}" {slot_id} 0'.format(name=self._name, slot_id=slot_id)) - log.info("Router '{name}' [{id}]: adapter {adapter} removed from slot {slot_id}".format(name=self._name, + log.info('Router "{name}" [{id}]: adapter {adapter} removed from slot {slot_id}'.format(name=self._name, id=self._id, adapter=adapter, slot_id=slot_id)) @@ -1060,12 +1068,12 @@ class Router(BaseVM): # Dynamips WICs slot IDs start on a multiple of 16 # WIC1 = 16, WIC2 = 32 and WIC3 = 48 internal_wic_slot_id = 16 * (wic_slot_id + 1) - yield from self._hypervisor.send("vm slot_add_binding '{name}' {slot_id} {wic_slot_id} {wic}".format(name=self._name, + yield from self._hypervisor.send('vm slot_add_binding "{name}" {slot_id} {wic_slot_id} {wic}'.format(name=self._name, slot_id=slot_id, wic_slot_id=internal_wic_slot_id, wic=wic)) - log.info("Router '{name}' [{id}]: {wic} inserted into WIC slot {wic_slot_id}".format(name=self._name, + log.info('Router "{name}" [{id}]: {wic} inserted into WIC slot {wic_slot_id}'.format(name=self._name, id=self._id, wic=wic, wic_slot_id=wic_slot_id)) @@ -1096,11 +1104,11 @@ class Router(BaseVM): # Dynamips WICs slot IDs start on a multiple of 16 # WIC1 = 16, WIC2 = 32 and WIC3 = 48 internal_wic_slot_id = 16 * (wic_slot_id + 1) - yield from self._hypervisor.send("vm slot_remove_binding '{name}' {slot_id} {wic_slot_id}".format(name=self._name, + yield from self._hypervisor.send('vm slot_remove_binding "{name}" {slot_id} {wic_slot_id}'.format(name=self._name, slot_id=slot_id, wic_slot_id=internal_wic_slot_id)) - log.info("Router '{name}' [{id}]: {wic} removed from WIC slot {wic_slot_id}".format(name=self._name, + log.info('Router "{name}" [{id}]: {wic} removed from WIC slot {wic_slot_id}'.format(name=self._name, id=self._id, wic=adapter.wics[wic_slot_id], wic_slot_id=wic_slot_id)) @@ -1116,7 +1124,7 @@ class Router(BaseVM): :returns: list of NIO bindings """ - nio_bindings = yield from self._hypervisor.send("vm slot_nio_bindings '{name}' {slot_id}".format(name=self._name, + nio_bindings = yield from self._hypervisor.send('vm slot_nio_bindings "{name}" {slot_id}'.format(name=self._name, slot_id=slot_id)) return nio_bindings @@ -1133,18 +1141,18 @@ class Router(BaseVM): try: adapter = self._slots[slot_id] except IndexError: - raise DynamipsError("Slot {slot_id} doesn't exist on router '{name}'".format(name=self._name, - slot_id=slot_id)) + raise DynamipsError('Slot {slot_id} does not exist on router "{name}"'.format(name=self._name, + slot_id=slot_id)) if not adapter.port_exists(port_id): - raise DynamipsError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, - port_id=port_id)) + raise DynamipsError("Port {port_id} does not exist in adapter {adapter}".format(adapter=adapter, + port_id=port_id)) - yield from self._hypervisor.send("vm slot_add_nio_binding '{name}' {slot_id} {port_id} {nio}".format(name=self._name, + yield from self._hypervisor.send('vm slot_add_nio_binding "{name}" {slot_id} {port_id} {nio}'.format(name=self._name, slot_id=slot_id, port_id=port_id, nio=nio)) - log.info("Router '{name}' [{id}]: NIO {nio_name} bound to port {slot_id}/{port_id}".format(name=self._name, + log.info('Router "{name}" [{id}]: NIO {nio_name} bound to port {slot_id}/{port_id}'.format(name=self._name, id=self._id, nio_name=nio.name, slot_id=slot_id, @@ -1167,19 +1175,19 @@ class Router(BaseVM): try: adapter = self._slots[slot_id] except IndexError: - raise DynamipsError("Slot {slot_id} doesn't exist on router '{name}'".format(name=self._name, slot_id=slot_id)) + raise DynamipsError('Slot {slot_id} does not exist on router "{name}"'.format(name=self._name, slot_id=slot_id)) if not adapter.port_exists(port_id): - raise DynamipsError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, port_id=port_id)) + raise DynamipsError("Port {port_id} does not exist in adapter {adapter}".format(adapter=adapter, port_id=port_id)) yield from self.slot_disable_nio(slot_id, port_id) - yield from self._hypervisor.send("vm slot_remove_nio_binding '{name}' {slot_id} {port_id}".format(name=self._name, + yield from self._hypervisor.send('vm slot_remove_nio_binding "{name}" {slot_id} {port_id}'.format(name=self._name, slot_id=slot_id, port_id=port_id)) nio = adapter.get_nio(port_id) adapter.remove_nio(port_id) - log.info("Router '{name}' [{id}]: NIO {nio_name} removed from port {slot_id}/{port_id}".format(name=self._name, + log.info('Router "{name}" [{id}]: NIO {nio_name} removed from port {slot_id}/{port_id}'.format(name=self._name, id=self._id, nio_name=nio.name, slot_id=slot_id, @@ -1198,11 +1206,11 @@ class Router(BaseVM): is_running = yield from self.is_running() if is_running: # running router - yield from self._hypervisor.send("vm slot_enable_nio '{name}' {slot_id} {port_id}".format(name=self._name, + yield from self._hypervisor.send('vm slot_enable_nio "{name}" {slot_id} {port_id}'.format(name=self._name, slot_id=slot_id, port_id=port_id)) - log.info("Router '{name}' [{id}]: NIO enabled on port {slot_id}/{port_id}".format(name=self._name, + log.info('Router "{name}" [{id}]: NIO enabled on port {slot_id}/{port_id}'.format(name=self._name, id=self._id, slot_id=slot_id, port_id=port_id)) @@ -1217,11 +1225,11 @@ class Router(BaseVM): is_running = yield from self.is_running() if is_running: # running router - yield from self._hypervisor.send("vm slot_disable_nio '{name}' {slot_id} {port_id}".format(name=self._name, + yield from self._hypervisor.send('vm slot_disable_nio "{name}" {slot_id} {port_id}'.format(name=self._name, slot_id=slot_id, port_id=port_id)) - log.info("Router '{name}' [{id}]: NIO disabled on port {slot_id}/{port_id}".format(name=self._name, + log.info('Router "{name}" [{id}]: NIO disabled on port {slot_id}/{port_id}'.format(name=self._name, id=self._id, slot_id=slot_id, port_id=port_id)) @@ -1240,9 +1248,9 @@ class Router(BaseVM): try: adapter = self._slots[slot_id] except IndexError: - raise DynamipsError("Slot {slot_id} doesn't exist on router '{name}'".format(name=self._name, slot_id=slot_id)) + raise DynamipsError('Slot {slot_id} does not exist on router "{name}"'.format(name=self._name, slot_id=slot_id)) if not adapter.port_exists(port_id): - raise DynamipsError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, port_id=port_id)) + raise DynamipsError("Port {port_id} does not exist in adapter {adapter}".format(adapter=adapter, port_id=port_id)) data_link_type = data_link_type.lower() if data_link_type.startswith("dlt_"): @@ -1264,7 +1272,7 @@ class Router(BaseVM): yield from nio.bind_filter("both", "capture") yield from nio.setup_filter("both", '{} "{}"'.format(data_link_type, output_file)) - log.info("Router '{name}' [{id}]: starting packet capture on port {slot_id}/{port_id}".format(name=self._name, + log.info('Router "{name}" [{id}]: starting packet capture on port {slot_id}/{port_id}'.format(name=self._name, id=self._id, nio_name=nio.name, slot_id=slot_id, @@ -1281,14 +1289,14 @@ class Router(BaseVM): try: adapter = self._slots[slot_id] except IndexError: - raise DynamipsError("Slot {slot_id} doesn't exist on router '{name}'".format(name=self._name, slot_id=slot_id)) + raise DynamipsError('Slot {slot_id} does not exist on router "{name}"'.format(name=self._name, slot_id=slot_id)) if not adapter.port_exists(port_id): - raise DynamipsError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, port_id=port_id)) + raise DynamipsError("Port {port_id} does not exist in adapter {adapter}".format(adapter=adapter, port_id=port_id)) nio = adapter.get_nio(port_id) yield from nio.unbind_filter("both") - log.info("Router '{name}' [{id}]: stopping packet capture on port {slot_id}/{port_id}".format(name=self._name, + log.info('Router "{name}" [{id}]: stopping packet capture on port {slot_id}/{port_id}'.format(name=self._name, id=self._id, nio_name=nio.name, slot_id=slot_id, diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index 9791fde0..1d1678d3 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -44,10 +44,10 @@ class VPCSVM(BaseVM): """ VPCS vm implementation. - :param name: name of this VPCS vm + :param name: The name of this VM :param vm_id: VPCS instance identifier :param project: Project instance - :param manager: parent VM Manager + :param manager: Parent VM Manager :param console: TCP console port :param startup_script: Content of vpcs startup script file """ From 79a57ca4209ded05bcab4cc882c891078f60115b Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 11 Feb 2015 19:21:34 -0700 Subject: [PATCH 219/485] New Dynamips integration part 3 --- gns3server/config.py | 2 +- gns3server/handlers/dynamips_handler.py | 194 +++++- gns3server/modules/dynamips/__init__.py | 518 ++++---------- .../modules/dynamips/dynamips_hypervisor.py | 4 +- gns3server/modules/dynamips/dynamips_vm.py | 4 +- gns3server/modules/dynamips/nios/nio_fifo.py | 6 +- .../dynamips/nios/nio_generic_ethernet.py | 6 +- .../dynamips/nios/nio_linux_ethernet.py | 6 +- gns3server/modules/dynamips/nios/nio_mcast.py | 6 +- gns3server/modules/dynamips/nios/nio_null.py | 6 +- gns3server/modules/dynamips/nios/nio_tap.py | 6 +- gns3server/modules/dynamips/nios/nio_udp.py | 6 +- .../modules/dynamips/nios/nio_udp_auto.py | 6 +- gns3server/modules/dynamips/nios/nio_unix.py | 6 +- gns3server/modules/dynamips/nios/nio_vde.py | 6 +- gns3server/modules/dynamips/nodes/c1700.py | 5 +- gns3server/modules/dynamips/nodes/c2600.py | 5 +- gns3server/modules/dynamips/nodes/c2691.py | 5 +- gns3server/modules/dynamips/nodes/c3600.py | 5 +- gns3server/modules/dynamips/nodes/c3725.py | 5 +- gns3server/modules/dynamips/nodes/c3745.py | 5 +- gns3server/modules/dynamips/nodes/c7200.py | 5 +- gns3server/modules/dynamips/nodes/router.py | 41 +- gns3server/schemas/dynamips.py | 637 +++++++++++++++++- gns3server/utils/interfaces.py | 4 +- old_tests/dynamips/.gitignore | 1 - old_tests/dynamips/conftest.py | 29 - old_tests/dynamips/dynamips.stable | Bin 961958 -> 0 bytes old_tests/dynamips/test_atm_bridge.py | 62 -- old_tests/dynamips/test_atm_switch.py | 83 --- old_tests/dynamips/test_bridge.py | 31 - old_tests/dynamips/test_c1700.py | 167 ----- old_tests/dynamips/test_c2600.py | 216 ------ old_tests/dynamips/test_c2691.py | 73 -- old_tests/dynamips/test_c3600.py | 118 ---- old_tests/dynamips/test_c3725.py | 73 -- old_tests/dynamips/test_c3745.py | 73 -- old_tests/dynamips/test_c7200.py | 188 ------ old_tests/dynamips/test_ethernet_switch.py | 87 --- old_tests/dynamips/test_frame_relay_switch.py | 65 -- old_tests/dynamips/test_hub.py | 25 - old_tests/dynamips/test_hypervisor.py | 41 -- old_tests/dynamips/test_hypervisor_manager.py | 52 -- old_tests/dynamips/test_nios.py | 139 ---- old_tests/dynamips/test_router.py | 232 ------- old_tests/dynamips/test_vmhandler.py | 65 -- old_tests/test_jsonrpc.py | 92 --- tests/api/test_dynamips.py | 125 ++++ .../modules/dynamips/test_dynamips_manager.py | 44 ++ .../modules/dynamips/test_dynamips_router.py | 51 ++ .../virtualbox/test_virtualbox_manager.py | 10 +- 51 files changed, 1256 insertions(+), 2385 deletions(-) delete mode 100644 old_tests/dynamips/.gitignore delete mode 100644 old_tests/dynamips/conftest.py delete mode 100755 old_tests/dynamips/dynamips.stable delete mode 100644 old_tests/dynamips/test_atm_bridge.py delete mode 100644 old_tests/dynamips/test_atm_switch.py delete mode 100644 old_tests/dynamips/test_bridge.py delete mode 100644 old_tests/dynamips/test_c1700.py delete mode 100644 old_tests/dynamips/test_c2600.py delete mode 100644 old_tests/dynamips/test_c2691.py delete mode 100644 old_tests/dynamips/test_c3600.py delete mode 100644 old_tests/dynamips/test_c3725.py delete mode 100644 old_tests/dynamips/test_c3745.py delete mode 100644 old_tests/dynamips/test_c7200.py delete mode 100644 old_tests/dynamips/test_ethernet_switch.py delete mode 100644 old_tests/dynamips/test_frame_relay_switch.py delete mode 100644 old_tests/dynamips/test_hub.py delete mode 100644 old_tests/dynamips/test_hypervisor.py delete mode 100644 old_tests/dynamips/test_hypervisor_manager.py delete mode 100644 old_tests/dynamips/test_nios.py delete mode 100644 old_tests/dynamips/test_router.py delete mode 100644 old_tests/dynamips/test_vmhandler.py delete mode 100644 old_tests/test_jsonrpc.py create mode 100644 tests/api/test_dynamips.py create mode 100644 tests/modules/dynamips/test_dynamips_manager.py create mode 100644 tests/modules/dynamips/test_dynamips_router.py diff --git a/gns3server/config.py b/gns3server/config.py index 21e1e5b9..05c0cb96 100644 --- a/gns3server/config.py +++ b/gns3server/config.py @@ -186,7 +186,7 @@ class Config(object): """ Singleton to return only on instance of Config. - :params files: Array of configuration files (optionnal) + :params files: Array of configuration files (optional) :returns: instance of Config """ diff --git a/gns3server/handlers/dynamips_handler.py b/gns3server/handlers/dynamips_handler.py index 85d79cf6..0b0be6a1 100644 --- a/gns3server/handlers/dynamips_handler.py +++ b/gns3server/handlers/dynamips_handler.py @@ -15,10 +15,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import os + +import asyncio from ..web.route import Route -from ..schemas.dynamips import ROUTER_CREATE_SCHEMA -from ..schemas.dynamips import ROUTER_OBJECT_SCHEMA +from ..schemas.dynamips import VM_CREATE_SCHEMA +from ..schemas.dynamips import VM_UPDATE_SCHEMA +from ..schemas.dynamips import VM_OBJECT_SCHEMA from ..modules.dynamips import Dynamips from ..modules.project_manager import ProjectManager @@ -31,7 +33,7 @@ class DynamipsHandler: @classmethod @Route.post( - r"/projects/{project_id}/dynamips/routers", + r"/projects/{project_id}/dynamips/vms", parameters={ "project_id": "UUID for the project" }, @@ -40,20 +42,194 @@ class DynamipsHandler: 400: "Invalid request", 409: "Conflict" }, - description="Create a new Dynamips router instance", - input=ROUTER_CREATE_SCHEMA) - #output=ROUTER_OBJECT_SCHEMA) + description="Create a new Dynamips VM instance", + input=VM_CREATE_SCHEMA, + output=VM_OBJECT_SCHEMA) def create(request, response): dynamips_manager = Dynamips.instance() vm = yield from dynamips_manager.create_vm(request.json.pop("name"), request.match_info["project_id"], request.json.get("vm_id"), + request.json.get("dynamips_id"), request.json.pop("platform")) + # set VM options + for name, value in request.json.items(): + if hasattr(vm, name) and getattr(vm, name) != value: + setter = getattr(vm, "set_{}".format(name)) + if asyncio.iscoroutinefunction(vm.close): + yield from setter(value) + else: + setter(value) + + response.set_status(201) + response.json(vm) + + @classmethod + @Route.get( + r"/projects/{project_id}/dynamips/vms/{vm_id}", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 200: "Success", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Get a Dynamips VM instance", + output=VM_OBJECT_SCHEMA) + def show(request, response): + + dynamips_manager = Dynamips.instance() + vm = dynamips_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + response.json(vm) + + @classmethod + @Route.put( + r"/projects/{project_id}/dynamips/vms/{vm_id}", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 200: "Instance updated", + 400: "Invalid request", + 404: "Instance doesn't exist", + 409: "Conflict" + }, + description="Update a Dynamips VM instance", + input=VM_UPDATE_SCHEMA, + output=VM_OBJECT_SCHEMA) + def update(request, response): + + dynamips_manager = Dynamips.instance() + vm = dynamips_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + + # FIXME: set options #for name, value in request.json.items(): # if hasattr(vm, name) and getattr(vm, name) != value: # setattr(vm, name, value) - - response.set_status(201) response.json(vm) + + @classmethod + @Route.delete( + r"/projects/{project_id}/dynamips/vms/{vm_id}", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 204: "Instance deleted", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Delete a Dynamips VM instance") + def delete(request, response): + + # check the project_id exists + ProjectManager.instance().get_project(request.match_info["project_id"]) + + yield from Dynamips.instance().delete_vm(request.match_info["vm_id"]) + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/dynamips/vms/{vm_id}/start", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 204: "Instance started", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Start a Dynamips VM instance") + def start(request, response): + + dynamips_manager = Dynamips.instance() + vm = dynamips_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + yield from vm.start() + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/dynamips/vms/{vm_id}/stop", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 204: "Instance stopped", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Stop a Dynamips VM instance") + def stop(request, response): + + dynamips_manager = Dynamips.instance() + vm = dynamips_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + yield from vm.stop() + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/dynamips/vms/{vm_id}/suspend", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 204: "Instance suspended", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Suspend a Dynamips VM instance") + def suspend(request, response): + + dynamips_manager = Dynamips.instance() + vm = dynamips_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + yield from vm.suspend() + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/dynamips/vms/{vm_id}/resume", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 204: "Instance resumed", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Resume a suspended Dynamips VM instance") + def suspend(request, response): + + dynamips_manager = Dynamips.instance() + vm = dynamips_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + yield from vm.resume() + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/dynamips/vms/{vm_id}/reload", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 204: "Instance reloaded", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Reload a Dynamips VM instance") + def reload(request, response): + + dynamips_manager = Dynamips.instance() + vm = dynamips_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + yield from vm.reload() + response.set_status(204) diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index 4b401384..1edc2a39 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -19,81 +19,18 @@ Dynamips server module. """ +import aiohttp import sys import os -import base64 -import tempfile import shutil -import glob import socket -from gns3server.config import Config - -# from .hypervisor import Hypervisor -# from .hypervisor_manager import HypervisorManager -# from .dynamips_error import DynamipsError -# -# # Nodes -# from .nodes.router import Router -# from .nodes.c1700 import C1700 -# from .nodes.c2600 import C2600 -# from .nodes.c2691 import C2691 -# from .nodes.c3600 import C3600 -# from .nodes.c3725 import C3725 -# from .nodes.c3745 import C3745 -# from .nodes.c7200 import C7200 -# from .nodes.bridge import Bridge -# from .nodes.ethernet_switch import EthernetSwitch -# from .nodes.atm_switch import ATMSwitch -# from .nodes.atm_bridge import ATMBridge -# from .nodes.frame_relay_switch import FrameRelaySwitch -# from .nodes.hub import Hub -# -# # Adapters -# from .adapters.c7200_io_2fe import C7200_IO_2FE -# from .adapters.c7200_io_fe import C7200_IO_FE -# from .adapters.c7200_io_ge_e import C7200_IO_GE_E -# from .adapters.nm_16esw import NM_16ESW -# from .adapters.nm_1e import NM_1E -# from .adapters.nm_1fe_tx import NM_1FE_TX -# from .adapters.nm_4e import NM_4E -# from .adapters.nm_4t import NM_4T -# from .adapters.pa_2fe_tx import PA_2FE_TX -# from .adapters.pa_4e import PA_4E -# from .adapters.pa_4t import PA_4T -# from .adapters.pa_8e import PA_8E -# from .adapters.pa_8t import PA_8T -# from .adapters.pa_a1 import PA_A1 -# from .adapters.pa_fe_tx import PA_FE_TX -# from .adapters.pa_ge import PA_GE -# from .adapters.pa_pos_oc3 import PA_POS_OC3 -# from .adapters.wic_1t import WIC_1T -# from .adapters.wic_2t import WIC_2T -# from .adapters.wic_1enet import WIC_1ENET -# -# # NIOs -# from .nios.nio_udp import NIO_UDP -# from .nios.nio_udp_auto import NIO_UDP_auto -# from .nios.nio_unix import NIO_UNIX -# from .nios.nio_vde import NIO_VDE -# from .nios.nio_tap import NIO_TAP -# from .nios.nio_generic_ethernet import NIO_GenericEthernet -# from .nios.nio_linux_ethernet import NIO_LinuxEthernet -# from .nios.nio_fifo import NIO_FIFO -# from .nios.nio_mcast import NIO_Mcast -# from .nios.nio_null import NIO_Null -# -# from .backends import vm -# from .backends import ethsw -# from .backends import ethhub -# from .backends import frsw -# from .backends import atmsw - import time import asyncio import logging log = logging.getLogger(__name__) +from gns3server.utils.interfaces import get_windows_interfaces from pkg_resources import parse_version from ..base_manager import BaseManager from .dynamips_error import DynamipsError @@ -101,6 +38,18 @@ from .hypervisor import Hypervisor from .nodes.router import Router from .dynamips_vm import DynamipsVM +# NIOs +from .nios.nio_udp import NIOUDP +from .nios.nio_udp_auto import NIOUDPAuto +from .nios.nio_unix import NIOUNIX +from .nios.nio_vde import NIOVDE +from .nios.nio_tap import NIOTAP +from .nios.nio_generic_ethernet import NIOGenericEthernet +from .nios.nio_linux_ethernet import NIOLinuxEthernet +from .nios.nio_fifo import NIOFIFO +from .nios.nio_mcast import NIOMcast +from .nios.nio_null import NIONull + class Dynamips(BaseManager): @@ -113,7 +62,35 @@ class Dynamips(BaseManager): # FIXME: temporary self._working_dir = "/tmp" - self._dynamips_path = "/usr/bin/dynamips" + + @asyncio.coroutine + def unload(self): + + yield from BaseManager.unload(self) + Router.reset() + +# files = glob.glob(os.path.join(self._working_dir, "dynamips", "*.ghost")) +# files += glob.glob(os.path.join(self._working_dir, "dynamips", "*_lock")) +# files += glob.glob(os.path.join(self._working_dir, "dynamips", "ilt_*")) +# files += glob.glob(os.path.join(self._working_dir, "dynamips", "c[0-9][0-9][0-9][0-9]_*_rommon_vars")) +# files += glob.glob(os.path.join(self._working_dir, "dynamips", "c[0-9][0-9][0-9][0-9]_*_ssa")) +# for file in files: +# try: +# log.debug("deleting file {}".format(file)) +# os.remove(file) +# except OSError as e: +# log.warn("could not delete file {}: {}".format(file, e)) +# continue + + @property + def dynamips_path(self): + """ + Returns the path to Dynamips. + + :returns: path + """ + + return self._dynamips_path def find_dynamips(self): @@ -168,6 +145,9 @@ class Dynamips(BaseManager): :returns: the new hypervisor instance """ + if not self._dynamips_path: + self.find_dynamips() + try: # let the OS find an unused port for the Dynamips hypervisor with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: @@ -190,323 +170,97 @@ class Dynamips(BaseManager): return hypervisor + def create_nio(self, executable, nio_settings): + """ + Creates a new NIO. + + :param nio_settings: information to create the NIO + + :returns: a NIO object + """ + + nio = None + if nio_settings["type"] == "nio_udp": + lport = nio_settings["lport"] + rhost = nio_settings["rhost"] + rport = nio_settings["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 aiohttp.web.HTTPInternalServerError(text="Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e)) + nio = NIOUDP(lport, rhost, rport) + elif nio_settings["type"] == "nio_tap": + tap_device = nio_settings["tap_device"] + if not self._has_privileged_access(executable): + raise aiohttp.web.HTTPForbidden(text="{} has no privileged access to {}.".format(executable, tap_device)) + nio = NIOTAP(tap_device) + assert nio is not None + return nio + + def create_nio(self, node, nio_settings): + """ + Creates a new NIO. + + :param node: Dynamips node instance + :param nio_settings: information to create the NIO + + :returns: a NIO object + """ + + nio = None + if nio_settings["type"] == "nio_udp": + lport = nio_settings["lport"] + rhost = nio_settings["rhost"] + rport = nio_settings["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 DynamipsError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e)) + # check if we have an allocated NIO UDP auto + nio = node.hypervisor.get_nio_udp_auto(lport) + if not nio: + # otherwise create an NIO UDP + nio = NIOUDP(node.hypervisor, lport, rhost, rport) + else: + nio.connect(rhost, rport) + elif nio_settings["type"] == "nio_generic_ethernet": + ethernet_device = nio_settings["ethernet_device"] + if sys.platform.startswith("win"): + # replace the interface name by the GUID on Windows + interfaces = get_windows_interfaces() + npf_interface = None + for interface in interfaces: + if interface["name"] == ethernet_device: + npf_interface = interface["id"] + if not npf_interface: + raise DynamipsError("Could not find interface {} on this host".format(ethernet_device)) + else: + ethernet_device = npf_interface + nio = NIOGenericEthernet(node.hypervisor, ethernet_device) + elif nio_settings["type"] == "nio_linux_ethernet": + if sys.platform.startswith("win"): + raise DynamipsError("This NIO type is not supported on Windows") + ethernet_device = nio_settings["ethernet_device"] + nio = NIOLinuxEthernet(node.hypervisor, ethernet_device) + elif nio_settings["type"] == "nio_tap": + tap_device = nio_settings["tap_device"] + nio = NIOTAP(node.hypervisor, tap_device) + elif nio_settings["type"] == "nio_unix": + local_file = nio_settings["local_file"] + remote_file = nio_settings["remote_file"] + nio = NIOUNIX(node.hypervisor, local_file, remote_file) + elif nio_settings["type"] == "nio_vde": + control_file = nio_settings["control_file"] + local_file = nio_settings["local_file"] + nio = NIOVDE(node.hypervisor, control_file, local_file) + elif nio_settings["type"] == "nio_null": + nio = NIONull(node.hypervisor) + return nio -# class Dynamips(IModule): -# """ -# Dynamips module. -# -# :param name: module name -# :param args: arguments for the module -# :param kwargs: named arguments for the module -# """ -# -# def stop(self, signum=None): -# """ -# Properly stops the module. -# -# :param signum: signal number (if called by the signal handler) -# """ -# -# if not sys.platform.startswith("win32"): -# self._callback.stop() -# -# # automatically save configs for all router instances -# for router_id in self._routers: -# router = self._routers[router_id] -# try: -# router.save_configs() -# except DynamipsError: -# continue -# -# # stop all Dynamips hypervisors -# if self._hypervisor_manager: -# self._hypervisor_manager.stop_all_hypervisors() -# -# self.delete_dynamips_files() -# IModule.stop(self, signum) # this will stop the I/O loop -# -# def get_device_instance(self, device_id, instance_dict): -# """ -# Returns a device instance. -# -# :param device_id: device identifier -# :param instance_dict: dictionary containing the instances -# -# :returns: device instance -# """ -# -# if device_id not in instance_dict: -# log.debug("device ID {} doesn't exist".format(device_id), exc_info=1) -# self.send_custom_error("Device ID {} doesn't exist".format(device_id)) -# return None -# return instance_dict[device_id] -# -# def delete_dynamips_files(self): -# """ -# Deletes useless Dynamips files from the working directory -# """ -# -# files = glob.glob(os.path.join(self._working_dir, "dynamips", "*.ghost")) -# files += glob.glob(os.path.join(self._working_dir, "dynamips", "*_lock")) -# files += glob.glob(os.path.join(self._working_dir, "dynamips", "ilt_*")) -# files += glob.glob(os.path.join(self._working_dir, "dynamips", "c[0-9][0-9][0-9][0-9]_*_rommon_vars")) -# files += glob.glob(os.path.join(self._working_dir, "dynamips", "c[0-9][0-9][0-9][0-9]_*_ssa")) -# for file in files: -# try: -# log.debug("deleting file {}".format(file)) -# os.remove(file) -# except OSError as e: -# log.warn("could not delete file {}: {}".format(file, e)) -# continue -# -# @IModule.route("dynamips.reset") -# def reset(self, request=None): -# """ -# Resets the module (JSON-RPC notification). -# -# :param request: JSON request (not used) -# """ -# -# # automatically save configs for all router instances -# for router_id in self._routers: -# router = self._routers[router_id] -# try: -# router.save_configs() -# except DynamipsError: -# continue -# -# # stop all Dynamips hypervisors -# if self._hypervisor_manager: -# self._hypervisor_manager.stop_all_hypervisors() -# -# # resets the instance counters -# Router.reset() -# EthernetSwitch.reset() -# Hub.reset() -# FrameRelaySwitch.reset() -# ATMSwitch.reset() -# NIO_UDP.reset() -# NIO_UDP_auto.reset() -# NIO_UNIX.reset() -# NIO_VDE.reset() -# NIO_TAP.reset() -# NIO_GenericEthernet.reset() -# NIO_LinuxEthernet.reset() -# NIO_FIFO.reset() -# NIO_Mcast.reset() -# NIO_Null.reset() -# -# self._routers.clear() -# self._ethernet_switches.clear() -# self._frame_relay_switches.clear() -# self._atm_switches.clear() -# -# self.delete_dynamips_files() -# -# self._hypervisor_manager = None -# self._working_dir = self._projects_dir -# log.info("dynamips module has been reset") -# -# def start_hypervisor_manager(self): -# """ -# Starts the hypervisor manager. -# """ -# -# # check if Dynamips path exists -# if not os.path.isfile(self._dynamips): -# raise DynamipsError("Dynamips executable {} doesn't exist".format(self._dynamips)) -# -# # check if Dynamips is executable -# if not os.access(self._dynamips, os.X_OK): -# raise DynamipsError("Dynamips {} is not executable".format(self._dynamips)) -# -# workdir = os.path.join(self._working_dir, "dynamips") -# try: -# os.makedirs(workdir) -# except FileExistsError: -# pass -# except OSError as e: -# raise DynamipsError("Could not create working directory {}".format(e)) -# -# # check if the working directory is writable -# if not os.access(workdir, os.W_OK): -# raise DynamipsError("Cannot write to working directory {}".format(workdir)) -# -# log.info("starting the hypervisor manager with Dynamips working directory set to '{}'".format(workdir)) -# self._hypervisor_manager = HypervisorManager(self._dynamips, workdir, self._host, self._console_host) -# -# for name, value in self._hypervisor_manager_settings.items(): -# if hasattr(self._hypervisor_manager, name) and getattr(self._hypervisor_manager, name) != value: -# setattr(self._hypervisor_manager, name, value) -# -# @IModule.route("dynamips.settings") -# def settings(self, request): -# """ -# Set or update settings. -# -# Optional request parameters: -# - path (path to the Dynamips executable) -# - working_dir (path to a working directory) -# - project_name -# -# :param request: JSON request -# """ -# -# if request is None: -# self.send_param_error() -# return -# -# log.debug("received request {}".format(request)) -# -# #TODO: JSON schema validation -# if not self._hypervisor_manager: -# -# if "path" in request: -# self._dynamips = request.pop("path") -# -# if "working_dir" in request: -# self._working_dir = request.pop("working_dir") -# log.info("this server is local") -# else: -# self._working_dir = os.path.join(self._projects_dir, request["project_name"]) -# log.info("this server is remote with working directory path to {}".format(self._working_dir)) -# -# self._hypervisor_manager_settings = request -# -# else: -# if "project_name" in request: -# # for remote server -# new_working_dir = os.path.join(self._projects_dir, request["project_name"]) -# -# if self._projects_dir != self._working_dir != new_working_dir: -# -# # trick to avoid file locks by Dynamips on Windows -# if sys.platform.startswith("win"): -# self._hypervisor_manager.working_dir = tempfile.gettempdir() -# -# if not os.path.isdir(new_working_dir): -# try: -# self.delete_dynamips_files() -# 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 -# -# elif "working_dir" in request: -# # for local server -# new_working_dir = request.pop("working_dir") -# -# try: -# self._hypervisor_manager.working_dir = new_working_dir -# except DynamipsError as e: -# log.error("could not change working directory: {}".format(e)) -# return -# -# self._working_dir = new_working_dir -# -# # apply settings to the hypervisor manager -# for name, value in request.items(): -# if hasattr(self._hypervisor_manager, name) and getattr(self._hypervisor_manager, name) != value: -# setattr(self._hypervisor_manager, name, value) -# -# @IModule.route("dynamips.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) -# -# def create_nio(self, node, request): -# """ -# Creates a new NIO. -# -# :param node: node requesting the NIO -# :param request: the original request with the -# necessary information to create the NIO -# -# :returns: a NIO object -# """ -# -# 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 DynamipsError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e)) -# # check if we have an allocated NIO UDP auto -# nio = node.hypervisor.get_nio_udp_auto(lport) -# if not nio: -# # otherwise create an NIO UDP -# nio = NIO_UDP(node.hypervisor, lport, rhost, rport) -# else: -# nio.connect(rhost, rport) -# elif request["nio"]["type"] == "nio_generic_ethernet": -# ethernet_device = request["nio"]["ethernet_device"] -# if sys.platform.startswith("win"): -# # replace the interface name by the GUID on Windows -# interfaces = get_windows_interfaces() -# npf_interface = None -# for interface in interfaces: -# if interface["name"] == ethernet_device: -# npf_interface = interface["id"] -# if not npf_interface: -# raise DynamipsError("Could not find interface {} on this host".format(ethernet_device)) -# else: -# ethernet_device = npf_interface -# nio = NIO_GenericEthernet(node.hypervisor, ethernet_device) -# elif request["nio"]["type"] == "nio_linux_ethernet": -# if sys.platform.startswith("win"): -# raise DynamipsError("This NIO type is not supported on Windows") -# ethernet_device = request["nio"]["ethernet_device"] -# nio = NIO_LinuxEthernet(node.hypervisor, ethernet_device) -# elif request["nio"]["type"] == "nio_tap": -# tap_device = request["nio"]["tap_device"] -# nio = NIO_TAP(node.hypervisor, tap_device) -# elif request["nio"]["type"] == "nio_unix": -# local_file = request["nio"]["local_file"] -# remote_file = request["nio"]["remote_file"] -# nio = NIO_UNIX(node.hypervisor, local_file, remote_file) -# elif request["nio"]["type"] == "nio_vde": -# control_file = request["nio"]["control_file"] -# local_file = request["nio"]["local_file"] -# nio = NIO_VDE(node.hypervisor, control_file, local_file) -# elif request["nio"]["type"] == "nio_null": -# nio = NIO_Null(node.hypervisor) -# return nio -# -# def allocate_udp_port(self, node): -# """ -# Allocates a UDP port in order to create an UDP NIO. -# -# :param node: the node that needs to allocate an UDP port -# -# :returns: dictionary with the allocated host/port info -# """ -# -# port = node.hypervisor.allocate_udp_port() -# host = node.hypervisor.host -# -# log.info("{} [id={}] has allocated UDP port {} with host {}".format(node.name, -# node.id, -# port, -# host)) -# response = {"lport": port} -# return response -# # def set_ghost_ios(self, router): # """ # Manages Ghost IOS support. diff --git a/gns3server/modules/dynamips/dynamips_hypervisor.py b/gns3server/modules/dynamips/dynamips_hypervisor.py index 954d6d4b..9a1fb6f4 100644 --- a/gns3server/modules/dynamips/dynamips_hypervisor.py +++ b/gns3server/modules/dynamips/dynamips_hypervisor.py @@ -26,7 +26,7 @@ import logging import asyncio from .dynamips_error import DynamipsError -from .nios.nio_udp_auto import NIO_UDP_auto +from .nios.nio_udp_auto import NIOUDPAuto log = logging.getLogger(__name__) @@ -415,7 +415,7 @@ class DynamipsHypervisor: """ # use Dynamips's NIO UDP auto back-end. - nio = NIO_UDP_auto(self, self._host, self._udp_start_port_range, self._udp_end_port_range) + nio = NIOUDPAuto(self, self._host, self._udp_start_port_range, self._udp_end_port_range) self._nio_udp_auto_instances[nio.lport] = nio allocated_port = nio.lport return allocated_port diff --git a/gns3server/modules/dynamips/dynamips_vm.py b/gns3server/modules/dynamips/dynamips_vm.py index 3beed541..b73b1dbf 100644 --- a/gns3server/modules/dynamips/dynamips_vm.py +++ b/gns3server/modules/dynamips/dynamips_vm.py @@ -42,9 +42,9 @@ class DynamipsVM: Factory to create an Router object based on the correct platform. """ - def __new__(cls, name, vm_id, project, manager, platform, **kwargs): + def __new__(cls, name, vm_id, project, manager, dynamips_id, platform, **kwargs): if platform not in PLATFORMS: raise DynamipsError("Unknown router platform: {}".format(platform)) - return PLATFORMS[platform](name, vm_id, project, manager, **kwargs) + return PLATFORMS[platform](name, vm_id, project, manager, dynamips_id, **kwargs) diff --git a/gns3server/modules/dynamips/nios/nio_fifo.py b/gns3server/modules/dynamips/nios/nio_fifo.py index 768d87af..60c9aa3f 100644 --- a/gns3server/modules/dynamips/nios/nio_fifo.py +++ b/gns3server/modules/dynamips/nios/nio_fifo.py @@ -26,7 +26,7 @@ import logging log = logging.getLogger(__name__) -class NIO_FIFO(NIO): +class NIOFIFO(NIO): """ Dynamips FIFO NIO. @@ -40,8 +40,8 @@ class NIO_FIFO(NIO): NIO.__init__(self, hypervisor) # create an unique ID - self._id = NIO_FIFO._instance_count - NIO_FIFO._instance_count += 1 + self._id = NIOFIFO._instance_count + NIOFIFO._instance_count += 1 self._name = 'nio_fifo' + str(self._id) @classmethod diff --git a/gns3server/modules/dynamips/nios/nio_generic_ethernet.py b/gns3server/modules/dynamips/nios/nio_generic_ethernet.py index 4428e2bf..fc0ab006 100644 --- a/gns3server/modules/dynamips/nios/nio_generic_ethernet.py +++ b/gns3server/modules/dynamips/nios/nio_generic_ethernet.py @@ -26,7 +26,7 @@ import logging log = logging.getLogger(__name__) -class NIO_GenericEthernet(NIO): +class NIOGenericEthernet(NIO): """ Dynamips generic Ethernet NIO. @@ -41,8 +41,8 @@ class NIO_GenericEthernet(NIO): NIO.__init__(self, hypervisor) # create an unique ID - self._id = NIO_GenericEthernet._instance_count - NIO_GenericEthernet._instance_count += 1 + self._id = NIOGenericEthernet._instance_count + NIOGenericEthernet._instance_count += 1 self._name = 'nio_gen_eth' + str(self._id) self._ethernet_device = ethernet_device diff --git a/gns3server/modules/dynamips/nios/nio_linux_ethernet.py b/gns3server/modules/dynamips/nios/nio_linux_ethernet.py index 28bfbe89..513bc12a 100644 --- a/gns3server/modules/dynamips/nios/nio_linux_ethernet.py +++ b/gns3server/modules/dynamips/nios/nio_linux_ethernet.py @@ -26,7 +26,7 @@ import logging log = logging.getLogger(__name__) -class NIO_LinuxEthernet(NIO): +class NIOLinuxEthernet(NIO): """ Dynamips Linux Ethernet NIO. @@ -41,8 +41,8 @@ class NIO_LinuxEthernet(NIO): NIO.__init__(self, hypervisor) # create an unique ID - self._id = NIO_LinuxEthernet._instance_count - NIO_LinuxEthernet._instance_count += 1 + self._id = NIOLinuxEthernet._instance_count + NIOLinuxEthernet._instance_count += 1 self._name = 'nio_linux_eth' + str(self._id) self._ethernet_device = ethernet_device diff --git a/gns3server/modules/dynamips/nios/nio_mcast.py b/gns3server/modules/dynamips/nios/nio_mcast.py index fe32135b..cf03aaab 100644 --- a/gns3server/modules/dynamips/nios/nio_mcast.py +++ b/gns3server/modules/dynamips/nios/nio_mcast.py @@ -26,7 +26,7 @@ import logging log = logging.getLogger(__name__) -class NIO_Mcast(NIO): +class NIOMcast(NIO): """ Dynamips Linux Ethernet NIO. @@ -42,8 +42,8 @@ class NIO_Mcast(NIO): NIO.__init__(self, hypervisor) # create an unique ID - self._id = NIO_Mcast._instance_count - NIO_Mcast._instance_count += 1 + self._id = NIOMcast._instance_count + NIOMcast._instance_count += 1 self._name = 'nio_mcast' + str(self._id) self._group = group self._port = port diff --git a/gns3server/modules/dynamips/nios/nio_null.py b/gns3server/modules/dynamips/nios/nio_null.py index b8113e59..df666fb8 100644 --- a/gns3server/modules/dynamips/nios/nio_null.py +++ b/gns3server/modules/dynamips/nios/nio_null.py @@ -26,7 +26,7 @@ import logging log = logging.getLogger(__name__) -class NIO_Null(NIO): +class NIONull(NIO): """ Dynamips NULL NIO. @@ -40,8 +40,8 @@ class NIO_Null(NIO): NIO.__init__(self, hypervisor) # create an unique ID - self._id = NIO_Null._instance_count - NIO_Null._instance_count += 1 + self._id = NIONull._instance_count + NIONull._instance_count += 1 self._name = 'nio_null' + str(self._id) @classmethod diff --git a/gns3server/modules/dynamips/nios/nio_tap.py b/gns3server/modules/dynamips/nios/nio_tap.py index efe47a9e..926e9b0b 100644 --- a/gns3server/modules/dynamips/nios/nio_tap.py +++ b/gns3server/modules/dynamips/nios/nio_tap.py @@ -26,7 +26,7 @@ import logging log = logging.getLogger(__name__) -class NIO_TAP(NIO): +class NIOTAP(NIO): """ Dynamips TAP NIO. @@ -41,8 +41,8 @@ class NIO_TAP(NIO): NIO.__init__(self, hypervisor) # create an unique ID - self._id = NIO_TAP._instance_count - NIO_TAP._instance_count += 1 + self._id = NIOTAP._instance_count + NIOTAP._instance_count += 1 self._name = 'nio_tap' + str(self._id) self._tap_device = tap_device diff --git a/gns3server/modules/dynamips/nios/nio_udp.py b/gns3server/modules/dynamips/nios/nio_udp.py index 0bae8bbf..999fdf9a 100644 --- a/gns3server/modules/dynamips/nios/nio_udp.py +++ b/gns3server/modules/dynamips/nios/nio_udp.py @@ -26,7 +26,7 @@ import logging log = logging.getLogger(__name__) -class NIO_UDP(NIO): +class NIOUDP(NIO): """ Dynamips UDP NIO. @@ -43,8 +43,8 @@ class NIO_UDP(NIO): NIO.__init__(self, hypervisor) # create an unique ID - self._id = NIO_UDP._instance_count - NIO_UDP._instance_count += 1 + self._id = NIOUDP._instance_count + NIOUDP._instance_count += 1 self._name = 'nio_udp' + str(self._id) self._lport = lport self._rhost = rhost diff --git a/gns3server/modules/dynamips/nios/nio_udp_auto.py b/gns3server/modules/dynamips/nios/nio_udp_auto.py index eb42e580..1caaaea0 100644 --- a/gns3server/modules/dynamips/nios/nio_udp_auto.py +++ b/gns3server/modules/dynamips/nios/nio_udp_auto.py @@ -26,7 +26,7 @@ import logging log = logging.getLogger(__name__) -class NIO_UDP_auto(NIO): +class NIOUDPAuto(NIO): """ Dynamips auto UDP NIO. @@ -43,8 +43,8 @@ class NIO_UDP_auto(NIO): NIO.__init__(self, hypervisor) # create an unique ID - self._id = NIO_UDP_auto._instance_count - NIO_UDP_auto._instance_count += 1 + self._id = NIOUDPAuto._instance_count + NIOUDPAuto._instance_count += 1 self._name = 'nio_udp_auto' + str(self._id) self._laddr = laddr diff --git a/gns3server/modules/dynamips/nios/nio_unix.py b/gns3server/modules/dynamips/nios/nio_unix.py index af913f88..234fd65b 100644 --- a/gns3server/modules/dynamips/nios/nio_unix.py +++ b/gns3server/modules/dynamips/nios/nio_unix.py @@ -26,7 +26,7 @@ import logging log = logging.getLogger(__name__) -class NIO_UNIX(NIO): +class NIOUNIX(NIO): """ Dynamips UNIX NIO. @@ -42,8 +42,8 @@ class NIO_UNIX(NIO): NIO.__init__(self, hypervisor) # create an unique ID - self._id = NIO_UNIX._instance_count - NIO_UNIX._instance_count += 1 + self._id = NIOUNIX._instance_count + NIOUNIX._instance_count += 1 self._name = 'nio_unix' + str(self._id) self._local_file = local_file self._remote_file = remote_file diff --git a/gns3server/modules/dynamips/nios/nio_vde.py b/gns3server/modules/dynamips/nios/nio_vde.py index 79af96d7..6b00cf2f 100644 --- a/gns3server/modules/dynamips/nios/nio_vde.py +++ b/gns3server/modules/dynamips/nios/nio_vde.py @@ -25,7 +25,7 @@ import logging log = logging.getLogger(__name__) -class NIO_VDE(NIO): +class NIOVDE(NIO): """ Dynamips VDE NIO. @@ -41,8 +41,8 @@ class NIO_VDE(NIO): NIO.__init__(self, hypervisor) # create an unique ID - self._id = NIO_VDE._instance_count - NIO_VDE._instance_count += 1 + self._id = NIOVDE._instance_count + NIOVDE._instance_count += 1 self._name = 'nio_vde' + str(self._id) self._control_file = control_file self._local_file = local_file diff --git a/gns3server/modules/dynamips/nodes/c1700.py b/gns3server/modules/dynamips/nodes/c1700.py index 6bfe30e0..faee65cd 100644 --- a/gns3server/modules/dynamips/nodes/c1700.py +++ b/gns3server/modules/dynamips/nodes/c1700.py @@ -37,13 +37,14 @@ class C1700(Router): :param vm_id: Router instance identifier :param project: Project instance :param manager: Parent VM Manager + :param dynamips_id: ID to use with Dynamips :param chassis: chassis for this router: 1720, 1721, 1750, 1751 or 1760 (default = 1720). 1710 is not supported. """ - def __init__(self, name, vm_id, project, manager, chassis="1720"): - Router.__init__(self, name, vm_id, project, manager, platform="c1700") + def __init__(self, name, vm_id, project, manager, dynamips_id, chassis="1720"): + Router.__init__(self, name, vm_id, project, manager, dynamips_id, platform="c1700") # Set default values for this platform self._ram = 128 diff --git a/gns3server/modules/dynamips/nodes/c2600.py b/gns3server/modules/dynamips/nodes/c2600.py index 6f53ad14..e3972253 100644 --- a/gns3server/modules/dynamips/nodes/c2600.py +++ b/gns3server/modules/dynamips/nodes/c2600.py @@ -39,6 +39,7 @@ class C2600(Router): :param vm_id: Router instance identifier :param project: Project instance :param manager: Parent VM Manager + :param dynamips_id: ID to use with Dynamips :param chassis: chassis for this router: 2610, 2611, 2620, 2621, 2610XM, 2611XM 2620XM, 2621XM, 2650XM or 2651XM (default = 2610). @@ -57,8 +58,8 @@ class C2600(Router): "2650XM": C2600_MB_1FE, "2651XM": C2600_MB_2FE} - def __init__(self, name, vm_id, project, manager, chassis="2610"): - Router.__init__(self, name, vm_id, project, manager, platform="c2600") + def __init__(self, name, vm_id, project, manager, dynamips_id, chassis="2610"): + Router.__init__(self, name, vm_id, project, manager, dynamips_id, platform="c2600") # Set default values for this platform self._ram = 128 diff --git a/gns3server/modules/dynamips/nodes/c2691.py b/gns3server/modules/dynamips/nodes/c2691.py index 60489405..273b33de 100644 --- a/gns3server/modules/dynamips/nodes/c2691.py +++ b/gns3server/modules/dynamips/nodes/c2691.py @@ -36,10 +36,11 @@ class C2691(Router): :param vm_id: Router instance identifier :param project: Project instance :param manager: Parent VM Manager + :param dynamips_id: ID to use with Dynamips """ - def __init__(self, name, vm_id, project, manager): - Router.__init__(self, name, vm_id, project, manager, platform="c2691") + def __init__(self, name, vm_id, project, manager, dynamips_id): + Router.__init__(self, name, vm_id, project, manager, dynamips_id, platform="c2691") # Set default values for this platform self._ram = 192 diff --git a/gns3server/modules/dynamips/nodes/c3600.py b/gns3server/modules/dynamips/nodes/c3600.py index 42e2ea79..fe11b48d 100644 --- a/gns3server/modules/dynamips/nodes/c3600.py +++ b/gns3server/modules/dynamips/nodes/c3600.py @@ -36,12 +36,13 @@ class C3600(Router): :param vm_id: Router instance identifier :param project: Project instance :param manager: Parent VM Manager + :param dynamips_id: ID to use with Dynamips :param chassis: chassis for this router: 3620, 3640 or 3660 (default = 3640). """ - def __init__(self, name, vm_id, project, manager, chassis="3640"): - Router.__init__(self, name, vm_id, project, manager, platform="c3600") + def __init__(self, name, vm_id, project, manager, dynamips_id, chassis="3640"): + Router.__init__(self, name, vm_id, project, manager, dynamips_id, platform="c3600") # Set default values for this platform self._ram = 192 diff --git a/gns3server/modules/dynamips/nodes/c3725.py b/gns3server/modules/dynamips/nodes/c3725.py index cad3716f..6cb4213c 100644 --- a/gns3server/modules/dynamips/nodes/c3725.py +++ b/gns3server/modules/dynamips/nodes/c3725.py @@ -36,10 +36,11 @@ class C3725(Router): :param vm_id: Router instance identifier :param project: Project instance :param manager: Parent VM Manager + :param dynamips_id: ID to use with Dynamips """ - def __init__(self, name, vm_id, project, manager): - Router.__init__(self, name, vm_id, project, manager, platform="c3725") + def __init__(self, name, vm_id, project, manager, dynamips_id): + Router.__init__(self, name, vm_id, project, manager, dynamips_id, platform="c3725") # Set default values for this platform self._ram = 128 diff --git a/gns3server/modules/dynamips/nodes/c3745.py b/gns3server/modules/dynamips/nodes/c3745.py index 5ef49d11..28acfe99 100644 --- a/gns3server/modules/dynamips/nodes/c3745.py +++ b/gns3server/modules/dynamips/nodes/c3745.py @@ -36,10 +36,11 @@ class C3745(Router): :param vm_id: Router instance identifier :param project: Project instance :param manager: Parent VM Manager + :param dynamips_id: ID to use with Dynamips """ - def __init__(self, name, vm_id, project, manager): - Router.__init__(self, name, vm_id, project, manager, platform="c3745") + def __init__(self, name, vm_id, project, manager, dynamips_id): + Router.__init__(self, name, vm_id, project, manager, dynamips_id, platform="c3745") # Set default values for this platform self._ram = 256 diff --git a/gns3server/modules/dynamips/nodes/c7200.py b/gns3server/modules/dynamips/nodes/c7200.py index 777907bc..4d69e825 100644 --- a/gns3server/modules/dynamips/nodes/c7200.py +++ b/gns3server/modules/dynamips/nodes/c7200.py @@ -39,11 +39,12 @@ class C7200(Router): :param vm_id: Router instance identifier :param project: Project instance :param manager: Parent VM Manager + :param dynamips_id: ID to use with Dynamips :param npe: Default NPE """ - def __init__(self, name, vm_id, project, manager, npe="npe-400"): - Router.__init__(self, name, vm_id, project, manager, platform="c7200") + def __init__(self, name, vm_id, project, manager, dynamips_id, npe="npe-400"): + Router.__init__(self, name, vm_id, project, manager, dynamips_id, platform="c7200") # Set default values for this platform self._ram = 512 diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index b3324185..a2ca1f4b 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -42,20 +42,22 @@ class Router(BaseVM): :param vm_id: Router instance identifier :param project: Project instance :param manager: Parent VM Manager + :param dynamips_id: ID to use with Dynamips :param platform: Platform of this router """ - _instances = [] + _dynamips_ids = {} _status = {0: "inactive", 1: "shutting down", 2: "running", 3: "suspended"} - def __init__(self, name, vm_id, project, manager, platform="c7200", ghost_flag=False): + def __init__(self, name, vm_id, project, manager, dynamips_id=None, platform="c7200", ghost_flag=False): super().__init__(name, vm_id, project, manager) self._hypervisor = None + self._dynamips_id = dynamips_id self._closed = False self._name = name self._platform = platform @@ -81,12 +83,26 @@ class Router(BaseVM): self._confreg = "0x2102" self._console = None self._aux = None - self._mac_addr = None + self._mac_addr = "" self._system_id = "FTX0945W0MY" # processor board ID in IOS self._slots = [] self._ghost_flag = ghost_flag if not ghost_flag: + self._dynamips_ids.setdefault(project.id, list()) + if not dynamips_id: + # find a Dynamips ID if none is provided (0 < id <= 4096) + self._dynamips_id = 0 + for identifier in range(1, 4097): + if identifier not in self._dynamips_ids[project.id]: + self._dynamips_id = identifier + break + if self._dynamips_id == 0: + raise DynamipsError("Maximum number of Dynamips instances reached") + else: + if dynamips_id in self._dynamips_ids[project.id]: + raise DynamipsError("Dynamips identifier {} is already used by another router".format(dynamips_id)) + self._dynamips_ids[project.id].append(self._dynamips_id) if self._console is not None: self._console = self._manager.port_manager.reserve_console_port(self._console) @@ -97,12 +113,17 @@ class Router(BaseVM): self._aux = self._manager.port_manager.reserve_console_port(self._aux) else: self._aux = self._manager.port_manager.get_free_console_port() + else: + log.info("creating a new ghost IOS file") + self._dynamips_id = 0 + self._name = "Ghost" def __json__(self): router_info = {"name": self.name, "vm_id": self.id, "project_id": self.project.id, + "dynamips_id": self._dynamips_id, "platform": self._platform, "image": self._image, "startup_config": self._startup_config, @@ -138,14 +159,21 @@ class Router(BaseVM): return router_info + @classmethod + def reset(cls): + """ + Resets the instance count and the allocated instances list. + """ + + cls._dynamips_ids.clear() + @asyncio.coroutine def create(self): self._hypervisor = yield from self.manager.start_new_hypervisor() - print("{} {} {}".format(self._name, self._id, self._platform)) yield from self._hypervisor.send('vm create "{name}" {id} {platform}'.format(name=self._name, - id=1, #FIXME: instance ID! + id=self._dynamips_id, platform=self._platform)) if not self._ghost_flag: @@ -270,6 +298,9 @@ class Router(BaseVM): self._manager.port_manager.release_console_port(self._aux) self._aux = None + if self._dynamips_id in self._dynamips_ids[self._project.id]: + self._dynamips_ids[self._project.id].remove(self._dynamips_id) + self._closed = True @asyncio.coroutine diff --git a/gns3server/schemas/dynamips.py b/gns3server/schemas/dynamips.py index a707d0cc..d9fe845d 100644 --- a/gns3server/schemas/dynamips.py +++ b/gns3server/schemas/dynamips.py @@ -16,41 +16,102 @@ # along with this program. If not, see . -ROUTER_CREATE_SCHEMA = { +VM_CREATE_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to create a new Dynamips router instance", + "description": "Request validation to create a new Dynamips VM instance", "type": "object", "properties": { + "vm_id": { + "description": "Dynamips VM instance identifier", + "oneOf": [ + {"type": "string", + "minLength": 36, + "maxLength": 36, + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$"}, + {"type": "integer"} # for legacy projects + ] + }, + "dynamips_id": { + "description": "ID to use with Dynamips", + "type": "integer" + }, "name": { - "description": "Router name", + "description": "Dynamips VM instance name", "type": "string", "minLength": 1, }, - "router_id": { - "description": "VM/router instance ID", - "type": "integer" - }, "platform": { - "description": "router platform", + "description": "platform", "type": "string", "minLength": 1, "pattern": "^c[0-9]{4}$" }, - "chassis": { - "description": "router chassis model", + "image": { + "description": "path to the IOS image", + "type": "string", + "minLength": 1, + }, + "startup_config": { + "description": "path to the IOS startup configuration file", "type": "string", "minLength": 1, - "pattern": "^[0-9]{4}(XM)?$" }, - "image": { - "description": "path to the IOS image file", + "private_config": { + "description": "path to the IOS private configuration file", "type": "string", - "minLength": 1 + "minLength": 1, }, "ram": { "description": "amount of RAM in MB", "type": "integer" }, + "nvram": { + "description": "amount of NVRAM in KB", + "type": "integer" + }, + "mmap": { + "description": "MMAP feature", + "type": "boolean" + }, + "sparsemem": { + "description": "sparse memory feature", + "type": "boolean" + }, + "clock_divisor": { + "description": "clock divisor", + "type": "integer" + }, + "idlepc": { + "description": "Idle-PC value", + "type": "string", + "pattern": "^(0x[0-9a-fA-F]+)?$" + }, + "idlemax": { + "description": "idlemax value", + "type": "integer", + }, + "idlesleep": { + "description": "idlesleep value", + "type": "integer", + }, + "exec_area": { + "description": "exec area value", + "type": "integer", + }, + "disk0": { + "description": "disk0 size in MB", + "type": "integer" + }, + "disk1": { + "description": "disk1 size in MB", + "type": "integer" + }, + "confreg": { + "description": "configuration register", + "type": "string", + "minLength": 1, + "pattern": "^0x[0-9a-fA-F]{4}$" + }, "console": { "description": "console TCP port", "type": "integer", @@ -69,24 +130,351 @@ ROUTER_CREATE_SCHEMA = { "minLength": 1, "pattern": "^([0-9a-fA-F]{4}\\.){2}[0-9a-fA-F]{4}$" }, - "cloud_path": { - "description": "Path to the image in the cloud object store", + "system_id": { + "description": "system ID", "type": "string", - } + "minLength": 1, + }, + "slot0": { + "description": "Network module slot 0", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + }, + "slot1": { + "description": "Network module slot 1", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + }, + "slot2": { + "description": "Network module slot 2", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + }, + "slot3": { + "description": "Network module slot 3", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + }, + "slot4": { + "description": "Network module slot 4", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + }, + "slot5": { + "description": "Network module slot 5", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + }, + "slot6": { + "description": "Network module slot 6", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + }, + "wic0": { + "description": "Network module WIC slot 0", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + }, + "wic1": { + "description": "Network module WIC slot 0", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + }, + "wic2": { + "description": "Network module WIC slot 0", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + }, + "startup_config_base64": { + "description": "startup configuration base64 encoded", + "type": "string" + }, + "private_config_base64": { + "description": "private configuration base64 encoded", + "type": "string" + }, + # C7200 properties + "npe": { + "description": "NPE model", + "enum": ["npe-100", + "npe-150", + "npe-175", + "npe-200", + "npe-225", + "npe-300", + "npe-400", + "npe-g2"] + }, + "midplane": { + "description": "Midplane model", + "enum": ["std", "vxr"] + }, + "sensors": { + "description": "Temperature sensors", + "type": "array" + }, + "power_supplies": { + "description": "Power supplies status", + "type": "array" + }, + # I/O memory property for all platforms but C7200 + "iomem": { + "description": "I/O memory percentage", + "type": "integer", + "minimum": 0, + "maximum": 100 + }, }, "additionalProperties": False, "required": ["name", "platform", "image", "ram"] } -ROUTER_OBJECT_SCHEMA = { +VM_UPDATE_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Dynamips router instance", + "description": "Request validation to update a Dynamips VM instance", "type": "object", "properties": { "name": { - "description": "Dynamips router instance name", + "description": "Dynamips VM instance name", + "type": "string", + "minLength": 1, + }, + "platform": { + "description": "platform", + "type": "string", + "minLength": 1, + "pattern": "^c[0-9]{4}$" + }, + "image": { + "description": "path to the IOS image", + "type": "string", + "minLength": 1, + }, + "startup_config": { + "description": "path to the IOS startup configuration file", + "type": "string", + "minLength": 1, + }, + "private_config": { + "description": "path to the IOS private configuration file", + "type": "string", + "minLength": 1, + }, + "ram": { + "description": "amount of RAM in MB", + "type": "integer" + }, + "nvram": { + "description": "amount of NVRAM in KB", + "type": "integer" + }, + "mmap": { + "description": "MMAP feature", + "type": "boolean" + }, + "sparsemem": { + "description": "sparse memory feature", + "type": "boolean" + }, + "clock_divisor": { + "description": "clock divisor", + "type": "integer" + }, + "idlepc": { + "description": "Idle-PC value", + "type": "string", + "pattern": "^(0x[0-9a-fA-F]+)?$" + }, + "idlemax": { + "description": "idlemax value", + "type": "integer", + }, + "idlesleep": { + "description": "idlesleep value", + "type": "integer", + }, + "exec_area": { + "description": "exec area value", + "type": "integer", + }, + "disk0": { + "description": "disk0 size in MB", + "type": "integer" + }, + "disk1": { + "description": "disk1 size in MB", + "type": "integer" + }, + "confreg": { + "description": "configuration register", + "type": "string", + "minLength": 1, + "pattern": "^0x[0-9a-fA-F]{4}$" + }, + "console": { + "description": "console TCP port", + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "aux": { + "description": "auxiliary console TCP port", + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "mac_addr": { + "description": "base MAC address", "type": "string", "minLength": 1, + "pattern": "^([0-9a-fA-F]{4}\\.){2}[0-9a-fA-F]{4}$" + }, + "system_id": { + "description": "system ID", + "type": "string", + "minLength": 1, + }, + "slot0": { + "description": "Network module slot 0", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + }, + "slot1": { + "description": "Network module slot 1", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + }, + "slot2": { + "description": "Network module slot 2", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + }, + "slot3": { + "description": "Network module slot 3", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + }, + "slot4": { + "description": "Network module slot 4", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + }, + "slot5": { + "description": "Network module slot 5", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + }, + "slot6": { + "description": "Network module slot 6", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + }, + "wic0": { + "description": "Network module WIC slot 0", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + }, + "wic1": { + "description": "Network module WIC slot 0", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + }, + "wic2": { + "description": "Network module WIC slot 0", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + }, + "startup_config_base64": { + "description": "startup configuration base64 encoded", + "type": "string" + }, + "private_config_base64": { + "description": "private configuration base64 encoded", + "type": "string" + }, + # C7200 properties + "npe": { + "description": "NPE model", + "enum": ["npe-100", + "npe-150", + "npe-175", + "npe-200", + "npe-225", + "npe-300", + "npe-400", + "npe-g2"] + }, + "midplane": { + "description": "Midplane model", + "enum": ["std", "vxr"] + }, + "sensors": { + "description": "Temperature sensors", + "type": "array" + }, + "power_supplies": { + "description": "Power supplies status", + "type": "array" + }, + # I/O memory property for all platforms but C7200 + "iomem": { + "description": "I/O memory percentage", + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + }, + "additionalProperties": False, +} + +VM_OBJECT_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Dynamips VM instance", + "type": "object", + "properties": { + "dynamips_id": { + "description": "ID to use with Dynamips", + "type": "integer" }, "vm_id": { "description": "Dynamips router instance UUID", @@ -102,7 +490,214 @@ ROUTER_OBJECT_SCHEMA = { "maxLength": 36, "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" }, + "name": { + "description": "Dynamips VM instance name", + "type": "string", + "minLength": 1, + }, + "platform": { + "description": "platform", + "type": "string", + "minLength": 1, + "pattern": "^c[0-9]{4}$" + }, + "image": { + "description": "path to the IOS image", + "type": "string", + "minLength": 1, + }, + "startup_config": { + "description": "path to the IOS startup configuration file", + "type": "string", + }, + "private_config": { + "description": "path to the IOS private configuration file", + "type": "string", + }, + "ram": { + "description": "amount of RAM in MB", + "type": "integer" + }, + "nvram": { + "description": "amount of NVRAM in KB", + "type": "integer" + }, + "mmap": { + "description": "MMAP feature", + "type": "boolean" + }, + "sparsemem": { + "description": "sparse memory feature", + "type": "boolean" + }, + "clock_divisor": { + "description": "clock divisor", + "type": "integer" + }, + "idlepc": { + "description": "Idle-PC value", + "type": "string", + "pattern": "^(0x[0-9a-fA-F]+)?$" + }, + "idlemax": { + "description": "idlemax value", + "type": "integer", + }, + "idlesleep": { + "description": "idlesleep value", + "type": "integer", + }, + "exec_area": { + "description": "exec area value", + "type": "integer", + }, + "disk0": { + "description": "disk0 size in MB", + "type": "integer" + }, + "disk1": { + "description": "disk1 size in MB", + "type": "integer" + }, + "confreg": { + "description": "configuration register", + "type": "string", + "minLength": 1, + "pattern": "^0x[0-9a-fA-F]{4}$" + }, + "console": { + "description": "console TCP port", + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "aux": { + "description": "auxiliary console TCP port", + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "mac_addr": { + "description": "base MAC address", + "type": "string", + #"minLength": 1, + #"pattern": "^([0-9a-fA-F]{4}\\.){2}[0-9a-fA-F]{4}$" + }, + "system_id": { + "description": "system ID", + "type": "string", + "minLength": 1, + }, + "slot0": { + "description": "Network module slot 0", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + }, + "slot1": { + "description": "Network module slot 1", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + }, + "slot2": { + "description": "Network module slot 2", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + }, + "slot3": { + "description": "Network module slot 3", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + }, + "slot4": { + "description": "Network module slot 4", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + }, + "slot5": { + "description": "Network module slot 5", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + }, + "slot6": { + "description": "Network module slot 6", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + }, + "wic0": { + "description": "Network module WIC slot 0", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + }, + "wic1": { + "description": "Network module WIC slot 0", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + }, + "wic2": { + "description": "Network module WIC slot 0", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + }, + "startup_config_base64": { + "description": "startup configuration base64 encoded", + "type": "string" + }, + "private_config_base64": { + "description": "private configuration base64 encoded", + "type": "string" + }, + # C7200 properties + "npe": { + "description": "NPE model", + "enum": ["npe-100", + "npe-150", + "npe-175", + "npe-200", + "npe-225", + "npe-300", + "npe-400", + "npe-g2"] + }, + "midplane": { + "description": "Midplane model", + "enum": ["std", "vxr"] + }, + "sensors": { + "description": "Temperature sensors", + "type": "array" + }, + "power_supplies": { + "description": "Power supplies status", + "type": "array" + }, + # I/O memory property for all platforms but C7200 + "iomem": { + "description": "I/O memory percentage", + "type": "integer", + "minimum": 0, + "maximum": 100 + }, }, "additionalProperties": False, - "required": ["name", "vm_id", "project_id"] + "required": ["name", "vm_id", "project_id", "dynamips_id"] } diff --git a/gns3server/utils/interfaces.py b/gns3server/utils/interfaces.py index 701bce48..27774eb3 100644 --- a/gns3server/utils/interfaces.py +++ b/gns3server/utils/interfaces.py @@ -49,7 +49,7 @@ def _get_windows_interfaces_from_registry(): return interfaces -def _get_windows_interfaces(): +def get_windows_interfaces(): """ Get Windows interfaces. @@ -94,7 +94,7 @@ def interfaces(): return else: try: - results = _get_windows_interfaces() + results = get_windows_interfaces() except ImportError: message = "pywin32 module is not installed, please install it on the server to get the available interface names" raise aiohttp.web.HTTPInternalServerError(text=message) diff --git a/old_tests/dynamips/.gitignore b/old_tests/dynamips/.gitignore deleted file mode 100644 index 39ffa4b5..00000000 --- a/old_tests/dynamips/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/c3725.image diff --git a/old_tests/dynamips/conftest.py b/old_tests/dynamips/conftest.py deleted file mode 100644 index ff70cd58..00000000 --- a/old_tests/dynamips/conftest.py +++ /dev/null @@ -1,29 +0,0 @@ -from gns3server.modules.dynamips import HypervisorManager -import pytest -import os - - -@pytest.fixture(scope="module") -def hypervisor(request): - - dynamips_path = '/usr/bin/dynamips' - print("\nStarting Dynamips Hypervisor: {}".format(dynamips_path)) - manager = HypervisorManager(dynamips_path, "/tmp", "127.0.0.1") - hypervisor = manager.start_new_hypervisor() - - def stop(): - print("\nStopping Dynamips Hypervisor") - manager.stop_all_hypervisors() - - request.addfinalizer(stop) - return hypervisor - - -@pytest.fixture(scope="session") -def image(request): - - cwd = os.path.dirname(os.path.abspath(__file__)) - image_path = os.path.join(cwd, "c3725.image") - if not os.path.isfile(image_path): - return None - return image_path diff --git a/old_tests/dynamips/dynamips.stable b/old_tests/dynamips/dynamips.stable deleted file mode 100755 index 0af011ac3287c7dc01ae68b8ac3dee01542f04ce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 961958 zcmbS!3w%_?+5XvFU_o$Kg&Gwl>QaLjOcXQ`qPrx4v$$vkf>Fc-a)C%7F*yst3j{aO z9M))TwN-0dytGBDEnbR{YA)OY)@r~DR9cBBdX^Qz8$=ZIf1WvKcPGexU%!9&k#nAR zZtuMF&O0;j%$zLG^G!;y*_6mnf^xM&G=HjFkj&qTQdd7jVE%F|naUviK1VrA833Ft zXbfSPyWg|d$~yA2i%>;L#4`#1GZRcgW`bM1M4lr|vJ!blIOLP%#eUq@tNDEG=92`D zJQXEXV5TFlt7n_xt7k6|^vLtkbIlSmuP~pHbYzd|I;?aZR=UVD-Qqv;jMR^H2TM5V4yNC0G28JsFoj`TMj|z7PA&Ac2Sd#@p{%d7XetVjz(z&#zwVN{%quRrV(ggOsxg;|dON`g<4(S=K(G{% zV+xR{AD+|!hNq<8ci&pWd)gJJ4!&pV*gLPu9Wr_d!`9$G^I*La8u4ZukwDsu`2RBg zk3ReU5gjidIW_C6;E2mM9Xa*or;eNo@(F)lN1-PQ{0gy8M1K&4o`}9?;ECzqg1r;P z|2$5)cS0a1ivLiYd=lc6J2DR4iB58&{FON6?u*0c>NxW0iWC2aICA?mPCldK#i{QPap-Ty$>+&9a{hIke744s!!>c}w?g4h)L#BL`EQ7m&x|2GVM{ZBXsn_8+{4a@<|FSsoUy2j|PjSk9H%`0%E>3*| zapZP1PW&Np3S04nNg#;&V_wQTw*XiQg2bUb%7Psm9@dMVxZ?#-T5Z!_TX6+U2G=@zbEssmBjS zz)R!QYg(Lqvf}9ZusHm$iNoipIQd^5hyHS${5iooQ9FJgr`(J<^jUH885^g4YazQ6 zmHXQ``F|EiA9lwncS@Z4+T!HDA`bnVIP|7C<*tkqzd25QhsVk1XK~u``8f0o;?(!K zIPx!x!_S*>=qJVDGa(K?d2#y1m^k$6IC8rqPQ8-i)a#)*`8eXx{}hM+o8rX3BM$$U z#>wYs9JyT|N1w;V$^W-;^4}6C|9`}h&oy!Kzco(&gX75mjyUpJ9Vh>laq4>|^eq+t zkN?>nr`!kQ)T=yB{-?#^=gK(w|1D1Z*W#4>dYt&3aro(sQ|_2J?YJ;bKD*syn@;Z?{-w=oXWSshj;_!1y966j9N1k`YDYrFF{AqF8 zu_zAxU>x~;4gOD2&QRRTE5U)HMEoo(kPqp_79C;~KMTPR>GO|^KOPZC&p$4Hp%uUI zxcCbEF{PEJ|E0(~V`@=pSxwozidwC#X2#T<%Bn?WGv>^#EK`b#<}IvRR8*_YsnLpx zl%gq%i>6ueaw_N4)|S;OBKZjumsXZkR@Ig%rIocy7vgtSb=e|3%4%w=YREv1Mg}qz z#41@(G_Oq4Di)S8!0)dpHG}w8~Vn&f$9i^^)|Xd-!8Wx1Ie zFGY*XYHBO07AbhY{4y3&JHNDss0e&4teq!b=b}zhl(I;aTCJv9Gh;JliHK8Fwx~3c z!J;biTdmEnDVrl$PFE~YR8qC5w5VcHg%+hO^lN2xkTi0wTB_7Co0_tc#pN|s3!`yr zE9NbnQ@bFF%{2Y85g9L;BV{Zok$fZNi%i>its z`8gWJ(=Qdt$FU$X`Y9V=chQ{6XllU-Td`E|H>aeetXfklYs<pm{zal* z6g(TAvgeZxSTd)YDw94YQ{j72sefU0?5#*>t_Wb_>NzC~%20G9Vo`aezjnTf7nRjn zA>}n?WpgSkOUoA5nnBD2tv{#Kgyj{BO8bSg%p$YLh^(bov|wok_%)k~<%-NID;7h5 zf{i)a9C%h26fK&wu&hX{Dq4(ILB&c~t1FaRv?n;8S6MZeY_Tm&o}r2*=t4zB)io81 zwDO{o`3sbJb1Kl`Ys_wnmqjIzMa7~rt!R!0O{uEZz`25env(f7N_ACjMP1RtvV|yr z-Xi1#<1z0x)&yAnQu{11(?dpDypq5nX{-|Dc5TJi=d0- zZQ;T>)p!&Ym6w)PY9>$MoLp;Vm6awhi?ph0GO>vKSNk;>BAO`}5)@oiKBuBmL3=|a z3@n*HhZYCs4LVjZKYOBl2N|kaN4h8X87$Q}Yg^GD)3#+xIh{dwz zl$JvJ$fjlXuzyfTqDWLa+NUCkU6T~xHVc9EPLE9tL<3^l40 zR6#+zvt~7A3#*`vpv zWvK&mLbIWMlnKigZN|Q84YLd9z!H{Q-EV0vEE~E9dM#R`q;3v6VObq)|74$cVouRz zV=fyrPAMqx<`!j)NskgQ$F>S~>I!Cfebf5|WMo9+Tp4}K7#n*X7k!NpFCR0OlaB=K zz$D>!viQ&65i|hr48^2NybTm_%$TGli#@mmWuORUT&6Z-Cu7bN!O7Ss#DqxX#P9^T zjm&(_*bFmMn!n{7Bl($J#ZaO$SmZ@GueIuAF6IP4JhbjIyxFUqY0>Lq+EPEDBX_A_LY>WL{0pSrZ}R8JQ=O z&b`6NPh_4<`YRD4o{@Pt>D*6@{6yyAq`wp);+axx@I^Wy+@*-CD9Fu9ytTS;6oDqY$<-P zL_bHOD|2FcJ5r+CC3>nvcS`hgC3=cPKTo2kO7!z3dYVLcN%VAyK1!lzO7x#gbhkvm zK%%P>Jx!wLOZ3qay-=cGDA9{0`b83bzC^!RqE}1wOC)-oL?0v3>m~Z761_>Hr%Ut} ziJl?RAC>5rN%U0`{c?%ETB3`=-U?eI(Z@;g*Gcp%BzlKLr>`#ZvsI#JMu>QJOY|&> zzDuHCCDD5%`gn%#G>)H4@z}(cKc=DbYO=Jw>8Vl<27v zJx8LaN%UNao-WbzBzmSqpCr-U5`D5nS0%bC(eov`SE3h6^eGa(SfXDm(dSEapG2>g z=u;(nokX{eD2dQ|iGIBlze%F!OY|0reuG4RRH9Fl=&L09bcw!NqR)`%Yb3gPGRX{F zC(&n0@jE2?jS_vUL@$)+-4b1%x9pPWH%sw*B>F6gZbEwVK3k&qO7vSLx>6F; z|00QQm*~Y3-6_%MNc0qmK3AfrO7s$mo+i;tC3?CD<=<_A|{Sv)eqBl$QI*I;( zM6Z|V4@&eViQXd7TO|5J68%w${!59zN}@k3(N|0KMTL{F3Gf0XFy68%pSJyW8umgsJY{;Wh-CHiv`Jzt{# zS)vz8^yejdu|$7CqR*G;f05|b68*0dT@8G0SA)sFO}|J{8#}aQbq6RkEH%gK4 z+BfbOh|)LeVf=QCbmNU!j9{UjKKzVYNo+&ZP`AMM5GN3K2z&=|BJmo5YlxGGR|$L@ zF$z~gEdtLa9za|#@Xf?jhfuY^(}?ZF#R5+u9z>ik@I>Ol#BPDFB0h;YUEs@zhY+U< zd?E2rVyD385uZ$~2z(~7gZR*oAdEVd*h$dx?KWTrcnk#HSNi3%r9kg}7MYH;B(5&KGzC@tMSKfm?~sB2E|h zCE^jpsRBPod^WLD;HQYsAyx!_oOmShp<}H7FNjl#djwudd@gaf!1oZJN8BOs9mMAo zuMxP0*hRcb;M<5t5w{3Dm-y$z^#b2ad;xK_z|)A+h>HcDLOhx{U*L(v7ZSS#zKZxF z;&g#8Bfgk8Rp1MWFClgcd>-)_VnyIHi7zES^n+-B;&kF3fd><35O)ilK+Gj?s6*hR zV}LIwUL){V#AAt93A~?p9C3@ldx@_gt{3QlNNn9-O8^l?}`2ueszKYl_ za4Ye6;&g#uBA!5;D)4i}*~CtPpCZ1RSP}Sf;%kTxeJ|Rd*iGCc@JeD2aks$t5KkoT z5cm$_9O5+s*AV9tuM+q+;ymIOf#(uWBCZ$sX5z`j)dEi=R*8!RoRDmxfo=WT#_&nn4h!ug)B)*>b&{5I;#QDTM0uLs>fw)`X1Y$1v zLLCAhy##nV@fv}@BA!9KO5pv(1;i}^?sRBPoJe$}l@KeOM5-S2fPFzHM=sVH=#Kput0VL)+#>K?;(Lkf1-_a1KH_SDrx7<17YjUv zSSQXGcp~viVzaW(0^dyhC*o>> zrxC9vE*5wS@w3GF0#77i?Xs>hEBp>{Nrb_TaP>HQ4SPswhU`>I6=jReg)2fr=D#r>nu~ z_SH`93U9EDQGCt0t|7U4$e6k&K?&UD8e&s}r|AU>qp5nH{fv$y-GQD2y&y@?bB3p? z!Ru1h#%)^CYMXX$aFX4p@9XS2)3zaTdg?_pot~~-mlOOdUVVnk9v-O%6OmB0y`dto zE6<4#MbA@Iy}%w$$T)yWyi?VUS+hO2cxHQU-T2?vs~(q)BK4wz$Lf`Ne7*dCjnxYU zM=IvDDn?%Pyp-_u|9j0+A~kb{&qd9cGFr71Rt@rE-JB<^+l{JzXsZ6U+G(8anHlIA z=+WQu6nNQ$d7=rAp$WfMJHrFfjD;&c?&%w-sD1ZkqKVYRk%8W1NOT0mI{}K_=V+kS zaNM^RzXQEV+I6Al@#YIQxF`&@d4?-h)&G^Nf2!&Sjkhp2gsdEmod^s1TqAt?1XrH^ zWuD%{Bb$m(|ANsL=G&Bvju5*WIBmtF9>ry{&gv-T5c1G-k*%$p&oo3C)#pHK=DOVB zdx01iQTxSB1}^?d^w12{K=5dl{PHTB!i8OEJOYkmFbP>$(cZkC3_@oLBMRdw+UTKJ zJwmK$o|zu~eNRE2zS%RwtAB%T@juEG6Uj%Fm_%PhO_#VqtXCWj&k&?QXbns72BKSW zH15J{Xr76i5NWyj+Jd0h8B}HBH3%xj>yopOZWX|dCVe&HJ;HdCP27Q?Gx&;HE81*C zUWXX9j4=woe%hL%nRa2Oi`Mi(PAQH?UfuQSL(rT?p8j2~{-xETqTzpYooLZHhyx)} z@kT+Zl6)$EMEy0l?9Cj4k(nZ1qe2Ppy99>iBe2!$|5^rx(FX=z^Gzur6l2 zmA|8+h^lK3% zZ~W$3s7%jaa+Pw&$XwW5xJ3H8y}?{pFBHUo$XIX-Mu2;V{N z{)PD3qX|lT$5@YIcrLS>SsTCZ0gYT5zr|bNn9cD3O-pmZk7{#{U2XQg;|!l>mZt_f z>}u9lM+0XpYS3AK)g}IL{T(*dzpws|1jWCnD|xj0VsWHH)j!k^dIN9v8TWmoC|$|f zS6_@YP(1RTm(j6aS+GE99r+w&!kC8b%f7xqFU;IoXo)~?rK5qDZygUl2F?P#s-xjd zG(e#DI!D9Tpa*)d@wb{x*Fi3U?7v-u?ENc6UR}v+fJBW>LfNes{5euc<<~6x0A!_& zY8?!2G7hVHmr?L_Utim3u;S4->l-2h|2+oeR!@|{bfjg%;Znj8NT?@ILTRnJ(TD@h zh+ciuy3JR66`v!w2RduyqvIpJqv7L0?CI&b`WNj_uT-IQUl}vb6k$l>Xy`+*U}0+W zD=xZ@rZzwCQt|9c{%(RPL*tVVBTcp+Z{=egBO8QpqjJq(9jWrO2ZdZcdY7Ytkvz?l zJF^oY9LK%9B~9IdtIa*1RqM#re*~+2-GL69|7DM(ZD-qdR4XEo2fpd+3*BQ^sC;I5 z+5FCfqv2XKS@wD2^?UrzPUUOk>F8q_eMTmEeU!@Gb^z&Zpau@u#$S?($~YQ_B0II@ z07_MZW5dR*(`;aAhHJhkvn`C$q58v7Fh%%+9b!UTXG08*1_zS{vsVMPz1V~NmqCS4 z4OLIRA2bY@5E>Y`(Fc9$usIsu#HeU2g`GFP`N$^xg~s?x5)@;So`6!;{~Xd$_0PWk zbH)x;|5Pxbb|ty;ukHbKiVVF8Dzg4I z3mwhuw#JjlTu=VT_=}a$Ul8gGj@yaQwyTh*Pd{Qzqj;>cohaKk`VcZ`Ky>2$B&93Hd7}6UEdEjy9~_s# ztb^I-;bA;tBr72=6ur;2xdX;0qt7w29*il*_}!6uy|^Fs@7x@jTSAgn) z%CY4rqtGZ*vWGKc)qm;>#L*jEs~DbahQBX`-(!WlFd+-=GsU+S;TbzJ4jYXSPTSq+ zTVCXlh8Swh>OG4QDD;Y%ppgjPgIt4^WLmdV!FgXDUs8RPD1|l8RdU-bb5@V(!fQrTD zXQa?3q=4KWVWg(_O>^1!R}e$LgjF*hNwc$^%3F+!5^a_Vdl7eSp}I%GIa1C-N}*bV z!ExJaOWeMg_@y7tYTCab1{lRHp3Wl<2oy33`0&u2-lc1MAgm`IgI;! z+{GX^xW>YE(h9?<7>9NyV5aD3=*4Oo^Q(U$O6w&E6K1CU5Lhx6Qp}yGIf{B#%7T_V zVpqJB5#%xfS%;oSUX)}#A{cM)6IHE7LgSqf0uYM6#I%(g(D8-6!Z6)~K;vArn@{gM&Q|V*7(q2IM`-9|i#yTI z876l-#-glo;4ae6tXs65|Bbm4>iz;FwS+8&x_hM*45iX`Li z0F7B@MkRj4SoXdt@q0y-q4N->bu{uZC7y~Yi19soQ+OJRyh=(hi~9qz3PHLl?rbxo zbC}VnXhto}=p|&N1}`UDpY3OZe`fq1I&2Q7qcCd-eSzs(`zc6e+-u5Go7Q###;EnA zq4RzH3?>G5Lf|1**lKPL6*qzfJ^6Nqlp+r_V)Nx_7Q58X9_Ao4Sp*KPM_}vOQSM$w zGz_@sAUS`;d>}MeM9nThR1C7MlMzP<#W)Mq2<4ikTyT6TV^~TS+***=nF#c(Gv|k2 zwjRPr7#S{iahSB3ynJ&RcsV!9%T_9R6T|l}H7{?;IN;T{->T|+)xal* z@@IIvI(ll@44wJn(WSWEigEp?eSP1Qx5Q@0z9wl~PbD6y>@nG8vAF5qB!b68=3M+wrrRob(jYP;_4Py0Y%X6#2JSC7f!4`=l zvd(d8Nvd9$qUy6nXZ#An#&R)D4c@}_M5W8=3zoW4e0WR6LOBi9&E;$_{9$={tVLJ@ z!F=;FQ6*8EUQ{K8^=ME?=GKy7na+QING3_;_vY_F9%gLhKBkCKF?gSd>8q2G}5i328(*V z`e9%50{Bi38h?VlflPbU(K(dup}+#jHdtVws*Z-|s7ZQ0oVq0JppYk~AeGvccZdyy zr@^PO0v*BIycliXXEd6E!tznO+oxBHQamWdT-@1MOkyBsg3XI@)RY%z#_-!}NsueD zKc9SEDsrTw?*wh=3u>-3U+^whI)sx6;nb50RX;5>6WSSB4>FsrVvd9A#mbE`&&JF% zSZWR&%`g|`H!Ia{q*G)mtiT*dlY>{S5*Cd#3G*2`X72Z@BkzbGKq|7hYJ<&I@YV$Q$?Q%45vnHw@&r*oND)i|x zwAyDzP|VhHpd0LWIfnuZsTSFUJyj};xlwgOD}Rvy{KS)yK;6RV<<~b51)`T zUPZVRzn4MAcmJmRPt@e1+gg4fIMKsR>#NyBT$M;oGzg-K`U4Lm9rIWh&Ep0(^X5HNA=n4c3}5p+JH5CN#&qLu>UHpLvEm&r>}9aX zX-o#kYU6ISji22@6w#G8fls6Y#m!ePc|*7C?XN%Z!+mf38VY+jC)1`4nibRIJ+ndw zcZEPPH(550p<iGs*i&q-DF>Byh&7=cQ1oT`3v>Q7TNnJG|WPC8Pcba0>-;{VO*Y1<*iZiNnG!_DW({hm}-DJYV`3o?XdRAoGXW zL!YO&w*WwKoTX|oX;c~k?5-VNkB^}9rpz%doq%MHhH8!#Fak4?rU@KP3~2h4`JM?9cYHwCBNh0-pHQyTOFWuV2}Y0+}p*A%!F zqZ#)dX{)f&SgKw=Ovuwe04naBwb57uEnI)DAJ(M+oU}DSHtlH|1T+ugG6Uc|w*nLf zTo(-(9}RenA{~bS(;nSudh@;d4+YaL=ZTYKbp(3uR)B3pBk5-;b9U6T!e);w0LI1nsYlt2tZqeI1eEXtNan{KC8Tb zn5gm{Apdt&K9}ul);Ak+j@I}5Xno1$QxJv8oZtmJ#q6)aoM8dO*4Uxv3|n>@qum*e z2G{1T8`(?hzDNCn*`ZOW%PswbTRfsY#SUzwJr8Sjs3Yp9Vfmi@GE!kj(MYsvxBE2K zF8c|PS-T}CYS6#R4SK%N8>@b^p^|3(YMt6eQvLdj4XFBwTEWx@PK=7pqR)*M?a|+z z(wwvp4Xt=PKN;X1y=iLVpIEjR6W0`P*5}$t4wl$DH9mngG7I)QwH7tFkiq_sFr!Gt zYyxI;k59kIG}1&g(lC)hm`Guy`J9+;+zQ7MWb%6#BzdaEZ$^itGRNtMqk#ugkYXWH zP)ABy+a{L<8u;nC`#U&QnzV>eZx4nh_qoOPmrdJ@PPhH29* zd-%hQ-Qm5l^+tbvHdjaB_x1BleurB8?t{w!-T03n|9|mYwksn36Y#s0BLzofiC@9% zUhw)UW*^dg*VI?5`$g@~R#VCYO|Cud?8wFq=(d#gXp{H-=l!?5Ul8k$FYJ`~zC5b0 zW(kz_5b_S&k*oJc_=i=A$v3cIn5`~3FQYHK9b%mc9hL%~>-JL8M^I7)KZ|aB}C@>1g=Wlt41F@aW%I61X1u@>IhGPPX19 z#<`{h{B<}c)134lB!LnB7)E%bH}SXiK)zsYiZAPH?ULyD<_%tr`H%63_f0wEjW0*P zof=%sX#TwsIUF>S(7=#G+HrDd!N`vp}Aw&wekJ_MJUBn7j?|Q<{fK=Hy*p;wlc&Uaa!cyjeeJ zqgtk7E+4E)(*|Kj1a`&Kw==NO22%nt`#(k|4akHGqHiED>P1Ygp%&09wx?Gd(8xGo z9?;+@nt}!N5U?|e>^#QqTT&?qweb_J)*D<7 zeK_ptdlwQd!$|4X_h{qtg4Yl828+`2aAIkfH|WTPOowpNkli&B&akG4A|kTUOm)Tx z`wxV!N2qbJS&x+5*a@Y6`i;f5C$QOO48u4aZZ2=Z$rDyAFXIzyI0&(SiN48IEQ!uA7 z-DpG6KO?>J-^eQ&d3gexY~H};%-pQvy5)au--bfL6F4%i7X@bboPz>yq+{+{;1;6B2E z@zz_+ExQW2#wB|Rz<@UC8ikIgGjj!l% zL@b&}N5gNiMc~uZOkY+pw4fNZ$b)5Sr;cKhlIjTzOepVir{N#~^(Cv<(ZExnYEVrr z4=hc?$pW&W25(4JgEdYy>q|4dJg^L54bO-$h$jn&ByxprKThuu0fgp2B#7b+72?GrhGEv-?9M1I z)~B+?grtA#XxW^wZ~o_xHAohdrlF3Z;!DQTL6JP z#l|Xtce9tp_@9LSRhOptZ-u3B(txm6O+t?sUE?~@+eO!4FGsf!>Uo{nHL%@j^7T)? zjK3Xx%@%xlpflAF#3ZDFg$PbUo$6qJRTQ?I%;6(e3e6H;|IW0*nJJD}m!*~Kbr@tO zf)BHI(*_BBoh10^U>B`se&B;^v`Oq9j)t2BAMo7VAo!>T9~=2br<)FAC%V!jR5EqE zc$hAW^1vNnpz&qVj%4#X*uOVmr`e5#&1`sHgu#XcJx+`D!L*JL8CahwMEn(p9LKA7 zfcvH3-ZU+gB4v3Z0|U;5WGn+?une~D9>E|D%zTk?%)p>W$lNzhq``w-#xCy}WHx%< z4Sd3~QEZV$L}#BQ>{~zKd1As#W?6K5{UC~`@J5Q_RS1tP&=y+K3CfaaJS^lyNtmxF z7g_0Fg?_~V%iambLj}iZ^4tjh8!HkzpA``8-^go==D@|Ivw+0O-o)+B1UrXe1zR&Wabd!P(&g|0^-=seC)+YgqN{H35Y z3*H8RH&t&G@8iI?s+)CqQM|uSdiW3M2Tp5K`Fcp)ktMB>yx2p>?)M}hEP~Am+uT^ih}E$p^F7)9%txxBFJQ=19>>Z zt(LrT6z+UsRQ2r;ry3Naf$<2Y=VFkx(^SF_E6nvq&bRMlebnX~3*o-hQYggJ)FAq9 zbAC!_+2Ke#W^}+}AYE{vFz8Wz6aH#UT#V^Foza|AX!`BNJjQdl!{*bw{L{tuKpEZh z{w4aCVr#&Cc+2s710eO=9Qf4S94PM+j#(R~Kf-Nl`Kg=lylG2Lq2gY9sn`j4%Cg6? z{eUmb{eTC}`da$|Uzqy=oM~F}G6fWR7f0?){lqr1*g?jAtF>>1ZA>pVH?jC`UjRt5sDY(PYb}1Gn*zth>*KX`Ew;;8tnDG^^$5A@QQ1zV%m#Xw< zPRe}x(_+4;|5CJtxi<%8#Xg#tk;bN@EpSv}e_`eZu7u5XK_8nyJ6&ZXjA5BNc5cQY zf@rT;;>Lw1FAj zo%>TVy5aY?IoeKd*x}#v^~SHE5m9}}=*T*PjG-(INP(T8-cG}r(Hq5TaDbXM5HVP+ z=;s5?^8=|F-NaDRNCx#xuj)q~uXf>NdkW4Nq_2RIVb8o_*`uc7wzNuH3D}>)$-$LiR*ulM;OV%R%GWKtT^IZf~Blk8D za6JRIBLM5?{=O}(HaIZj06OLMX1+X;WcTO?yq)_JeYX8-@G4Xf3)6_av9sHeYN3u#|wIXR~r$<1f184IxR(XbP}06RWr z5yFwJw5Nb8<3m%>TAnY_Z%2bT$*WIK_388N-r&zLo;$tzfQ*hnUXm@Edi@$o`4PmF zgxP=&u)doCMbUu8l=1=uNc!rT0r`o#X^W&vsllmsR&0mbJOYlpM6|wtSa>4qlc+Wq zqHnrUtqv_+R4dc07W_ZsvA2i7D$K(YjWuoX*rU^9&KT8yOd&1G`W|xkLXRYsB*vSBH4M=IX{1gHb!@&&FS&%V0_6adHOyom9_j{2d-? z+Kc8wo3G?!xLLTHR%KOTE>ft?4dRxfuNjw*S~1^Kroymw(WwNlId)fh&kffi(~*{k zWM~TU9FdME1JjJ{C|%!hWJl-zky#s;UyZ@w7q;Euhs<)@=RwF~XV>s<0+;afJo$G# z;d`{JD>hL&mW46nW60TH(a}Gwp!Vs=#2AG1(57udzpeX3bmu~2;QSFDn80_6wdYvf%`Ls^Y1?MnRVeY_zN6N4T`9 z7!gq1L8Ibzk&PEKp(ly|^%`eNcO@&~UpN|PL)>JDVyFHI`JBLSkk@#483uefeD1~_ zr_GpewIY}C7rXoiBFV!XKPewCJyst0fg_zN4f27~4RJhhG8)>n6Ox>=;16E@1xg-b z3hG@5YJ@2$T4e+`zz-A0_G;|?~Xs=vq(>f<0t|_;! z03+6Uh&_4w!MxF5cmh8$$QSyIX&cl-|zX zf!@*3vOR{A9fljx^ze%W#y{5zlJOq98E!U{XB_lMG7oxS9D2f42a+0m-c^sMFZhhB zNj#r-wUEAI6RW+^_yhFN?Ds1^--XI@WST+S?&CF4Twms(q-s}kP|D|^gpp|mN2VuS z^@z?9sYN`WcdZi7SFDlgHCG2P2a_H?vEtN!#E!?=1G#VIbJ=N}sI`$pZam;<;&(kn_}|3-Yt7Wg16q<-UnutbF63zPXKE=8aFR=_U~ffx zBZ)Y&_8?N>P`AxLnt7Z8U4p)!;3zANq~#az!k!vOUWUsU7yXcvD19gL5e|cJr}=2b ze)hkFK0%&n^6%N|{O|%hiBX8I%=xl8*ayNkwQ-mKWE6r!N&Jo3*v9qOEWspf8klW+ z1)^&2#V_M+a46ixvG$%$d$_5d=e}-7+4_fko5vAV{}k72`ivlWMbY(<^JTI4o@o3l zBk|2WzQSmd@|Nkyq@#@%4(A{re1(iJJ4@e#TSZBr&8mM3bH~sU&@KHD^_SYemC-_< zB7$iz#ntoB`%xSkkM@z)8(ZLI^55j4~rsVuM7N3fN_*6Y3@4tv|!E16e^b^YOHVGaa(k>)rk8uvi2`Y1*qK*0=+#oS| z=zGvraM)ukR5bk0@|N~3RHXNjbGV)Mq&*dSW^4i0_vGpy8$%fH53%&I@t*vP;r>0a z&eQgSYaC1c7wT^EJrP4Y_})dE#r_N~N1kwSkL4fX?_p>}_#N=qL;kKHf14xg4UBN` zSwzeK*18gpMNk`sZ%S`^lnWe4-K%dgRrwgQ1#g<0$0^eFJzjmR%h+`+#@_|x?|Ijw z$cKXkjW`F1RmA2^SeV^hXul=L&(Sx+|A_H_0{KMv%fOuaU6Z-Bu%f1jWaf%3=CCM^ zF}FZs?!5nk~$X7Sk-+>=(i)X9NlYUgU8yI0GCuDR`j*eGP=ZWI-&Nu9iS9jq1 z99eJs!+}p>dJb#D5y*hfy>|Gq;6OsZpi`@JzZ1JNLr2l46#l~Ozel>ca@`iQ@10*H zAP*d+eN{UZ>4F0tTXlS$q$^3y+Uft2zOR1$)9qLbLyn2nI(Ba{hzt2Tp59q|a<1O# z(Z3wM-Sf^zxr4R_HrN73?f!lJ{AZp5P7RLn*mt?DVEW(LY1xNa53aYp&6UZx&jn>@ z%7bmjzSKq=2foi}Dk3W?$zE{(3%NyBN@7_*3HkVfW$ujKIL~m`*-D_VPdiPhnsMx} zm^OoCeDWgA>PEOrzjymT@M0>bVVqbU7MJ(qopg!X_mVIF(OiRDEyr1l-q0<8#%3!&uD9|2LdZ1FwhukAv6aPA=g)Bv@wA zZLs3=9=HKG5x(E0;?0LUm-*s(zpD^W&FvW7;3~$OWAy#5YVm}hMm%9i#S?~9JmIGi zPxxuX6Mh;zF~wJ#Z{afGVHf^TQUBFP8X~~tbB@f}b2=cWjs>baqhvgWO<(8rYEJX|C`4@y8!MN4j&*?HE;$A8nuBDZc&D8FD^bUyoYkcB;)& z5-_dUuHewqM{3}MM0NBB#&gdjqu|3XG88*QJ_Msf4=8NrjYc`*n0f{CWUW!{aC2h2 z=z!)GE;8f1HOL23=_%-DuoY=he>!Yz26k)hfgC3$^f{PN;mQpw#M?M&)-zQ+g$)zy z0C)|;Z<)O@BCe4LB(;5g@uF=I0pijjODB;RaO>u4t7Y_vvZjXHDMLPY>QW7Mx%X zS%`^-IVjg5MR5AqbyGkI98FvTNhQ6Px>Us-h!&h-A4%V(E8=euhsjE#X(zBWoYSQr z^JB!18gBUY8+R`&R!hDYyAsC7Y&KN*I=J4keY*o^#)5+b-=mr+Zkk;i69^}$%_@}Z zu3gSp|71+;?_H+q8ET+YL{+oC^Be5dxDtc$)%2F45$esRnG7vpQ*k^~x+Zfm2Q=Ag zz{yqvPEIAgYjQegEM1ew5+PP5pD++n`w;PPh=J%F`qMExSYS6A{)Fo2d+0M+%2(LH zPb{ALjjuxaa6bl93&`nCC?G%mF^NgKax6|cam7s!i_;UUT33$AX^zFId8cRAM*7S! zt%-^q^1^K$^L!Nd$e%#ga1w}dIb&ORBvx0BhD$LV1oP5)fMq!bBV19!LQK=dxR#em zf7V??mGxXHxa<4QUvd&A2|FEaE8S3uKyQMhfnN(;_P0Q9qE?6xnfYG{e1}XofEf4= zuE#H#&e4Wzcn6JRKZuEz{~?ssm6y(8s4H(QAM5zbzaqM=$Bv6VHgQq~^E;6xx5XU~ z@Y{od?@+8EUK0IJAq&xLW`-iC(4`PC<_SoLuVg*IGXh9c-lBaVc=`{LMtG_KPbjvr z0x7@~rN1UIbP$tdq({~yfR5kjcA=dno%}Rz#d|E|b)Xg;gJ^WHZBf~l@6C*zX2xW3 zO@ev0AgWJ{Hy`mZZ1^9yL=nsKHe_kqEym;bG!W0xc$&yihz)V0(hH3jyw#Ia(Ai$= z_$zdNjLu^krvQe26g2oN3Nj8%|NCv|z%WGa7V0n!HuK=Oa`*(cbP*0oGXVbc$V)0; z5XIETT$iPg_(6cVxDOs$i)mp*&Of#b(rM_DXnzPLQxGKA8!8VTHatfj zJ{5t+C=?HyjH9?6D9|*c8DtW*NQ*hVlEFddMq}q6n0Gzp!~TDgigVAQ^T-3v^NI;( zR6p2}#onTrAQ^W*i#&xZ2F?;XSJ_H#v$c37)ZB421_%=beiF5%mgf%N zLBU|jdMD)qJ6w)$MD_NyXgEfd{OhkSLfF|Y;(@z(u&MMK&Bl2PW49P?vwaBjvi(A> z+mVwf1VR}qLdP5arzjk5^)J9%_;m=k!Q?iGqXkRANqCo7S9oTKqXpjAc8^`V^PUgS z$`PASEv#2_r7NGhn978F5h^FOLxZacArw1m4h4+F6N<-U3?RBmL_4;xLOUBjiY7Vx z$d{-e2C<*fe%E3^yfdW_$KI@A>vJ>iSCk=rs#rpQisofgI)lT5yDtQ5cVSjk(czxx zP%1t!U?5c6Mm6ykdm5AGVJ~$KV(0+(2)`cx`_%}AO$vf5;-T|*!A!A&PxAuJeG9G(cs&4^YU47z6 zh{*$%j)p4u9qu6~82-;wv(EH8!d{_2YSu{H!@?RYcn61M|3Q)7Y{7JL4Bh2^Ty#DV zz@Ay~dOJ5}2ODqw9$dj~xwEw!>VdmDm_K}ON0%I+XFv6?3zdv*@O2h9!ws{G>hm_D z!z96j%WTbgUT~=AAZk4-TI&?n>AlqvkK=nWTSNO~-aZtwh<2sEl5JRQ%%gCIn)L}J zr=wupb;^Dg`L?aDSfCiM_MjptcrTjgG=w7Xegvw`weWy{fYwPjhV3!OhKBE{7{NUoE1gZIr+>UCHjBI&sKz=Xfd3PW^YB*4iYBhh>0Q*`tsukAf!{i;Yt!OPW(tv`vTA5s%x zR1sUXb@DWhZ981&M-D^s>S!E#yVf4kF*uv`we*Ff0Ot>EDUJly#?v9lk5d!lF}^wY z_8{IuNAMQYIh@6_tF4hrVqa67_`o@6@T?x$E4bjY4c1983I_1uS-@?=SYg4NVr+#p zgL(5gef%df7p4kEaXWg07~fQk=c!U5xDXJ@z5>M{g{UhJ)o3%+tm*b8nDpnM38zzI z91V8NVBputHwosK5LD=pio#$(BD(ZpU}2>VN;EpJ8WuY>aBW&b-*gm@7tCrd=syewW_Cv)POIw1pP>?FWg0KWA3%nTYMoG%ga>Bl|99s(hvWBzYih?F#AP()~<7!P2J z6_yQQBi~XC=M^6yXBeN`AB$MHKe4Bv>ltkjDVOe;DR2l)ANTEzHPGmxx#hm>+woPQ?Lngkf)fex`N>02dpq z`RU(Lwm+>T4;g0ilQXdOn^ZA=a=-91<6cWO?f7JHfK>FXv0F;Xm=Yb>nh&6O4>kt2$;y^Rb%`Z%J_~s&c{^jy1E6R5GwB zEtJuV^`6<9;5*Lukxcs?IT{gB+hRi#qfPea&xG2l`Usb?eOFB8M`;p+^W70`&0};9 zS@^OKOz+IcDPRmeJ~3d$?7Jy2Z$*bJFxXnJ?OlE*iq^2peR(y&0wh*%7te_S7G2xQu18?-758SMp^8r@MoDcj_ z*vDsteZ&l2vX5t?ADwWVfALpQp_aN-Q`(88oc~Ad<0lcZLY~I8d zHDVkq-j1XYV?92hWRCTbfqpFvq&cq%q9OFDN@x)%W?#M?okH~GP@jlZ58<=YyB_Wim~TmK|YK7G*ED~dH=61VLryxyVz97H~-gsa?!UK33=cR^W7Uo zWuntwI6I9xle*hoyB)y+CRou-tc3*L4AUsJ~CV7?LTS$ z#RZHQ&x6^!kc8{~;l}$rkftknqm>7e<2y>uJFzwzg;`$PT&|7$Tu0muF8Yn1^#A_= zmrY~e^yL|0uQ}hvBF;EpjMm8)BTLiD=x3+0JUyq*xBx8XX6@6?G+sYONp#>Shkx^| z*+RR-;a>R3#kpFs{=qlwmawXT`grWgzpTLnYmQ>^-9 zw>RSuSkQ*7w)wxr<}ca#Mo(()xR<$ff}8IIH=D7a8g>7)gqN7<9cu>!_9u2234xw# z9Bb=SA&#bjAUf7wXTu$bj!;sdC*84j+n}xLqml5*HpkjY_~^f}TwU*I^aABN);85cCZ785a2)PRP*`<_UL-=>TFlU+2*p?18GllOr@l8^ zI|JVB4o2h`)a%zV8sdBlP9V6%`tP!FOxrByBhaD|-UaY9=V0$|OH$(w$0HlFx*ZSF z6`QNS=jr??q4QXhXHbYG(O=k0{iOC%Lynk zx9NJDb|-RAWKZ*Upay9PX_5h+gb>s3qHS^?wY0OjK0g zmD@BKje9bRPHfnwT>`mjX}}&GpA33C)G0<3*zbnjj)q#~9BB`cnQ)hw?K8DxE6USF zrk&p<S*MBHR>Uxl@PiT zfW=;be8La%jec~i--hmF?$P8etsN^XmGx{JwTlwK0JAgWuxC9xJ6wD%ScH_kuYbue zc|w+fj|Tc6ApH$&x_fQip3!fhb=NVATz&gHpJ5vwGZIgeplGLyepHhS$h$F!RINq6y4M z{}9IxIWrgoVQ^H)3MReK0Z$f^d2s5;n0ddL(_x8_#)(Bb*FjhqR7MvD=Ka3#e_D`2 zjOH$+z+BfJeuL&AR{#H{ewkMN{_ucRzx!EltA2LW@86iISoO2BepTvqU!Id>MCte2d#fg_>zF@C42gjRCDWV`+ z|H6h?h5eUV4-#1q(*7X&Utm+dX>-s!V2|j1@?kfce@?*I0}azRh~U6c?6=iGouCBx z2zgUZV&_qN)AR(Kg6d7_JnC#h-)Nd{6KAP^q94|J@LSS-#LN{-5$zMur za2cRNr}RDIo+(@bnB%9=wfW*O(|Y za!==*c27H$0;wup#W=5%r+-REx*IX^K^v?Sih_40=|=;5k8rNCKRA7ec}}bV){&p# z@2Xn{5A1q+HBc5<3g#qwh-Vf4TOv8=rzl6=zUb zx#$@bTv>25{*5`W*hE>#3xwzZ*Is~@GCqZ!33*YEmU4)rc(78QixFn7_!h$#;K2A6 zHitr3ggB()(=#OB3PmINMv%ogC$&}g6yD09k#5^%Mbc6usHFzSq31Qq(VY#@R6*DR*?S`WQVgZLN$in zi!9jiVc`1lFob*Y5>poO2R`-~FF=qKhz-f`*)HsCQDYnrh>wooNXpMb^+<+(g8L$D z%e$+nB)3sXz6*SG1V;q?`=BKFDv`IOYihIBs2CRt8GG~rb^l;-NglDME>6Ik9i_`S z0_Ln%@dU3nRE+A49d(tSzz;U8K55OHSneToA5Yf+N;@1rHbQlq9G+sXmC;2*~8hL z=tarTg<(lE+ZrvJYPIOZ2CGGvf*@M78ZQ(vMkWm=Pv6hMa<>qsqwy2AXx6v%9;P@R zd<6)n`5XCi59yo#@g@QeumECb#K;}7594MO@O+PIAeqw} zT#^EIeWF<$O|%4_z;Zk0{oC-$abg&&RV~iL4XX#u$j6+<@oKKCx?GqXo|C~FSpAGC zg4xCn{0;P1x3;F;9c!=Wkwy%N)E(TehcxsA`Y%dQ4VoZIKdMstE=S`|X0~DzTTN1| zNdJU>S({Dfd>)3QI3e^oCN)qwjK}5r{DjbdV)JvMe5j`j)&U(&Kc9NG$MB;Ei1;{W znZ|}`yd7x-dpy~gm-U6CnO9~0j_CL!IrwGiE$d%X-nkX0o;yNm%&beqF{U>Po&99H zsk2L@7F!J&i$9)jJOJVMpATB+2{1mv=PKD39GSERCTzI!RHHm93F{>dNf?TrS%Ko^ zp@`Gj>qX=G=SGGh|Eb|=Ry`u)&Dk74dXAt${hwPV0b|(Om6Vnr-^$`>u@2dwgY@hC ze%ui>cRV-ZZpy!*Sku@)g4rK;|I|qT3E7zw$5ta4pa`PliDw7eR$uJh#lV@?_+3?R z3JsG@4Q@JJjNfO9x;q+^5s8B1uFZH4$)ii;E#%^@XG|JVY7DbU( ztEjt*MJ05T!n&@;TC3LDQnjsGtyq;y)Fglq#Hxt*OC{iK))hgmT(p|s`!n-AyPJUD z_WS>T^LoKP&&)h?IdkUBxy_kid`+w?nAg$Vk(|}Nl;H%3Oi}OCZ}NkSC+98SSg7NM zoFM7HHg8?*tM2G~{`nBX>_f-xA(70Xk?7WuIyB;iG09apQXj#92m)!xh9YHCFmF8~ zL~>MjWHblAu($OSe~l_hia**7>3B7I9W@*R38U}$E$<7T)w|v8>qP8l#K#VB@}9g8 z;VFtiHOXT|;pzWulfs#}uKndy#olu7Gn&+gm0GbrxHvz!_#_%!iBhw${~GLp1aeKq z*Ab?LjeqZXn>4Vz6<$Jg%l7p%;)5@=eonD33vj8oL_OYx%6CZ-OlhC(>viDf6)wT# zhz8qqQ3Zwz#)tOoQrcNz+u53j^LbNgk-Ay6BDyiY#AZ=-E*xt9>#Vgts|lLH)H zxQH)#onCU3bJbPP0e6Up#w=Z#{<8FYLM@)s6GQXf)_BK9zYj-)r0E2niZ^0A_O8b{ zJUTEGT=JtmW`sIN!AIlBUQ!!eGP+N?2zqldR^`cM6>r#-;NsCJVxEW6t|=VN77He! zl?@H)(9!7~d0bk2&0!Wlf{XV?GOWvEHqp{8@55Q6T5m@4=5A8iV3WuX$9QVO$#E<; zKswlc6#`7&7c25!4p*h3@7Ly^UtF8lmABQ$1(a=#!rk8ZFdC+O5cdkiKHnpr&GpVaywz^ku;%Q;5AKS4bZk z78cf}*MT!q2_PefF{;R3)?Jvo9se`$lp?k3CkA4t6qUFaH=n#$=e}G0)2{Yr3EsGm zmrxZ-LFZ&1Tr!5ExyM=jANd9vm&t{mJ31>43V->~86yMpf=wIsP4c3>>>f1i zX76|nLjP#)pNnnU+Q>)-FF?w)ZW8KFi z3WG^5@&Oe5rS>g*Of>Yk3Z_0`L$J#wJ01Xyo^5OyGkgjMc26m6n*z7&)HjK-CDPZ_ z64FNO)&b5f_qTKHKA`J>po?<74JYPrRX-S5VRx5Sh0}i z6AcCRc#g#f1HC}&M!rHPSk4FZySRsaGy84pmwx3`P)iKBdV&nm&Q1UhFx~pnSabSQ zd`(8QNj*JQjpd*dgDT6EBnTDX&jqdoJcb~PQ`u(~L{9Lq~ML5xs-z(P*?&EXbk{x32^ zX-`EC!8K;71~Go?V~SY8vaBgW)36>LjHVUX3XZ31Ti*9R_wBR z>j&CxKC($Kz-f) zVs~gV(tq~gp-L7|i4Uhod#@jkZKlqDxF0&ZM!plhu1;Oi>wR+eJu$ZyIWAI|dYknE ze04ZEcfDBq3JX?#7=z5Sm#AfT(ke0MAbq*l)YLupQ%A4tYRg$~-uEb>_5Jnkdl)$m zP|YdwzMuI&d{5J^e>J=BAH#F`TJO8*KjTX+eMsQ&=xKaw;)-fg=c@lK(r zKVHo@Fw>m$H8%j3#GKjgSBFc%~44QKli=&$tl@S4ltW2 z$@f!Jbv$JDliv6ij_;uP(6WobFgEx?CMSg%EK}a*&+d``RL^`Lei`~tC?P6art^^X z3TR_(CpwPaQzbTA0UbJZrWgOn)mE2_?F(OKPu~-cD;i_13y;I%5$Ma!K@bKVcjxB9 z%TKarzP6Y(z>{5VL>;YdmD+1|Mwt>VS5${ax}qo#q6~`eAbNvfyD)Eoay%iecG}{mBb|CZXL3R*R5iFHVknOo!LwhDmu7fF4(N= zkQL}P*nGTu!ke;zr&=!m&{cDxZOePdlO-uPo@^+#(FK&{4lsd&8^QD=U$wD9rPz)O zg7fd7MQv5bWKt4d%BR9mZF!u?ynu!))?1#%@x)_T`PLkUw^(q&zxjmVh06vpPP}vL zVYhIMVB`1**v;TjU|ztTrbTWkpa@#PKIUz3yH!|<+XCt$vl9A+?rFhFwNHslZ5`}K zvT&j4H-F~?Izd;w^K#2U9bE)5ikYTiV(PMG$^47hm z_xtgG%DX3=_Uo4m(4n*IW>Rf2n{ls(SNxk=L3u55t82w{^9|3|GEwDAm4eeva%}fb7|_8-C?;D0f@e60eeqr;P5qgtXdDPE8E} zW3%|>;=yXyaUnIKPH0!yoN-cXzJUoq%&>Mg0C-?Hkys#n^{!Uk(A~gsn&9=KyA%@+ zOBy+VVlBHAi$oK#X-due>mrxQ(-b`+yNLUa#TN3G7tXtXWuZ-uby2St1>?-j51|g& zp;K)JjIcmL&fFp5l^D5v$IV1Md-$Eoxk#s=V)R(&MC zaGsY67{@j5;QJ2SR8s=~=dw7=YRgC0`SEfy^dWYFyC(38y%YVqOIJ-}U0As}G4?85 zU>!(|oxo3ZATeLiT-lPC|A>Aw-zUb-cKKu7`3>C%rI-Wm!X(F9h@mKIujX{4L1o?D z-l(UT<&Z{BU{b|0&Ggqd=Yf=z_;F=85wsI_a5)J?EMR8{4~L&z5#2ZSrG*6RFFYLk z=oa2MBvk3n6%e$6E;5Z{k&tGWhvOrUQ;mqJoCp1b?hOO*NI_0N(7c`_Nc&TOsGQ9m z^@;nIQWl0jJvfVt%MnWG$ z1RQ@y(QP8<<5EsYo=g` za_OP{W$l!SS&uhVnw|7Q!U4yIH<*7+7Yp?PSh9i-Q~#tvTW-Hkc{R_vqEYUq(iN-& zg=Z*oCbL!XMGHmJt*Cb${&~?tQgf)o{EK>Hl?B13e^EvI25pLYe3g2aSNxPxC(>iV z+3}Gt*$7vkakBHt#vBf^wiG7Z4B2d<26_gYE(D_W$!V1y6T0n59j6U8e?_w6E(d~V zJi%SAC#@HE8ElS{D84e-JYG+*l)>f@Pn|I>cL<7AbZiICj-Nb}JaL%sW>QWfMbzgm zFyAiMn#y3)u_X53Mfu&2p66&Bwe` zXRv7uRmC2&9S%0VtfilvGs;#6lHn^^4R<+#!k#Ll({4NqJ3wj$w}@$H=xF1u(<024e(lgyM(Jw9((8OZc>>mIyP&fXNPL zVcT>!N$IzttBx-dPiDd2B-`4)Jiyocb8Mo_Ec2dmjR%*|jwy~PIjhFO*dZ{U?I zzVzbn4zy^b_Nyxa!|W;;#?&Vupif_Hf5a=QPpM7hwC@lyMu1QHj`lt0@;p<|H<}PA z+ah_p%lb*OlR8&Pgm1#@xKN;XeBjNxf*>Ymvf$jU>|4y+pE!`s=*(w&1ko;j8hb!} zT^8zrEYw?oIyQ+yvxv&IFc0gYdzC_f?M?nhPAkwzy_!l3oZskKC6LIu37a&B!_7>}n@{)mE2W9d2TNdq&Vo=tje#|gptKk1_-wTiGJTS7|#bs*Th znu2b<{lh)6-d^X40C}D?*CA;x*gTQM(6$cN+Yj^<>tem>`~fVXnzH-&y{shoQCbPK zAEfyxgSJWu7^T>L%*eU^43o*fl+= zWrJ+cV?6=hrayY?sZ}sx_tUA;g=evFzOa=sGwAo3QbI&C%*$rGAUf%>t4C8jIj1VL zs3vs;MZNVRBPi?T+e)!~gTL;KldJZ%wSo+u6mAO6Lb|LpF*Y7BZCTX9cpJ3!bLd$Pn@j|ZW;6^1GefG+kiKbn^5e6wT@a~lb^_8}qxFF}jbZ=1hOwKd!Q znDc}8S*%jbovk@ob(>f1$Dm;PL3*%=azedwzio55SL9h0xz0>JGlPfO0EfFG&!Ts- z$aA0Vkr$eS=JgOwx6h>-N>SANpCVg zan=2u54`FEKrH>BX>b|0WizHI<2U9wm(iHbXrTis3}oHrKu|J={#brN`z%U&dx6nb z|9nLmp2H!+(EoIkk42HI6)6A*(-B&L=Qsecn5*Fq859@UIBX~f_KYW# zoMVywF;?a|g@NcWZmhx2-i!j!j=?UB9Eqs}?}-8nqSfYrmjs{LM^|jd=^~Nq0|LSo zuVf4l{FyP7XU4EqV+gT)($7jgV_u#_-S$usrntv)_mdv{Q zix=myD1580)-#l%@XZsNlJdAh`_8ZY;#)jDQt#)}TY zCj=MVE^y_`7~{cX;qq#`$$5fG2XBt&TK9x~6wg(%Oo9sVxSIY59@YO7ayxl`&~0~v z6G!s3ht~kY9La{z1F!3;F#Y%JehK`}<~tw1bAb14?ImksC-jRPI6nTVxeF;Z{#Lwm zYU=ctEz{)SZ;?OeyrS^S;Xl^w4ECGRZ)M>sf+zxUiICKC;L_jWD^Tg;2!6!iEuU_J zf;1&Nf7MT4U^+1O-`FpZxa=zI7#!GNpsG6a3;Ev%ZiYsPc(i1R$D%B78!T{<0!!$W zfQuc=QjRRL*=6S7M*Oe4fNM3?Ik;{WTwk)d;6#+2{{&FJe$Pj@ca-=S=*RBiaSYiO zhmiSyq3$!vvnTrSm`{oi4+{+Frj}UuWCxGG+yp$N`^1{lbS;HD^ZTn3PZHKpGYhW8 z4O|4YF$;rPJ`4_`BZ9%$-NQigYv|4%jfeyVnEy*z~ubY&RP% zbT^pS_?17I{DP2QaA)!b)LmR2ssluNlSA=&Z_tEhWF;~7lCx*xJ}DvdWPvI9n1+E` zshT%M-`HY~rHRgb%Z-*H%*hLQlcE{`tcQ$HmEL6pvjiNR!AOn^xEO*C?}f1 zUx(y_RsV;~XXpHd;!ybGQA%>}QAPsqQ7Uu&(ZN9Ch5&EU!3GE=Wr(>Y(o^vCHm9rr7VyZU1tC9rgzg-F?*cHH41x2VOHoi<-z z;0gWejF$Kk^*^g0xBI8tDqXxK=l^7N;JybGK^t|Qm+t8K859ip2Dsj()on2(*z}yO z-kxy>z9%3MU4S!=2T=9!bVaYb%-bv!qLJ|_lM{~iKR zK{p%$TqFYUqPJKsq4&<(GiAwg-;UBMP4qpm z_t*y5vBiW!=GjyH;T=s&x~D~}uVR~pgXtPinj$)uJcTiBhpZA`j?;Le{egt_nQLh` z>dYjz9^09;$!>*~-iIdFC$B8jQ5pi+Gl|w-3Aw>9^qKKouA{|%D?{=5eU4ZhTrPu0 zQGM0>kv*-xjQCBGtYRyNB6O|LYh}wszlmdP6X+l6 z5*O++!OfAqMK_-PN-)9~Y$BpSZUd;qwwMwcu=nGB@QFp)Hpm z3G2MNL`peio*8je49|R@iaGrYe8_9J3yJZE#YSr}4Jqgjt(wGe(^n&b6un`-I0-_u zJj2g>UK;hu^9yazO@?jf=wBB7(*wK%)FoVM9J^{|Ik%@8;$hOmd|zzol%e3NbUEE5 zzDv<2R>1-P_TYCat>(-a@o@##rCzedAg3mV)H#00-f0KSVDj6N1$FkI^sNQG{6-d# zz~CBvAi)Iu4e`gL=O#=#a&Yta0K4298ZGc%Zwv65If*3zWP?pb9hf>7T`R@U z2D26f5#jrFslc338WG(IB$U??sotb-O zvHLpRVU;P_TEBt)gId$~WRu1qY$)mGY*Icuppt$ev?}p5OlF+WQVm)r%}eF7M{~5z z7=d%xCd`;bJp92H`+e#JAm6J4Q=ryVohr3igHF8#V0L0dxQMzw2I-fpsmF&1cYn+emSC-JO z|-+7h01Ot+wBU}(nua++EbCT z#NJ#9&D}eDG_PNj7aa(Xn$hnkLInz(up%BRi0obYv$HctFyZL{;%rKR`J8$JtyBnbdz5}wH$vcI;_i2TOa{!`e+MBIVEPPuNiFD zr}eJwYrZ>{ERg#UTR#>X{<88HebQl`CYwx{k6`m~Q9>H-@)SAGSc7IpF^O+IuA8!2 zbi1oxz6%Dr^%ZQo-PV0|!0bbREYVW@)3!hf4u9|`>WNlje&n=pf;+xRt5a8%R#WJ# z3A$=p*OnKx-NV3qucmLa@6-{bT6@o~q9cPSTUm#DG?aAYmS2H7Q~1_mmRahU8&gu_ zuhVQiefvp0ry5#ID9^VK^R4}iRX3Lp?JTB7FxG6**{fw%X+IH_0U zHM@SazQg#~+%4MO+|kNUw{CXqdl;4L#>#`gsaE1OCGmCOEb?pyU+wYYV&aW~D;PWU zd1MZp_ORHOd682%J2G2n#(55Y^p`ua9!^$Mv-3CWj5U6JG3YSmX1VGXd|f#QqT73p zk6WSu=lq%aHn~J?sGC)y(cKL?_6EIWG6P&Y$GtgdOjIHl%nsgIcp1)NRHE;1LsY+pFf(Gl4f9&&b3PyORA{`9-ncUx|K2kly4f08oz z*SkI|nduO-hsSk^-wj&;G6+ZQxj@KPrI06aAW57HjIBcI`!c}th`unRVzVvJOK$lo zyD49y^19Z$-1N70q4&++C*bpXrd{9J^Rl|T@2CRq6140=0kI8<2HEwi3j@(qZ0p38 zEgS?{f7DOLgMI*#cF!YH8Sp7%6H zx8`*yp5*8n@>G13evukqY0@EabmD*@IJ>ts)jG{nKO{MM0ipb?3gw%0 zwC#26;4t+^{#gCA4m^F%^uErIjFU>G05IwE0gwwRh7aDo20ydzMqYWXv#0N%Zl8k~ zDUOSFZaz!D_WbMdK>d*nJ=A@P+i_xyzsxQ03K3Q>O^X#~-WTt{{>hOq)2wu?Ctl?S zf>w+7kzs33Z(?5*yq*h9s!tMRir8A{4~fA5yzu>${X^$G3(aI0J)rKmzi*!Bok9Rg zc(a=UybB6ZY5is)ye>}xid1m%^ALSSLwNy2J%j()ZU$%J5r55FiX+2S7Zh@zX;n>8 zG*w%5L2>l$+N$XXMgJL2%N*jm%vON?baM$~jcrCJEL2YmyyRU3_)0NltTCpD_1X5``0W z@iB$w65e2t8+a)i9lxeH6u;=8(eaB)>Jrxw`|IS=+W1Zjik(DX2`3(T+9Aa@fbi2S zKNwEn_Pve!nulLw@!}fr0igv=Ft^jhUUGi-Ri`|BWXt!)t{qw=@0ZZ+n?bV9TfSc>uB_SpDu!juRA=v z?R`%$VeNO<&0AFjxacS7m@Hw|?ubrWv1& zEIx8O{f8_*E#Vb*<#{ zs~-^B)hD0OY>NPvd8(36Sv10;IgkU^QJpTyVPhSWp;tA9wVdNZ)Za9c5dCJol@E<<`@Y- zH03u(+v-B5)uGu|ANE`Qtp>J&je%uR)>FG9VwyU?GON##en*bdet)cfr{N19(LCG@W~qRU=5qzy1iRYZKQ1kx$L)r?e#cU4A+T3+VgEzr_%s%dXF^sQ;sz|E1v zJyMa%w>SyMWpHa_eKAK-6nBO2^jbH0T{Ek9?OzWOcy*j@4U)ahrC8I-6(Vp zN9^Of)Az(zXsx84fP{JQ7__sI{$cR;^ocE?s7bP@94Y%col!6Pr2endqOnf!UDlbR zhaUy(Z@*yk&B7QvnsP?t_xups^EqYSdmY^4K$ldUNm^acESb+R;Aq&H4?n_HeUl?X3E6B%$9Ls*IW>Mix*<0b8UsiY)t27&oDHV7Lvfyop3ajSlJ=iw3c=< z>yLf1m^_T!bHzc(MkwF-(jVCJTxQ-juoUxPmCS?j_kvB@zN}?%u6-B)yE2>oitNY* zq(>A?HF7oq-#k0udk*SU8Gud0e?=OEA8v-x=dubSQ>5 z4NdXT+__F%@J3n#F`}%vyr^r27{rx2r$kpG+ zuV((UUI>*CcXKc_$J5{cRM}ZGPoc8E@_Uhuht7Iqf6C~$$A+bFtXi#@^1(45h4bfB zON#q|;{=7Kn~sO`xS(jr1>r1F@HUwy;H|mItuHb5j4(_YRib7lOes!KPXx- zxxao37fi+(YKuAiBWl+9wvV#)IzgHpEcO1%%zisQF?LEgdBq^AtXRK&T}yEB!2P8x z>J_#SDp4MNdpne5L%e2+L7Ww@jyp_ESnN8#)qVMGlYN7OFnQ4vbQ~snqK=+LC&fRu zr(mkLt?nJufTY}*2D+XlpQBS4OpPAW?FJ4N=iRFLPIoJy%DNs?p6IoRyiP zTshhB?2V3lV>LWtU!37uT}_~a;Ptn&FNc!V-Bx5ld5j8&(D8jG7umZ8;hd(~5JI}8 zthRKff>1R0_}mdW*Wv|3rB}f*Z-w(k_#lDPSeO<++Jzq^fE@Zd?+mi|4}5tmJmgj6 z7Oz%HME|B${5>r(Th@NXBL5u@?#6jEF+4UJ*jO)a%XjAn` z^j%>M#{dAP02^NcB&JaJ2v;`^34JG&9`EpXLg{d$_)^lI_lOKK+!9qCtJX-}ea-@n zE@Fb9L0TBU9RjJTZc2wK@$LUM7~~2YV|Uw%!bpFenQo*!VN1sJ4d?BwkLU3; zs6HOT@;PQdez81{IhbFLu#GvK-;!`*%#q=Es4VPSmXNl*JhrJ%IBt6qxgX`=_i3=) zY9NW(7_M3#j@zz(gZPbKqPfl0pYL{jmQq@Mqm@9A-%wb<-G}2FU4USn(pV#1JJVD+ zfc~3@|1Ctp2$kjiEI;h&%g3O9w|-3fxcd!txK{f258H%9bFeTho+HqTh;Vz-PnF31$4UOB~0q)oV{(Sr`AfF1{{tNp<4X+Hm4ZJUpMW zVWNHdZxwtZny-c)rxrf%edMpqD*Q&*Cw~I!e+rS1i1-1Qr2l z=YSf3&pPz-p=@iZ)_d4NeZAtl-=`90eB5_bsq~69Jf)v(v8=1x^KA4p|H4YpB!`WK zrzRd6!gn3Yruefc9?fCuXKYS+AR6I^n_PY--OWgl0==2UiKBQFdomIClt@FxQ@Br- zqh7>)|1!=-UI5$hdfUQ&A-dXWj(ypUWeBKUE*0SGZH;7^Z+Yp~+3tB}2d&sXj$Z*X z9VC1$v%ZUOaPEGej^5KD_%TzX3zTU#F^gqbg3q}62^7SRpScJ`yujeik#(lkf{a`yV`G>zltTP|P84&6A#pX?|go?A<5Gm|d)mo}`#ibK-R$L*lah*r0nap+BYI zy(K#BU1H!z`u*wotzAqOZeJ@;*1!WpzpuHbK2cB_YOJh_^8f6-G0}thK_J{eBhTHd z73&FvxEX`fF4h`B)czbjKoT+bnhi$5Z2e-@qLVvk$^NpuwU94-I{5^|)~=oOiw@52 zrIQy&KmJ?!@Msa*uaOUT9KNf3SU{Sn<^Q34c<5oSN?$&_Q2+oCSK8*Ce0U^$oH)V% zwS0KDSO&1M8~JcHmH6<<%7=Y+y5_%DK74+M+RplS{p4aT^$h+rGA-syn(^er6P2As z_pq|x@_Uh$4|DttiXD?Q+0Il!>>m4_)a)Ex({*NFFzH7;b=I%(E;uS+*+T! z>X<%tRj&l+|Cwe+5ME#}{o)|2V@B4UGq`wN+prs@@0Sgv7J077TzS5=L!qwcZ8UMU znqJ_um@b}N%R}>1!KQ6|57U4pSz7!dADHtV@$p(9$ml(|ABr!~eF#!Hmb5H?%ngz_ zv?;y{J?7?()%p3ykIr9Rlh-FQ%=VDT5%~qj?=d#R&654E!M3V+1)Dptw4K zb?r|27Fxjlg%s)&vO&hH7hKkd8_75@P*5NLGNn@l{`#SPLRLt2RPo^AHL;y}!5dcc zzB)gTZsaZC!nxSW{Pb!^pXv}Oh0MXGe^bkhmPA9@>im8bq|XJvh(C|F!}3k}%ek&} zZR3KI`Yafa+h{c>Usv|!qv-Rg)wb>GKyp@})Fk1tv-5iwR#v-NVa!A-379SCF%zH# z#Ph$>>!kfo;~{#+C> zAh`JFdGR&z&$(okYgBQ;07qA#SqSw9h)Dol8}G2zZajIQj7_T+kS1UaOrI!yx1MF^ zp2d2s9bR1){V*|WSZrgyZaoVw{|MzspZ*_*7W5A;4i8(tv7rCj(1KIxP<1&zeX~o_ zg>K#|AjRP8v*xjffRXjOsCprKxOjQ#*H%v1MOzl+(@4?J{erm zm@O~SESx;u^0!@{w1x=g$7I0~6F6`p3K~Gf;|*qNsi@P(*}yVu0X7*jegC)w>T7y# z-Mu8RHtddp)Ul&|zQn!%a*Y3OSzXI|Wc@E#d-S2$lu$jul>f!499~g;dm5HzDC>Rk zJ2tVlv+RP~Vjdi+zFO~#CJYJo+k73BeBCxHN#Z(;$nw58VE5h^%-_hopE$pZb=2zb z`>UXT;{Fe&1^R$|bl5l8+gio%w-mwe+hMkkWmvoL`^s;(`KN?;$@l#=c6HW>zaStu zca4|TyxcV^fu@;)Qk1b-+x$@gMeZ8!{EalvG+%69<_$AE?An-;Q2eYi%<{vu*-aL1 zo)&*qK3}hb*FBsy2Bv>1{Iz}>)9}*(e}|vWeo{{ec$qjua=RJ%_W5kHg}=&z=DtJS z_H`QTPlEAp*|zn2nzKLd;>qFRxAo(w-VY(}^fhiuTII)Koj9D;d^1kFV~ROrTmnfF z4I>uXKDE@@9-&QmwhB(gm9vd5(?{*Cxh5wtUr_F5mFyN$~<LB=v9Ih_eM$xj)d5O+_G+)-fJ`9e5Q4v3}r+qy{jfBCoUJD)0)cslXFKXkJ1Ib~A zcVp_lie<`*%+f#EyOCVzQzP>OLD6}J>Tmi>^dKCgdX)cKdAtv-l5g*R`M#d=c-y%w zlG!(97fCaV!;{DFRNlgzyq}O~r{O=!<1KJwUcaqf`UFu#C*{|x)^$ECF+lB0t-d_o zt%167r7BmQ(|Vd^&|4l?JRz_Azot{ZFsHoMQ!jbEdA1k>1YBPycih# z-jlzbo!s0GsMFU{xu@?*%<*m9W>K+vk+G-Gd%}~b-Mr-3&0l$t9G1PcZx4F@9J-V} zcQTF6ne+Uml^Uw9<=`LtZvXs^a|U8}_Usb+vm1MMf7i|b%AS44PGNejz&B`! z`l-X%p8Udnlli+_Oq{*#~+3}wD#R!*vaZ;-)&&$xc6+@4}ik{raCLr zcrW`dCgGasGuCuVz;j9G*mbRcYfroGeVNROmhEv?;zl~)?6&DY+w%+mjlK5n{VgiO zYwT*Tok_j7v z8!GYfKP!)4y3IBJweom~0=e?|Yd>Z_j{w#VpQbUh=4UkH$>Xic&W1NBSN6$%FS7D@ zVvlfwJv+#r9@n)u`(Sw+C}+Fa2e|vJE=nEG@N?FW%Cp}cKzYKQ51bd^D{d)*)=c8- zV`tc)XJd)F(8=c?45W&IsqKSv=zYeXJMTlueu)#ybOb8?LJz^XkQ~l{?d%gdIkq!j z_mqtM;4H@TY7Y87WY=L+smdZj793s%ETV)f{#~iPK8*nSC1fE@svX~s4(Cy6*V{x! zE!4(rS72WH6IAK%R!=sjD7fUbVKb^4hDHAvPBJLIc-hj&>=z#tn7i~vqd91Ak(V6! zKwr=w%l`e}jF0Y|QDL4Sfv zMhzQ2rZ}=e!#S?kZ~(xIcT@I8Gp@)ECp#YGvzNfpPrWq=kcH&PuO;_e6t4!N#$D=EPtzb;j{hTxFj*?XyjL+&muvG4bn zmfPP)N~`VfS^N%^T2e(6<8kKz0cGS*&%n&)-&kA1-eDGD4$^{BqgjH{AAA!8 zrmi%n4070?10UxwW^JWC9LZ2yWbJQA>&S{TnO?@HFeZP7;MK*x!m9-R9Q`Y!Mf%5` zF8ZkW@=#)&TPeD~;qvLM6&KbhwzDAmqgLxlG6l5IBA&CC-TZ=$bN0F~*z_*tEuP8( zJB`IEnF3eE+rvD&X45e!6m!GeYC{kZpb~ zDSPTD)A#}QwPGD75)S7q4cdf`!>n^iUYC_0<1-7nS9#X{iJCp)qu4!a2F!?$8i@UQ z+lsvCg|vd&=cK+=g8i9uL0oIedmrxB?Ef)qzd~<${nu8kvv-yDZe(0?L_uA0EV3xK z8cCEfl}s!A!(LMS(b}lJLRG6HC%4UJ5UdU`X1ll-oIWJ8n$N$0D%@eIhozfYctl27 zcrc&CG@lkOska@SsY^^Q3|GArnT@{-egP0f%QSnHcCSmFQYlTx~YwC%?;sg9hbcqW7i_T!oS;0~{UospW(v1N8T zI>$0CbXFnEOAZa;9Ir?xm2F!~QIj0C#1YdE2J&Gn8H3 z_-0?LA^DLE#vSGL0=D;BAVimgZeMf<600Lu5YJ#$WqA75=!b+l*b^V4d7lR$dy!+p z)7RoE!@}BF5Y9`5ho4>;-AH!0GgL@T#2F3k9|*X@xNQ3JR%h*JQexQ-AEFLJWB$U> zkQa;0_kL$tOgR~8K-xbX)B0iNd&}SK#yH^aU85-akYCayHS(_{?UT(k82O za6e;6kdoIrM>~wEjbA;CC`X;6ExxGzr?vZl9&-4|YIHLD?PC)5(l2M)`;Yyb`oHSm zGliM{F%bc%tlz(_;IsNS^#8bjk3n4jd-xrh?VmKVIsL0Has5jOY5rI6^A4V%Eq5V= ztf}qmVhD|(1CD?No6cmTeL`lo&=aCgZr!mQc0gjF;LX~c!^y`>8_D!#PxHkdwgTuo zxxuz3o7;p|H@4%jdiWC+4mSOs%-9a%3EgZT6i-MOPZ3Y(tHXj#S19qTLE=+`O+U3S z+~RRI4|Od2P_V7Mx@lYVMZawqF6fg2ui?`rzIfg08Byp#)cWWELL3^q9*WMI9$eaZ zd|mXM#-C%DKcn&I*ySflOI>8{zC}g*2Y;!HFBEDB{pHB{r@|a8y(KGxoGc=q7<94H zhcGZtb3qA{PnNZeIlj3)VEL7oacuhZ&%vg@P`JIHOR9HCE0r{eq-El7!ts|W*L*ry zN(gc`xbO31yXJdo+$liA$Kc;9T6CGXeI{or$=Y70v*+o3&wNK*b_ri}69@)tmknQJ z&oPeEw>qE^TFt(vyVzaxx_P%p^N00p{tEG?SKl7JJzQJxB8uB{ zGSe6}-xa`z5p2kQI_SU#U^*fL(@MegL2y5Ho$C|FWKObu8ualglsrCDa<)n~tK>PZ zBnNM{yE;2nG8D zU=2)MC1+3)%zR1t-^t{++)DmWlz+5Xy2Qvw$>%0W`*rJ5a^d!_RQ_O_Kj>zC|J}^@ zGxhxt<^PNMrv9k%#l5@sE0y10`7hdhczGdBp&9*G82|2cFXPr zoBA_!OhQFw?XUU#X8ZXBg_6nCk$1bTBi$-KH8-Q|k1penIfW-6yV77fsrG#(uepfD zkS&DLZsa|-n7s`=n_+%=uS>4CL?Y9J=k9RH$JpenN;3-O{cXKh>D~zf3TITxo#x9v zY7hu;PMq_i97_5E5uA>tJ_eE8QH5`?k}MZt!$6r2xGD|E9yqQCyzOvPT4K z15#g92Qn45?f^on({GzDK1gM$DNI(DpU;-Bq@TUY&sF)d9_1BABy|gOyn)5=FYLi!d1%DY^-N%c8`LBuNA7jM_(m}UB z!dK?iSA7~erY1SBv$CA|^xE&$%a2j`+Ye9O~--SGZ51-)6+*U>*n<{sx56ewK z9*e4`787OpGF@g}o2VJq`dziq;Fml2wA+mRm`5i@y_L%mOD>(f45 zKMuF?Gxj=GZ28pG!`Cy61^WJ}oz%2cPwhOVpK71^C!QZx>aBXZSE&!T?@)<5lv<|L zo0NJ-ORT$3Wa>3TV%_->E@~ds5+4ZUP|E<8gD7N%y;$Y`Rj*wu!o(37> z)YK44Wawq;_HCT>vG+!M^SyKd-z2+_l_}$6T;p`q#JkZ#jEni+LjnT(8rx=JFqt_{|Urzvfw#GIa*E zWT_qg64Q^XPmV1Jxt7*C(d zgUfg@lQCWyZ)$#W_6rTo#hSm4 z$J_#OQN?DuctA@hf=yji(JrNq87a0~selZ(0n$0r!!byAuOj22J6z{ineeWA;KGM&Sjht}R?zq)FzWjRp=n>$>8B9}?jm0OY=Jma-DmT8%XQGU_saC~rS zu-T3W3ZBN#T&Mh4#XVe-mThNNT0hGG>vB!fUaVY~u``qLb7g$?nV*3U)LSujTHhoi z7ym<(;RF@oW?X!=0^EFNFEovPI3=7orsc>Z=-gFk@8ztDCrtZX)g6cZwlH25Y+9q* zAG%zofKJoM)cI5L7c$zdH`>oxao&`a1)JuRF6eNToAt8Shoqt`v#Yq#cpI=?PKSWYvqiXXaZ9{u))(Rz zyc(jsyXkrwl*&%m@lsOcOcyS@_*aIKqYtqiU2giuM-`|M(BqZb_!0H-pQDGo6t~1t zg{E49rt}$GtfGK{g%g)ja8wC}&9_}HvRJU`SzAC|jd#|^XO-0_W|m-~FECe24i(Mp zL^!8VC~@`RQ1HQP2AlCaJuIZ{VAE8uf}&7@Xpz`2B$LbXR-)hfI5cTiVKZddjEjG! zCa-^-bj)0XasR&lHl8(}a*hsiaWzD00z4Z+uJ+;yxTniMZFGr>5M1YNC|Svgi4k46 zAC}bSy-U>OqKfs2J-FI;|CU7kEd__w|E3@m=Ru3SYTewlAP88#GS|CK2b+Ec3R-fY zg*AP1UF^xc+T=-j@!I>!0A^|4tD~#N4W4@~WUPeu@^Fm*@}zsK8Z$U}LoKvV@>%;w zZr<52E$kY~6Qekq3Yx4`}T|YNgx%@=T%Qfm#D2u zsKq%$j({CH@g>eq!Rnt`eDz&(zZHxpHe^ zJnGF4v;>n^gFd-r;b50KxMylVrB0uSJ@#ra>NUFr%_ktX;9N(L@aVNd{#jeT0*qVC zY@*tw)`O1Aey9!UNan`z7?xt*$I@^aC7BFMV{go4=!Qu5g{8`~$*}w9vMz?*`VDZ? z<;F!me?)GULkvwq%&UtdoQd!Tn{<0K8`LR@bH7(&<8n8XAg6eH7^Q@*wXM7gYtPY` zscf=2ei%<+x}`cw0*+j`G#apS+M(l`ii*t@Z)Eu8aH8P*OaP}onT@$00up^;FJ@m= z7#fZR_^LO}`AW|&p7+fpCB#-0*2TUoCh$5q}}Ya^Ykd+2!a8;L`V;bMAXEipyJ-|X!BDEay$=s;)Rd%PE)pJl8P z*(1j#<_?T?oOQY7g5GeG=U9ZTp|p*9DGukq@*2`e5 z!#{o*hCCy~xW19=OEz^nLfb4x+Aai}M5;*lIbs`6z;=w~?qRw|O59b?=0NQ+ z@lEkgt5Tzh*XfW9#LA@!ZTKbWhw&k1{zjIlmK{sH zT1*Op?Y;z|y3JZTn1i2*_FL5*CVOCN3q;wYD}MZ2+?QS@U+zow#7<&EM{fng896RE zgis#AA@!xxfh&XSW-BPHdeB!~uw++VuT{ytK=$o@WF+QDx)W*ZFPEt7QaPS%Nt z=_?c-@tbirV*i7GkStVeCTP&}o>X6Y=o4K0S$xTS>Jq7PbU8Ms1Z=&aNNT1%+g@b` z5c68w`DK#6quS8Jk<^XfbMxkKJRSYV)HCZkn@U9^^k-unJ50^OsIU2gNduotOrQ67 z=&UJGSyqUks6S~`bLbWEZ`i`W03-S;r^R5%AaID(EllcLjy<&6gOfeH?=(Avm!iV9 z(r738?h*#kx)8)*dPB!}5!X%7Z51Wv@^`a+@$|9Y&-lb-+x~jL{h_(-pVg!Nxoll&LGt55b|H`&-4fR0@xToc-wpl8%+zsKx8r z#3{oP^GhH$A)+};p*m7jJ281eePVtw$u@XqZQ_P|go{tHNrvM`m$ofr>j>Vw+#I=u zEg_da6BR~ZC2mo90Aow0e#2hk+8dv^VFdcf`sDmDKZ#qU&}2&6K4ze1%S(BIM50<5 zH&xnFv6(hMCLJaS-u9J>t)Upm+(MdL9l7g6_>pK=d9&y(?N^VfoVi=i>^nT`k4F{J zpXGQm`@Q^G>;}joE}ev})BX@^U!#)2rs2G`_FD`k)Wk3&M+)KNTRKN;f&)0gJWfJu zDJ9?l?Bqe03sorbsIgnk7SsPZb?XU{9<%vr``o4Hje72E*0cV2R1y7IKJf8_4UZzM zdz;0!Xp>D*0pAOT7NRe;9-r+XeWRWo(kG}`XS4kA^=B@_TBl9pY4(BE6MKANCMz%T zga&Q)EM~~X%e*1;1uLMl*}{Wt9@nWl{>-8FtBD%<+0@sr;hTZb2flz6fla^~x0N-Y z0n8S}KTB8AZ~6RVXaXa&F0@XKWiFt9b&0{HZI@c0x>^@wsFIfLZ!~GQSiQ`{bW8MZ zw<{>-x8tvdi)x#@yI49pg_$MFgNY>)RTS&bBd_R1XZVu0SPm^Q-!W<(a%DcbC_Fq|bXiai&uTmn zt_l|sEiKs)M#>1Y3n>2i7+%}~z;N>9Fb4n|!tqHBOjG=*j2r74x^rT#QLu^YsKPKLXu&Gqp9@lg#nHIAhn`SO%Z>o=^uM^`QKK*`Fzi z%_+)@ZtX3^uJK7RaTJC=VggH9-CjE{$dYm0E@<)lrZ~_9Xg6v<% zQ8+8#aXvD}{gSqZx_CipqGn(yK0091n=I#T$0q+i?_7xu4dJecNWTSlXK`xiSM` z4cX?4fsf{~clWr+Wz&(2Fn;hG_NS)A98ZS{xCj+p=+|RbA5JX{0nD;aiYo=oy|-d$ z6TnPAeUcbCd#GwkwRYqz))8=WcDbZ54RQW%;&|?()JGEX%-_W4CHDKV+Gd#^o;Yz} z)oaoHQgOQK@E{Q;g;|bcUl|4Dp&zczn*=qwWGjcZ`}8LHqY~tHsK9BlkMf$gp=OK@ ztxtpjPjzuy7d_)Rk`J6Mr2-BF`r;B(vqc~u>MB3ht6bw{tm__IK#$XZSynCCbrfsN z$*$J|WAn|oddMHpgD%OR#v=w>t>uoqyMr0p9@f0d@FnO|GPc<|LAM~ZU6Ul{hy$fo)~#dw*PCgT{PQ3 zFx&tAy#BXjGhP2*R0;LJc^epp6;(9oi8-?osb+!A z*t3<;Sup2b*NM9L;?=Php%@XiEV8UiUgy=$vqDL}?%ru>znP*zWgsCB4NX44fW;)poh1 z)+Qw)#~3aFXY92FvtMz_3Y4Ehew@h8MLvmPbY|$%xGr?GZPq7RzA!dp`M_KMZX8*E z|Lpv>JEuiz}n9Ug^X1#F?XSKn(MBN}v#*UYJBCnlfuYQYd>5ZBFl3hdR-Gp%# zP)wW;C&^u!Z0U5^wcLZYwE&mR03O6A*gyiw)A|)Xc;KmgpOZ>u>sBQH1<+$d{LmvOemWHP&T~P*%X#+qkcW zxvUAw8r(B$Z|+Vv9wiB7CijeoJ{iuS+U0?#`WR+;A0Zo`k;?wX8>mG z&{NM$-SYsZL{C3ln}|uiQybUhRKs|FMMddO@+BWCe|pdSOO^loEIwION+|H)XTsjQ zEOW<9C90=EJD5z%^tl5JmR>+Ay1ROjj+TtR=iqa`qfPLQ=%DmaD{p!C1|=>)tMeXI zC}2uhAS|zUriSB2Y`fNP#b*D_?U&j5_W2L>6`6Ic`~Os5E`7?-1JLjgjrjyE8EQDu zIZTv5%004qVSVE88xJ2EP^`!ax?jCMei(Eqe(+T!h7&j(^x}H``C$*JI81xgYpsFD zvK!cEP!PAYS9tv4q8h9&*YDqt2-i3jefe#KgbJ3HMeFuTJ~cOHzPUcTUwSrmBs^@M zc>dd{Erlkk+0&+mfh-QA)LxF(%$y>MWVbr!n2(KojAbkp)JBBuEiE<`;5^=q*=MB>ZzUfxmk=-S(-dQ>Iu2g26%uQbAt$wDthb-&iO#S$$E(DX%N&USWKF;oc zT|)X-J^syD|0Cb|Xg2%>_dhf*zaJ;8Pmm}u?l26Z?%f$`PI=KLTXd9TN5$sY$X1}2R~@{0RrKi5727an>e9y5Lf$-( z0zx%WUFl0#n(-k!EZF=r7PW0PbsSIJR>1yi`$HEq#*wTA#eDvrhkkIdpg_I?c)(Za zwwaBsFKEJ?9%_SW1exJI@VCG}={fE3DLTx~%{b+dH48kE{kPDdmYoa2s_N>1`GvLy=HfC<^dXk!4r`9NU~lJ$M)46DfO?RQ{R^j@7W|JZhW zyIJ-MR1JB6ZCbL;^69;WwR^1ZXaRhMP$uh#xn?!Oa%*5YS(YQ>ipKV=(pKG#12 zZHd=IGL)5dWA_8EzDgkD?02-^oom0d_P*-sug!anmD@M`~E<7QJPYuD-OgaPA-rb zc+l_@3nJe`>`L#^GCW)mfd|jF8;<`*eb8odg;g0aY&)+bcOJwZFa-(CR}^(Xoo#0< zbX@!5c-pOQ;nElEVu|b-ju-n5LsM5`d0|UOz(3HH2miWnDu$3_ZFOURBbs&!mPQRs zTZjw-Qi{nSfa0I0dJF+7UC;YWew`s;7Edw+G}_-^m$um7-<2-3zjv1|vcLD2cG%xX zO4r%n$4k5T?O_P0Bmn?m2#^8?pXUPE0m@-Tr5q0}J>&@%33h-I6{)81vH<@%W1XI0 zNQ(snl_{&};Ps$#UdFWwhDKi4O3)AI5**0r?E#yXvm0Br7twR`Jb>IB5O&k!&e2Gio3$>>EZb8 z)%ItL$$NpDp?GN)9P=&scX(st&4J+ z{gH^SJ!;&sR!^^HS-xIU``7iWBxw6pY=5?xr}oPBtLS_5Yk}<-#(h=(j1L{=+E%KCRh{RcWIJbO& zqJH^(U3q{di^VSGCz}=O9IOBbd9PTHP9h&$&HtnBTi~OpuK#yQfFS5>6*WFum%6k; ztWAU}5vV(`(OFzH6;vudXhRVzD(bFKwKj2g8^&=pDk%QcN~>1dYQd@yq9oby*kF~X z3Ko@suUP^ZQ9=MA|L-|>W_EUyK*FQH-=EJ%b9d&>eVlvldEIl*3Fgy@&Kba|dMtzf z+zJNO_~a8P7~_k*N$aG4yB2pJ)`R~Vzdr*I8WWQ$I<_Z*Xk0|MQHXkcAB{yYG6LLl z6EH@7B8^7)tQdX*2)LO1L8@O6OyF&Wwck&-_PbusqP(@;x7p|0)%qqWJ;l%;VUMy= zv#$+un1ZJ~wxC(i6y%L2uJRAwlVDn;=P6aIc8~e=#iy6LmAIaz?B1f>oFH*gK>$r9 zc5XKo}`99|ootBi@iToE?>fk{FA*{(^O- z>i1D80%3>sZT1&SzZrs!4}uC)>$5~I^&ewnlltw_qAdhV2trvPp`&kZ@Q#oie& zLuOd~4t5A+M>i1>xLr;pUqeb6Z_ba|O{|96*5_kbiEtAwFaD8D$;$=~N>}m-X$pao z<^I@=Yq7r|_N*dk2d^lcniskYu1#(v7GIU4Vvfb$gqJPO@S4T0X-#;u=?{3Ko7#oT z)DV;f7Maep&}9YR9ia`2&Y5zH1G9sFJ{WSstLM@rqO~|QBU?}KLO75%KC!3*#H8wM zG*~?drJAYupZ*E9ns{-R_2Nc)vCMjLj`iXadhs$|*!`!pOY#s{f7;1PcQMVe35Z8m z)8rC4ONtjgjc>$VSnhNjHE}aFVZsyk5OLm0g8eDjFWL?G_$wpd`9>_B@G}CfU^+|i z*J_BVRFiQ>zm7;6?YJ;eI4QCe4?)8=Pz5i_NXFv~-9ssPq-cfsogKO|J;6`t97^jG z>WgeXp`Jpva3HA<4l2H_NTu;Xs-I6Ab;BJFzPOwU=$*z-#J&~J&E!7MS@T;SrlHV~ zKEz+|f{T+=v9>9C!|fwQ5Eiam@JyODz)B?aO-1jcz@$gn$5G zZ3=yb!%VJ#PJxWefD!w>p%tn9tS;OD(w^F{u_uZClPp;(K6$Hd`tkH_NqrEp#W>A& zPDp2}!1?80Uo``aGErWS|6>>6)3o!;6@l0z)S?v^BaUFbdvbz)f8pyZsAZ)jj$Gb* zdIs6A7WX*kq%>*d#ajMgq` zA@8g)E!DZT*og#0+(ZPo0hfstIm64kso9aJddnVT;52e7qk-ob;#G+LMluCF6;&<9a6 zb{Aauzw{x*_1|vBLc^X$d1b!D?H_$M9^fVqGD_E~9~RV-!jG7jgOP%7PxTyg7U3n> zBgIC_Ep3g%B5 zrzNjh@ybB;v~t>t$m)b)(m%joiuT8kn)w)~yqMUNen$L2E0ek!hZ{eqECtVrHsL!) z8+ECeUw=C8U0&YamOk)INJ~0hP-iF@>JBd>Rm|^g%qJnA4=irp&j87I5j1C zL|+ZWki*-Zp?~36;W8~7%3y7JVF9Y*-IzlgMv#0?Y#NRRNnmvFs9gDy<<2*oB zi}Ksr;7>J;G&V(hoK!fN&H7dwt0%*4owvrr2)FJV`XYUB=Nd(uMHQz#ShBAE!Vi1v5rtFnsj@19l~0vTChy*U?C~>*Y1p zdBj(t@{X7~y2TQnEsE*2hLYSGuRW?fEt4 z^BQ`7e0b}HJnWLSigUHmcw}v8ex^Obu!k3HwD`diEZsgg899c1TnjP^>$>`}z7%K< zL}?f=OzRf^CEV0QsTgkv;#w)0)z$K`G~HOuII4O+ z&>o5>bq;a9EI}3Z?`J$@IXVYxZ@~&S&e9FU?{Af{RHY3ldPPx3vG_}V+Jxf#&$!rv zqI(J#$v0)HD3ZgaOB7uwR@{!miZ#%nNxDm;$B?54g>X-z?pL%_xDD~)?`-#}GxcA! zsq8>EXEtgw1Yuue=y@?<&=jwu>_9(|(qIyT2J=1n8qVp6OMM*n$TU~YMt!7SgpC@; zA2)vRDP1shhP2ANoBh?MdL$#gr-7X}C#fbbUj>}ol`I_h!ft@eWoVG~8R%hd{f{JIB69(b zM%~1;cYi^@(zPmOz-nk!%FVT`a*`~gUqK7Y4g3ZaQwuj>c#zda$Dw|#}@;plCeNAh4E%9fS?S$WlzVKZOj!6C2Y_gsDCH7Y=`flCyE4-v3QQ z4(aGT?YY&cFRR~bVdAaedp}Bq-6q92*m;?-mms;xxB7%Xb$5)=UVr{uYN0t;4;M;m zwe4|Ra>3V@eMqaJI*2JqHOmq%^qIU64O#qz374WhS&BEt?VJ9s*u}kZT#S4qn|nek zhDR7z3+9-3SGL807L{mSa63MtJu37kY-eg{Znw~H6zvK(k~UW0C%krvJmTs-K7z<_ zGhNT`wGuaV=rPP0{jm$p-j(p%5XImKjO#Kg4@>FqIB;>T@{`i)tOhX@F7#^oWIWfm zKZ7$kEzG0T9%m4VQ$GPBx(AU05myU#Cq>WN?fv1ZA9@=dDyD5+K7t6Lf__n!h z>GT}u5jhxaaxfp%@<1cUJE!zpMB*v{H&I^rCk+z%RzC(52B-I=xocLYdPd>xY14L1EgBe4lCaQJdAZ&|h}vDXOTH`JTQnIr#Tl zkUoE%`w*X}){o^4+BjMbaki=oz^(dX$a)Yqr{erj(KI)(iNfkv*VTMD08v< zjq>>9Km>HN;k&sWjwPuDotyf0eDW3|9jWoqc{=+~en*}q2*XGRI@rNO8$*U&no>B) ztusBjw2wg8hHI|s@jPHg7;)7xTY6iU(T%ULF*vMwNmdAPTA>3gWkqqWHQ-B_UZn)scqiPcsU-@Hm3jXwOC z_5vx+RSVL4cj`ZTN9n z9L-n`yvf0vP3dnw5^r31^KJT@I`Jl#M|NC|0eFB{pp9GqGb+eKpQXXvIRnr2CP0O3 z9&|FmuDUf-(WI^}GJQ`zn}qmcBuE)+9?SMSc+HWj&6OcJ<>3KF{@ojl3G7(%1QGKX z8Fyny+l4D`F7h|{O0Ps6QoUaUI0)@Yjs(v6)lEpi=62|nz+qXF{4n~(%32Q`IIw$7 zzxw{A_!rdGcs%L)TWKkzC12bhyP{h_^6haT-O8pc|A0*+O79zX)^VX8BbNJyo#ikx z&2Q4zW1_SbAHd~KbQFk9aO&&eOb4q9VnGJ)C18xiZ+{6`@K=7-_imnkk{D!UJW!v` z%b))hq4i=SLi#oErZdaqZ=yEw9!o6*CW3?|FiFuDhz=lQ8=l&PCX>EnIQsN!*uXOyAoWk&Ib4k@}CMS{m%H0oE2+{qedjHMU|3}REH=p!i5 zXSV~&sn4D=KD&cIixRm>M)^zL|CqIz2j<4tFk#605T0}O5vZ<|z}g46zfW5{IS+6* zUgzj%cQ7+Yasb-4^k)D^ik>P4)9+@)r4~h+juS0gAH~BUyx@>33khsr^od1d2AitD1H_Fq|Uc&jrBs)5LCKM--S!@GhZ; z=o$HVs>_+L=wpFu*j`d3K{ar~<0Z9fGi`UpWVJHiZh)(kegQBKR-MoljejuZa1paO zpzfuwq)XNdNco$tF=bKWv6>qk((UcsLDqcY=5R^j~`JLO80gb`)Im{6P z=*%*hacC|Mwd4EFageXh42*-BlU{BDqVYI#hS`92u$9Y}(sXTmw#Emjv7$8zS zr|~}`gyYrt&5&$d!QxShUzzr7QbfNYp8%Y@h+LMDD~bO!Q}`P&)J-z(&k#NWXylYP z3@-&F&v}`tWyDF<*YbctZX#k4c;iZuGx1Ki!+s~WI0q~lOivM7{00o0+X8)l2j6kP z9JDbt)doVC3`Fgs=x0(?24ccKI7trw0ok!VNxFn*;kPU;ye419ySB=8~ncaU&t;k5H97YbD0`#>B146rsA<#f}o?;ETn8{TVL$R()IT74qM z?bo7Y`cO)4?E_=-8~~rXub_^W&>~3Jrs^8 zSv3_S5qNV2)(h-2kC)ztD4=UKb5KiPNnp^fLDq%pN6bag{j@}g|C8$o_Lj9v;am!$ zqv9-^fb`Kn@scRBekBIS+&RzIGF(5qVHi~&0Efp=Qwc2Ap8!Av{;d*!>TCS8!%6;7 z^~NfF7LW+b@(H&-yrzX_60d47cK{G0Y&G!^I(Zf~s`^=-IA}5jW{u=c08!0!>fm~G zz&$3%;2F!K9D<;h){zfL$-7%8e`XRSmw^9I@iZ9zhJ5~5{ z()JJZnSi7co8F7gFXqxL=nQg6Z~~qolfhLu>-a*SPy1h6Oy398K@_JR5wHesOs}OoD5RLHVe0cV*t&3j zC{9?h*~_-gVM##C=FB@#kUX>TfY#KUBPTM$HCrOFdK_ycrX@u_M%RWuu}Po76d*KF ziM{2izzdGGjh6IBfFc3xMI34eJUxm2;+CSQ1B`o<7U&tk1Q8oWm>j$f(q8yV`2S+6 zKjEf>FIxMX=D%UCDaz1zEl~!JpJhvFN`#L~I3G0&P)nug5ZW_1NT5~MhEHe)@~k#@ ziKb)&y*yw}KDD9oZw7s@`74d?2Vgk@VIS3ZqSr|k#(|-wzi0l2<-|(_C%Pgy8KE)9 zz$k{q7x%T7uO zl+0$xi`5)=hCViKo8_qg0n$xHyW9|cNasfSwS?B9g;4b(YRHH9e6`g>&6@fUs-eXc zi~(m##q=kZt>V8@^hWD3{X!g}?Np?5>Zkvl{?zT1Zm6%GYc@dm$S2A>C0F;M!+JV) zO4r4cTZp)?(akS+O5;}iu8vMEk&I@I4&MW%R}(c>i#N56uNZg<__#X*p9osdgpZsK z9|4|Ee3~~KAnFJ_Lur@>cxu`O&uLWSXo2U1-X*+JOqcT^;l0@iWL>1LBzGXM6hUY_M|f|KU@*M5 zTktHBB_TDK*QChP9DYMV!*8epbm3!24TvYs7|KQ?Xwq&){aRl1q|1-+ED5mhUjlc4 z<>$RmBKa#5P#2r}lxzLXo?2DyAeR21U5Os&qN#wZfd<6=(zuk%TJ!q$O}h-zY`? zOp_Gzpfmrf4fuju;%|(W)FfbYqWG8<-btU>;Y6!WaB{PsI!~2l5geQ4=^Ds5;6_bi z!jyl<%d;M>A>4N!I!mpcF==xFjhX+?IvzCjO9PY_(G-p5c(ms1?nqVCmwjv>AeNH{ z_y;)PC$HrNNk;@~G7S;>qpcJoWE#{8ktw=hb zbt-K7#5iD{#RMQ4z?8iUOr!?=o1bKzy5P8Guge)k^n3E}<8wY(dx4AO&|9_zqs&6z4##&&Jb5}yw)HJ| zPB{8w42JwEoZNn@_2sR_Lz3d0CrOtez(A|72Ub>1zz)$*T1y8&$+0wh+y1@^=3dJE zhIxgzVFIRQcPAJzU?FC00fRN`<`J%Lp}~Lwpd^3;VBk9B4w2YN$+{Dsdd=`lXEzwB zUV#fhc&RcTH<2ORxOvo7r($L@6Zu7CkH^;voOVcBnN5UDK&YM}^kSDG^dl2ObW?;D z!PXOigr|%}=>Xcf8puIXFKK+1yh6@2I>;9x$!dT2^A-|#ahGQz1uzbJ;2wS-u9!0P zrjJ&);VO+kzOARMuJ^0Wa@l8uL`ZlgP$9=~6Hg;bg)M&(5 z?6q2Mh8&4Cf+-}V$Xd3?U${JUaoOk8j~~aB=xBQoj;MIA&>9t?LlcFGj^X0Z6uS5m zq>DfI85e(23uuFA6`bNBpj!*L9uxx$NEd!z<^_DPs|zSaJ|mp4qq(g=EFUl8vScAm z-oh!`2?UkQ@mwm#BQy{i5Il{vp~*kOy27ejayFN`xC<>D<37!;XpD_wE5wrpXg3|l z9$snmNdxHD=JoA(oebe|m+@~GcCWy{3oZQHk%@m|GXwviJMA;@Z&17Vca07Il8V4G zzw=u@EEO0S-<$=sQ+BU5@lIK{^}maCpn!I)`_*5M9P2h83D()hqrdke4<;Su1|=V7 zM1yr8iT6OhBSJZowgK&FJ7`dl$zxy{(ZaN;Db~=!^CXgp3 z`~}Dw(huS$y|_jnPcmFiN+Dus2S65@@5q{*N(kl%PmYx#<_ zGW>Z@)>)u7#D;BWI);?A29^;ERfzr+57?uu+o9rr4a(WRbva6}KzcAx!UscuG6y%qnVi>}puF>_gYw)qpq#jZKndoQ zJPYqq^w7jfV#7_4jCu;>gwSSTB`}ndY@{SM14qGg88z^=$ITCLz@g7)$^Qkrv0XMWsxDfS#b}Zyxm?7jsZnH`J5XF14 z05Jplt0_8VU`iE{H$!#-PX)4AV$%HQEYQ?4%KW9vD06lic}nYsm4RQj!J6Zj11^{2 zv57B7VLuH{LB-fluhLAJGL2GwpXMe{Y=M@#_R|Egy)!1L_U1E3}9?HD6v?Y zAW5>Tn~^5M(s|ghAU1v#qTN9thG(hJql=%mfG|~vKu=IdT@``uGHHw{ZyIEVEE+z7xTDsEo zOJq*6?72q)-w<%hq~Zp>A}31Cfq~a5vlAn88IL-*EeQ%$eF?54b$y1N`8uT*`pOpk>#Cmsa zh&U=WEGbMa+BH>N&_*%*bg|JVnJRwV=Jis%7N!a^m!i+@)d_($lM ztOBT8*;lVy9a;)2oCe3BFn>ag#b++;7 z1H@p`QErf76C?c#=GY@eIg@pynJPxM!KtYT_Qrc0#*E~kHm`qy*KDe=WXHi6W-IHyBFh+? zElO4?z2fi?>R8=oLV&{s&{gn|r(h%kNrExQRJVUcq@$~Q{-Av1478ZeV|O7954ox2 zQm}Kof3zLGM`XXa!Gy47zt}_&X08hech!C|`=AyK1}U861xPP>%-C<11!L+_2Ppz- zXMprC;B$gCe;@BmeJfKR)y6^{HKjg2T;gO=hR$xOrL2x#l%bBE)8@=2C4qIUk<#^B zn+#{uNPqCCfqHd8p^-kfdH#^+8LX+)45JoJsASX42JlJ>wPodb zJ7(=EVE}vKsO>50$=FkE4LzRMv8T+_Oq+am6!52g{YhHrAn-^FYhf1$;q4q6(HyG8 zMgST;`%nfN9S!|SEC`!5+UigH|7D^PnKh0M{TbFqzg&b_rr@JUHJznG3?AX00r1=p zJ-`qR1QmLs%ed55EU~ zZqw#sx(+xB7p@7p;4Br_`>0v$4B|z~HOq7XVX$Cr$}!^45Ax(fE5r4zjq5CXHZj7K z0oU@+XiDv3lX>>%gDAm5hjvTz&gj@ao8g^tmkD9^&d3$s8Tk5`duN2^+r2Yv`!$`O zDV_7r$n6$7pVmo$1i(o1&Ok(o!}rekg;*x?UjaP>gvdLiYY^&4Mj%tULC?s?C!HUT zo!34+%hWeMrunfd58dXucF3Y-*c^cLqQQ?+n~H1bwr6 zXPAw_MWYk08O(U78vw9dXe4?H{aiR^C|Q%y#WC{Bcwz?cu;4siw}-pBZXH`X_z2f> z${2>VOWWn`I%V_{gE7_4T*E2j5mq+ogd7KIr1cyoYIW?CaZww+a?i`u6Z#}e$2o0Y z{|B#)6LKG^@tyJq%CK+vaqMHzsZ4y^NB$TlzMa!9zFls^H`5!#!nn;nJ8;D?vFu03 z7|U#a59Gd;55F2Psl&&z%^(BqSvER_WwycSe~t1;(ot+s5@!Qqxs$kdaHJ?^syS(v zjZ53$&=Y^RP;5~0!Zxp`;dSy}Jf{1UDSrV|R!j50fN+sI7Rcm%bo4#cndW^#@q_-` z-WTUa81jY1VBqDo}HHaW-IA0t^NVavp`0^!!BpLtC0x?gs@0vJy5dEAak4!ki zqsYKbyZ40v<&k?|yj5o58w6trCFFf^6d}~s`(oK&O$f2~MM^)0*0J}+ z3#A5J{>#GJOD~$jT1BS4tMmRBfD2BBZ9=Tw`+_j%h`cY6NWMb{v6P{Q5T`(pa@=4LXyFKqhH5yQuB z4HWP>V(*L5zt4aWSp$z0LXh#mtUB<%xcV<9gxLGyXu`+teIej;q}~_j-IoC#^1e8V z;A!i9ap9j$@CfgVH29eQ7wY?#{{`RRg#!kKP<)T!-Wl(UTzFs9P;JYEaS-o|0TAa5 zTg{zICR_~ZtpA0LrXns_ruT)b@r`uFd*gHPzL;mT$A~kw!{+~X z>!HB6qvd_E;CB|tg%m*6!>)n5E#a~m>DyWlYyM>7o$$VBM?dVG|3zo*5kEEcL&N(5 z0E9i_YCP@0`@&Qc4XZ>ut{0}7D2x#@86$=X#|zUKaSZ$}dc)&9r6g8#b)7mkMWmLC zoGyM2R-4+b)5XPTH+{3+_KeNx zBEQY+EAU#JfLs1$nf8bd@a-_|5&hf6H@|^zDQ^oC&BB|r+IO`uu}l~vjun>K{2Is< z@glC$91)gnCc@gzEu>CinQbuo8Db>qC^pEjagm;dz!OJ`V(pqD0&Q@p_x~6)i<~Tm zwRyemDZ>=e_C84a{uQ0EN7%h91kL~7@~(L7E{jJP`n2I)aTNJPTknb|XA{g*-W6%` ziDjSo?(ppscIOHK;QyL)h4)T|sLeuQI9D7+h_-dED0-^$r8|*t-H6=W|_UdnrdgbFX*CyW;dGu%m@{g^yH>Bb49T z*QbuiyJG+C)}|DK+tJXcQZ%|9?}{F?O!Q&z3LE|$Q}`UFcg4rI{kP%s#p4~pC*8i) z8UG4wEL=^r*>TdmD|llaws*ynTdh4MP9w>?qHBbwqti$_7w^!!V)hnDVDkj5`vhLxnQbMHkm``LlBl6)*jk=NEd1 z9V$%!L-e-~MvNT9moG8qpA|eGDtL~g8g4t_4+(wT1iVS`J#Rcg#PskGa!OkC_>ztbZ1znfB=>vREaX3u{C7IPPQt0z&c6}=f+B?^%E#gVUUZ32 z`)(?#6r#R755?kjT%-_yrLS^PfkHB2Ef@1q!4ackX)YviFV`(~qd4b5t}qUD=hSj> z0*a+IT%3%ecMTV(qUc`D#px(Ez01W?6j#5)#Th70sODlVinWWlSdZe=N-kEQ=v~ak z*(kbJad8fc^Z(7oxhUqp#l`t3PI#M(>}0%xi>vW;&T=kppt?)B*npyI0T*?u@ERAJ z=;;zJwxC!W=b{79alOgK90Jt8xJY5191qHlRDPZmKK$@k9pQs=!{Oq?3Hb6T;)6Sd z55*~bnD9Q&jqsu25iTkO5`h|o4-F5|Q^tp4uFLpPt8v|NsOx@?ixW_E{E3T`QEZyW z#i=N+{wEiwqge4W7fVr``VTJ7K(XN&F4m&xoz2C16q}ylVg-t|PjhiLit}IL;v5u< z|IWp^^z?tYI3GpF-?&(b;+&VbxEjU$7rD5B>i&g`4Jek*dFOjl8|*%vqIllOy8LVtHaJB8mE z?$+X}`V>To3Uk)BQZ#@*;cBo?)T_te#t+a0_{|~mr65!^?wTQA%FOFJmoEjADg}@z zT_G+rEkXS5W`2+x>P|VbU>?|(gHThP)2QJ&WFDsJVW2ri{tD!>tHcv`u>akCk=ZBs z8me7Czt%4uUt57TN7aKUUr?#GRusPuewo23~{Nb9HInFN%>QVm9}y zEi6WT?GXz9T(A~d1_yk-`|DWt`5E!rW0&ACc3V!krT5()Bi0lpWvQyCrMRgcfr0j_ z3}qTJ=F{p~R*7K2)_Q2=gV6y`NZj*F)M7dZj0awaR_;lAk5F0}zt7G1ouBqQyw`xw zNq3)#^gh9#hWGXkooc`baN!X37*}|&GdR=?UQdt`Z@_;{IH}ETA!UBC}A@H^AbMU`E2pqSnrf?na9SyBcmu`#j3Ofj;Z1Di#^A*^g zCl(pl_3qtgb+E#w{Dk$L(~0$sK5ee==G6M~!l@65=?Oqn$`EUP#rsEikzp{eddFh+Hs{zszB1oTE+{!b&{ zu@O74@hHJWxTP2NlSl)hu3=hK)dZ@9AZ0g233{c3qM%24|F-QXpUMOYn>9kO5mJr` zko`9iAp373K=$86fUMv0HU2lT0_3t@TS)~g&k$|?8kRz}=ZMJqqy-BJL;^J*l2%r;mXiFxvb9N27g<#)rXju z(IzSKAYS=31;L1$ZU&9aQ=I!L>9(~tKJ8YA zSRab3woHIxyRkru_IsCtWl_$)K1ZY>ivc!xnoGYz$hUnam>*)EQFH|t1vbm2z-Cb2 zmLmU&pi^HC0U-2`q1YppBIdqhG;$@8=Ak%0L9j<;&8E=tWYlaj?t4-(@e=<^k$jbzk5trzaKi|I>+iRdLWX{?@p3U<%IGQ= z*4}-wQnH$CKjAuTY2-xE#(pnH>9;0;I}aZ5w+^bV3saiikXY0F3t zMhs$}A4EHPH?C~qv%lfS}63T!PTI}*&F?tXi7ktHb~pP?tbV8X4I z55a>VaC!w21lBuCzxc|igzo8;QBee7w$_jKAs9CwLhLggB)}Et@u=o;auMV|x9=aK z2^RfSb#iSMLk`4V;34bRkN~eeE(RRt;b|I;7bXpGyZ*#_{3`NE<3_ehkqc3ohUiDm z2^Z2!K<4Q8fgc1Z7h!#aIi>Fy55t=wcy2iWloJ)nXItd90Z@!6;554A6W~7veA}I$ z5StZ4h8_tCUR^a{_wL=XbI&hKPCMj$iIiJY<+Z6Tkakp7Qg1X~$4H7wnjt+aTEo zVlTQdG_iYJ`iVFT)Mk-#hlJ>tnEX@SQ0+!tBgAtp5_~jt4>%iO3Ik~SA||ckQvVYm ztCNPToV4Mtz4Ab7;j`4vv^oIW#hG!4MfzlUQ8!NozG<9e7baa5|=`N^7 zO8nFAigVvY#TG<82hY*8;I+I%#b~6WdgW}&1*(7f3m71f+7sw5MJ_`1>PO5mVKIvh|uE03a&TORb6ks2qTebxW#$eT8wAQN8vi3 zK1DSCu31ALFB;F~##dsCchLAC(Rl8`8kbN9d5Yz-xWf&;QMRWQ!hlpx3ZooP_LeI@ zK(~t=m;nbTuci{x)nn-0{T$LALEko7Ba0Uklp-F2CxX0ZbCmkf?-AskAW!MpNZ(+x zRQ-(k71~nqUGh!n+C-PC-?F{Ao!(qX49KDvY45L~_sBwIdyjFSELW~UBgv8YP~Qfe zu+w9CECeM&OAAebpZeXHjuvCwToF8t#=&gSr0)j0H4>IFx+Q<6nE41x;Ng+|2mH5e z!sIEC>lR+x=j>@Yh0CRxwTKr_zmMy;9REMTdPD{(Yd)kL!$9%MQ^=rzl;9`pZ{qHe zlIT7g59lAUlqHKMtwI>xV*IS04q>!LeI0u97MfWF>;KRcho4`H4}vFCM&FX<7>-v7 z591*@LX9<1d?i=bBKhW``S@Se9Q;pTgPYJb4JavU_n%Pz=qh>NgFO&_J~cl1>T0sh zB25|cw|vJ5R<0VOUxFY3l(-;)+%_IB;Rl;^pTm%d8}tu&fMrAj>V$8L49eWwU%7IGNh0tovJ0usC2=?7(qZ$YA72&AVgWlJ2T_@EF{ z)s;DZb!`ar-e`nS7!yE37@1?YBg$Xj(TFND^&NvwiHP=VN7EKCg;U6%5Ao=+nDb{L zU%?R6Hf>GQt}xRQfYTiRD7`)(W9Qd3^HEXnZ`JrGp+^e-%D@XD>c)3;!})Kqg6tj$ zqNw$quV~%xdGZ_w;`3@so}x|7SJ#6ryODJu{*_B9K{A=jJZYZXH_P{E9u%Px-yib` zC$#KkQ-P(pk-^yc4eaOOPaA|#tbUV{g|XI_D6C!XR4tf7cvQr(0nuUeA2>qrFob19g*dG7@hk|e5HAT*kG?e{zVo@46c)oj{TE1 zi>wv7f>(nZTuWC(P6QfJMD`xxxKq8%Ez_DZvj}m<77|nC)+gw@093&=iNGcYlSEDX z7if1M(&!<}_+5a{m>e`pK`jRFMbn|@$!2h3ROpr$;cu$5;NyaCYa`tSD`MGEu+>&2 zE35Hj(;tO&sPG=I6uAj+{6iysrelyHh%lS<36buOBpthe4=UP7?CUY;8;6g5TS!bO zTTWhc`lxfdJ17d@CcWtaj6fTiw;ag=A)AFqx)GXcWM1-2ZbImns7D#Ae@0Db(^%S- zCz|V^L%ko+9vK24O3`xO7%LFLE7(WY${Zkf;g+-RzPN@6pS}ph4*L(OxTFXr?t)v| z2>c1Ct0*O{7N$1*YFQ5I8XpH#jUK9Tj1mfDcEGtJpu!e)bB?TX>trQuM??d?_i?U8 z$q$`T8~)^DzZiNz$ZD4HHwCS-NQpv!~1S(>kPAMJj?r89t%$h(C%Ghq(~B@ zjzWUl7yOx`{ZYXP73~S4Mwp!xS&g@fTEV}~$MCmZ#*ZHmwHMR0i0+UqP*j2in-~Ly!w1h|uU7(H=z|1;qHrd=#;YkMkoE(uNb$ z3}N;~`DD~ab@2|#!c*15TwVJ;@(s$wM8g%_Vw6Xz{)2uIp;-bMdIrkL^BEJurBr4G z&EhBWRSZvIhNi&k%wHB#&FFST^69`PKrnPt@?lH|{3E&%dXE$-#V=*(Z7xOo6Loqg zRLxUp$Ay<+#W^L4kqjl)$15#ZKBy08Mp0KIFAB1wAT0`*U96Aacuq`!0MAcET9mgb zEy~-J7A28QOCv-I{akQwF@{iS(SVmd0qiDXL_Sp)q&35yd=;^s{X8w*MNrTdGpw+N zP|Li)fahrZdeBQqj=62Z`e}=2WB#1diw z@Kg4gH#pJE2k{-0mIlj#jHn_l4Zi@LACzs+MdQ~>i$Qfkm$!`4&TIf!L=p@bW{fM^ zg;`RR+@O^(a_Kh5@oPvneu0LG3~6A2-2z&6{b<@)r}NAZ)qb?PN^S*aBTbt&B8Mh@ zJ%9DUh7<5{;R7@WDe@|!bkJok7f@;FTph2Z=qZf9by{fyeim$}RLzCTeuHz#+ipV;*f-kO$W99-w#4$XcUrsw&1RN>K#>5N4iVh!V2RUyqWy zrl2;S%tG#A5K<|627p0Qdh`HB5Z(f3_w6X~P2YezP|+UXO;2+_pCAgJfFMik@#_zl z&PS&ZSt_UwFbI4hpCX!qJ;+E#Psl;+Y-Fw!$%84Ka|+iu(4pQNgiE~3ks`IQk|##G zY0d?;@?yfl#?zFT;#}@%RMgAzyfdc5wvY3Nk$GTv!-+Rb<#E(vaKbzfSpH`>AVj15 z3mPMKrD&Jsp0j#2|lhA7%`5MI^05G!CzzA|7>?2h|mzdKNNfXo=uAIW1Oob0~b zk(3tfnxQzoGp2S^SH_dM*aRqNDY2>9ih5Vx3?(~B?t1YC%xaUmT*(@wZ}?$~&uXtR z$tOmJNrMtfk&Wm;t0pE{u!6FxK27s4i7VQ@WC+Pd8mAr##x7EA#C0TA8zf? z0c{?0ue*m5_u3ArnW!J3F?=Y{BK-s65jP+Q3-bw~5~I8nX(9YqXVWX*Kms4rc99$| zB`OWypyUNX^_U7MT4gSYK_xJ;G3ijWDt_?m zo%8@yQPCFhgIA{-H3V^qJkL#okSO6w+RdOXeFzYO{2}Pp#z_@Y1g_Gpi|(`(u6d+zU40K^^gD(hXf3_R zd2(oT_^Y1kJ>8OUV1Yb#k285K3d5w}2+;34mxlGJ!=!18lRv@31Xo=}Jyz(xXmkc@2v(6}Wqpf;>$OAFqUXE@{;#ZKfH z({tlnUGfZX7Jhe2e#izd&|!f7eANIw7TOI+`?L|+RZ}5Su?0nMB>G?n^QhY~i}2DR zDP0Eka_vE4OlmzqIP?KJV^ANU2U7HWz)Vrs>)Cf26xPnePNtU>dzC1+Hh6}#Qh(8fM+A9|X33tCwNFx3zZ*WdWL zK_!<+Q8$lz_}+3#j zGJ<=?v;OH2f_8<#SS`}%pJnyQxSa$uX-wJ@_7BuT0@1in~ zx~!hKG|IWmHU~aRje*dsTtMbnG^r#;=Hm~lVL=5uxc6I({avg{kya+U$G)pa9ly4S zI+it+hQVE%z(3VCg7G{8+QnK0jeuDKq&;~cHwTd#It4W>MQ&!a8JP#xB6v7Ill*M2w;omoe!Kr2W4Ae6@g~`D2`Ba_OO`z*s zs=gG}8&`=<=%V)9K6zs)6xf*_^4juzjoV+0$H> zF$=Aec}*^YMq4iT_#pCRP`qX#hk5fC#eX^~2Z-)}lE&W8Y1| zNZV_~_B}9UC)$W7dl=x|oPscthGfE*=hwr&98J$_-C#k#>vQMei4R$ zL+YbR1iyog2Zo%C^*+RSY#(%fJl02t9FN}B@z_4-{CKR7cs$q-2F7IUhk@Oi5l4_#>Vj#iwI1p3sf+aYf}es~ zG6P8Cr_#K1hRXfXcvAEoLjsj9q0Xgwb>u=2Z*d#r0ExG{OPxQYf3M+l9{Rz*ww+%X z9K)L5!T3{e8W-YLTO`&8es>Ul#`UerCVyG98Qw}0LdTUmaRWvxdOFi|DNbKEWL&VN|M%4kQHV2w3F>$!&oim*IxezAOD?)8b3+Q z?ZJ2N;lkJcA>i9l`&$pUefx*0^%LuDt{)I&Ai*&WMTp*5>)qMYvbns1y-?LzFm7_<}sPQer zYz{WQr&b>P{Vxyk{fc)F{{2&jn7`ltKE9&|O%Yzxt&&6E0mEQwqI{l-moVzi3Yq>v zPvg87{z0X9K>rv47J1jfKd1mdt2Y84g?~`pkH|lW4uZe+$3M%z${+vkJaZEo`_J#( zmbnE>sZEWM-iX4ZZqlZ7cj#wN;T_nA2J*kL1IgV2_}XSZm(p%jVv}JP#c8`*?#4z2 zY3a(;6xfDATnd8!t5Y-hH~c$RlOIllirn5)$Tb~VzG0v-5`81MH1K6eK%3@fEfTANzj00d8UQ`r`BNzu;jru*1L+x;hZ9SazY zF8n-r0nUirIRd$@D;eb54OjRn$jT0XcsLZ@=g9{~4@vp!`{Q5c`{S+Wjdt!1sQ0=X zs};2!^88~Uhd8e%>kawHG(baw)Iy$3I`RC?0Lny4mp;YQLfLw@fq4*{Dh$F1Nf)dx zbq5Cu6uML}gVe>5)F?4Fu|2&`O=@xKq*FTud z7*}%#LtB>KLHZfflZ!^p3~jA8wjxiK7B0oTyS`at!N?%2l=z;=^C8LJp?I32-r{DX zbGz{Rr3Ej76{nNe1-=17K~D?tP56WXP66<3IPH_<$=o}U=Lh$0XYLfY&mbKE@}{?8 z*B98m*kP-t9uw?}einp8k-{#!CsKxMSOuKWfKLa2PdftYUG7FW$G3w|Kl?AkXF22x zz-Qj=#|S>5{~zE(ZpR(q&tGxy4fs5A+cARA^dT8|VbH0AV3Mh?5#BYzX3-87{S;V~ z+F!JL=$->_Fl;DmgAwf^a$;j(km88q@1RYWMq!w&OHpzb>Ht*tPGnF`y!BXt>Su=m zDlg;|g44Qo;Y8kSmi|h#n@P30lZiD`aH7`%?It&$nd}2Sm*E~9^U>W!$O&kv8VBJnrI-Xn zm`mM_@3#vscK9>lMV#M+7~>51n_zlC9pZA}ZQ=W1o&dxMg3^q-!-A3L*9@cMZ~4Cg zBU)Zm9PkGy2qJ{CW8B@aMA|8A9() zJ{Az_`W`!~_%jXi(YL021ZkY4aERcU6evwTN|(Nu-@wqBchj+e4jjSm?*jk-4D!); zkdK&F9a26rq-`cSpw1~F8$>!qe?Xiebc4^`|I^ON*p@x>$mHzXu4lMjGl}7P%;fBR zX7uey04!&x*(*9jkB%Il?!J!aedBMA5k5^nX84qX5B5|_ULrOI;i*9=ZX_?k-7}@| z8}bq>DH-w-_M;SCNE}sIH8K_7%ivlD(mi#dkI;oln{gq5L_WcLX-*)hd5Kh!* z?$5hV{W`Vz1emUILkR}tp!{O@#psT`DJ`wab4>S;jM1S&&Z_RDia_kQ?df_kJ3v#q zUZ$SA0~-LqUH|LU{`&%6&g~d(q33>UT5d_d?WF0J)m-8E*AAGjB{1>7)cK>Z4s@1Y zP5gO4jr4Bbj zhGi;E?|Zx5-8legXUXSu{>Szxc4*@*3Q+;vFZ zQajM3AF%0-c5c&ey_&&z^^L~@jE7ccCRJeckkfB>H-6AAs4g;gsI@=o{&?(M(r+DO zNMC^w8o*@Txq&o0Tv$O|Gt%#REz1iotNj4XJ%~^yv^| zka10`9byCpitX{K`H?`BnqO=Jj>PIH>Y_Qp_mvnfiKX!~IBAf9TZ)Rg@=;mZT>I+x zjODqOT>rm4SJuDev2-G39G+hV%(sppVprveV+fp1!K{Hp!M5hq($tEkpGM3=@KZ3B z`>#8uc-`@0iqi<37lYGc8pFREH2}A-FRfgS6UBmNK3A*Fqr2GQ=^GrOoi#}u%gB{; z_TmC|1^!o6ivQ`wr@|fG(Pspd{qC=i$4sEpCiew;poPYF4LWm`qVDG_=idE()>?)ELf(p_dIb4+U`qU++5Fl4dtLbKf7ZTk>&jdQc^_Mi8f&hDI zSG#F0LLLzJu`S#8TyjYc#471eWp`>w+;JQoT~T=)z6wabdieg2%R!g}Sc|*}r=lho zdGSWHOeI`$)GtH@S-bSia7$n5j}PJ@rE$wA@9I2R{gC2;1XPbluG~glB;we}t=s_G z{;sESgVlr|t}PQR5CEYs2SAXbIo+wjXFS4w@Uo|tdevU=)brn$J*)Ze>z)n#_bpEY z{vv1sfp|%UT8D^Vv)3xP8(4}54t0YMFlCP}7

yLJkWw5=hkAIg<07^3C`GR7Og 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

yh(!EbonhEoio09Tl}S*Gr+w@9x>rE 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 diff --git a/old_tests/dynamips/test_atm_bridge.py b/old_tests/dynamips/test_atm_bridge.py deleted file mode 100644 index aed46f70..00000000 --- a/old_tests/dynamips/test_atm_bridge.py +++ /dev/null @@ -1,62 +0,0 @@ -from gns3server.modules.dynamips import ATMBridge -from gns3server.modules.dynamips import NIO_Null -from gns3server.modules.dynamips import DynamipsError -import pytest - - -@pytest.fixture -def atm_bridge(request, hypervisor): - - atm_bridge = ATMBridge(hypervisor, "ATM bridge") - request.addfinalizer(atm_bridge.delete) - return atm_bridge - - -def test_atm_bridge_exists(atm_bridge): - - assert atm_bridge.list() - - -def test_rename_atm_bridge(atm_bridge): - - atm_bridge.name = "new ATM bridge" - assert atm_bridge.name == "new ATM bridge" - - -def test_add_remove_nio(atm_bridge): - - nio = NIO_Null(atm_bridge.hypervisor) - atm_bridge.add_nio(nio, 0) # add NIO on port 0 - assert atm_bridge.nios - atm_bridge.remove_nio(0) # remove NIO from port 0 - nio.delete() - - -def test_add_nio_already_allocated_port(atm_bridge): - - nio = NIO_Null(atm_bridge.hypervisor) - atm_bridge.add_nio(nio, 0) # add NIO on port 0 - with pytest.raises(DynamipsError): - atm_bridge.add_nio(nio, 0) - nio.delete() - - -def test_remove_nio_non_allocated_port(atm_bridge): - - with pytest.raises(DynamipsError): - atm_bridge.remove_nio(0) # remove NIO from port 0 - - -def test_bridge(atm_bridge): - - nio1 = NIO_Null(atm_bridge.hypervisor) - atm_bridge.add_nio(nio1, 0) # add NIO on port 0 (Ethernet NIO) - nio2 = NIO_Null(atm_bridge.hypervisor) - atm_bridge.add_nio(nio1, 1) # add NIO on port 1 (ATM NIO) - atm_bridge.configure(0, 1, 10, 10) # configure Ethernet port 0 -> ATM port 1 with VC 10:10 - assert atm_bridge.mapping[0] == (1, 10, 10) - atm_bridge.unconfigure() - atm_bridge.remove_nio(0) - atm_bridge.remove_nio(1) - nio1.delete() - nio2.delete() diff --git a/old_tests/dynamips/test_atm_switch.py b/old_tests/dynamips/test_atm_switch.py deleted file mode 100644 index 6617b199..00000000 --- a/old_tests/dynamips/test_atm_switch.py +++ /dev/null @@ -1,83 +0,0 @@ -from gns3server.modules.dynamips import ATMSwitch -from gns3server.modules.dynamips import NIO_Null -from gns3server.modules.dynamips import DynamipsError -import pytest - - -@pytest.fixture -def atmsw(request, hypervisor): - - atmsw = ATMSwitch(hypervisor, "ATM switch") - request.addfinalizer(atmsw.delete) - return atmsw - - -def test_atmsw_exists(atmsw): - - assert atmsw.list() - - -def test_rename_atmsw(atmsw): - - atmsw.name = "new ATM switch" - assert atmsw.name == "new ATM switch" - - -def test_add_remove_nio(atmsw): - - nio = NIO_Null(atmsw.hypervisor) - atmsw.add_nio(nio, 0) # add NIO on port 0 - assert atmsw.nios - atmsw.remove_nio(0) # remove NIO from port 0 - nio.delete() - - -def test_add_nio_already_allocated_port(atmsw): - - nio = NIO_Null(atmsw.hypervisor) - atmsw.add_nio(nio, 0) # add NIO on port 0 - with pytest.raises(DynamipsError): - atmsw.add_nio(nio, 0) - nio.delete() - - -def test_remove_nio_non_allocated_port(atmsw): - - with pytest.raises(DynamipsError): - atmsw.remove_nio(0) # remove NIO from port 0 - - -def test_vp(atmsw): - - nio1 = NIO_Null(atmsw.hypervisor) - atmsw.add_nio(nio1, 0) # add NIO on port 0 - nio2 = NIO_Null(atmsw.hypervisor) - atmsw.add_nio(nio1, 1) # add NIO on port 1 - atmsw.map_vp(0, 10, 1, 20) # port 0 VP 10 to port 1 VP 20 (unidirectional) - atmsw.map_vp(1, 20, 0, 10) # port 1 VP 20 to port 0 VP 10 (unidirectional) - assert atmsw.mapping[(0, 10)] == (1, 20) - assert atmsw.mapping[(1, 20)] == (0, 10) - atmsw.unmap_vp(0, 10, 1, 20) # port 0 VP 10 to port 1 VP 20 (unidirectional) - atmsw.unmap_vp(1, 20, 0, 10) # port 1 VP 20 to port 0 VP 10 (unidirectional) - atmsw.remove_nio(0) - atmsw.remove_nio(1) - nio1.delete() - nio2.delete() - - -def test_pvc(atmsw): - - nio1 = NIO_Null(atmsw.hypervisor) - atmsw.add_nio(nio1, 0) # add NIO on port 0 - nio2 = NIO_Null(atmsw.hypervisor) - atmsw.add_nio(nio1, 1) # add NIO on port 1 - atmsw.map_pvc(0, 10, 10, 1, 20, 20) # port 0 VC 10:10 to port 1 VP 20:20 (unidirectional) - atmsw.map_pvc(1, 20, 20, 0, 10, 10) # port 1 VC 20:20 to port 0 VC 10:10 (unidirectional) - assert atmsw.mapping[(0, 10, 10)] == (1, 20, 20) - assert atmsw.mapping[(1, 20, 20)] == (0, 10, 10) - atmsw.unmap_pvc(0, 10, 10, 1, 20, 20) # port 0 VC 10:10 to port 1 VP 20:20 (unidirectional) - atmsw.unmap_pvc(1, 20, 20, 0, 10, 10) # port 1 VC 20:20 to port 0 VC 10:10 (unidirectional) - atmsw.remove_nio(0) - atmsw.remove_nio(1) - nio1.delete() - nio2.delete() diff --git a/old_tests/dynamips/test_bridge.py b/old_tests/dynamips/test_bridge.py deleted file mode 100644 index ec415dbf..00000000 --- a/old_tests/dynamips/test_bridge.py +++ /dev/null @@ -1,31 +0,0 @@ -from gns3server.modules.dynamips import Bridge -from gns3server.modules.dynamips import NIO_Null -import pytest - - -@pytest.fixture -def bridge(request, hypervisor): - - bridge = Bridge(hypervisor, "bridge") - request.addfinalizer(bridge.delete) - return bridge - - -def test_bridge_exists(bridge): - - assert bridge.list() - - -def test_rename_bridge(bridge): - - bridge.name = "new bridge" - assert bridge.name == "new bridge" - - -def test_add_remove_nio(bridge): - - nio = NIO_Null(bridge.hypervisor) - bridge.add_nio(nio) - assert bridge.nios - bridge.remove_nio(nio) - nio.delete() diff --git a/old_tests/dynamips/test_c1700.py b/old_tests/dynamips/test_c1700.py deleted file mode 100644 index 222d5a8f..00000000 --- a/old_tests/dynamips/test_c1700.py +++ /dev/null @@ -1,167 +0,0 @@ -from gns3server.modules.dynamips import C1700 -from gns3server.modules.dynamips import DynamipsError -from gns3server.modules.dynamips import WIC_2T -from gns3server.modules.dynamips import WIC_1ENET -from gns3server.modules.dynamips import NIO_Null -import pytest - - -@pytest.fixture -def router_c1700(request, hypervisor): - - router = C1700(hypervisor, "c1700 router") - request.addfinalizer(router.delete) - return router - - -def test_router_exists(router_c1700): - - assert router_c1700.platform == "c1700" - assert router_c1700.list() - - -def test_chassis_1721(hypervisor): - - router = C1700(hypervisor, "1721 chassis", chassis="1721") - assert router.chassis == "1721" - assert str(router.slots[0]) == "C1700-MB-1FE" - router.delete() - - -def test_chassis_change_to_1721(router_c1700): - - assert router_c1700.chassis == "1720" # default chassis - router_c1700.chassis = "1721" - assert router_c1700.chassis == "1721" - - -def test_chassis_1750(hypervisor): - - router = C1700(hypervisor, "1750 chassis", chassis="1750") - assert router.chassis == "1750" - assert str(router.slots[0]) == "C1700-MB-1FE" - router.delete() - - -def test_chassis_change_to_1750(router_c1700): - - assert router_c1700.chassis == "1720" # default chassis - router_c1700.chassis = "1750" - assert router_c1700.chassis == "1750" - - -def test_chassis_1751(hypervisor): - - router = C1700(hypervisor, "1751 chassis", chassis="1751") - assert router.chassis == "1751" - assert str(router.slots[0]) == "C1700-MB-1FE" - router.delete() - - -def test_chassis_change_to_1751(router_c1700): - - assert router_c1700.chassis == "1720" # default chassis - router_c1700.chassis = "1751" - assert router_c1700.chassis == "1751" - - -def test_chassis_1760(hypervisor): - - router = C1700(hypervisor, "1760 chassis", chassis="1760") - assert router.chassis == "1760" - assert str(router.slots[0]) == "C1700-MB-1FE" - router.delete() - - -def test_chassis_change_to_1760(router_c1700): - - assert router_c1700.chassis == "1720" # default chassis - router_c1700.chassis = "1760" - assert router_c1700.chassis == "1760" - - -def test_iomem(router_c1700): - - assert router_c1700.iomem == 15 # default value - router_c1700.iomem = 20 - assert router_c1700.iomem == 20 - - -def test_mac_addr(router_c1700): - - assert router_c1700.mac_addr is not None - router_c1700.mac_addr = "aa:aa:aa:aa:aa:aa" - assert router_c1700.mac_addr == "aa:aa:aa:aa:aa:aa" - - -def test_bogus_mac_addr(router_c1700): - - with pytest.raises(DynamipsError): - router_c1700.mac_addr = "zz:zz:zz:zz:zz:zz" - - -def test_system_id(router_c1700): - - assert router_c1700.system_id == "FTX0945W0MY" # default value - router_c1700.system_id = "FTX0945W0MO" - assert router_c1700.system_id == "FTX0945W0MO" - - -def test_get_hardware_info(router_c1700): - - router_c1700.get_hardware_info() # FIXME: Dynamips doesn't return anything - - -def test_install_remove_wic(router_c1700): - - wic = WIC_2T() - router_c1700.install_wic(0, wic) # install in WIC slot 0 - assert router_c1700.slots[0].wics[0] - wic = WIC_1ENET() - router_c1700.install_wic(1, wic) # install in WIC slot 1 - assert router_c1700.slots[0].wics[1] - router_c1700.uninstall_wic(0) # uninstall WIC from slot 0 - assert not router_c1700.slots[0].wics[0] - - -def test_install_wic_into_wrong_slot(router_c1700): - - wic = WIC_2T() - with pytest.raises(DynamipsError): - router_c1700.install_wic(2, wic) # install in WIC slot 2 - - -def test_install_wic_into_already_occupied_slot(router_c1700): - - wic = WIC_2T() - router_c1700.install_wic(0, wic) # install in WIC slot 0 - wic = WIC_1ENET() - with pytest.raises(DynamipsError): - router_c1700.install_wic(0, wic) # install in WIC slot 0 - - -def test_wic_add_remove_nio_binding(router_c1700): - - nio = NIO_Null(router_c1700.hypervisor) - wic = WIC_2T() - router_c1700.install_wic(0, wic) # install WIC in slot 0 - router_c1700.slot_add_nio_binding(0, 17, nio) # slot 0/17 (slot 0, wic 0, port 1) - assert router_c1700.slots[0].ports[17] == nio - assert router_c1700.get_slot_nio_bindings(slot_id=0) - router_c1700.slot_remove_nio_binding(0, 17) # slot 0/17 (slot 0, wic 0, port 1) - assert not router_c1700.get_slot_nio_bindings(slot_id=0) - assert not router_c1700.slots[0].ports[17] == nio - nio.delete() - - -def test_wic_add_remove_nio_binding_for_chassis_1760(hypervisor): - - router = C1700(hypervisor, "1760 chassis", chassis="1760") - nio = NIO_Null(router.hypervisor) - wic = WIC_2T() - router.install_wic(1, wic) # install WIC in slot 1 - router.slot_add_nio_binding(0, 32, nio) # slot 0/17 (slot 0, wic 1, port 0) - router.slot_remove_nio_binding(0, 32) - assert not router.get_slot_nio_bindings(slot_id=0) - nio.delete() - router.delete() diff --git a/old_tests/dynamips/test_c2600.py b/old_tests/dynamips/test_c2600.py deleted file mode 100644 index 53bfb0db..00000000 --- a/old_tests/dynamips/test_c2600.py +++ /dev/null @@ -1,216 +0,0 @@ -from gns3server.modules.dynamips import C2600 -from gns3server.modules.dynamips import DynamipsError -from gns3server.modules.dynamips import NM_1E -from gns3server.modules.dynamips import NM_4E -from gns3server.modules.dynamips import NM_1FE_TX -from gns3server.modules.dynamips import NM_16ESW -import pytest - - -@pytest.fixture -def router_c2600(request, hypervisor): - - router = C2600(hypervisor, "c2600 router") - request.addfinalizer(router.delete) - return router - - -def test_router_exists(router_c2600): - - assert router_c2600.platform == "c2600" - assert router_c2600.list() - - -def test_chassis_2611(hypervisor): - - router = C2600(hypervisor, "2611 chassis", chassis="2611") - assert router.chassis == "2611" - assert isinstance(router.slots[0], router.integrated_adapters["2611"]) - router.delete() - - -def test_chassis_change_to_2611(router_c2600): - - assert router_c2600.chassis == "2610" # default chassis - router_c2600.chassis = "2611" - assert router_c2600.chassis == "2611" - - -def test_chassis_2620(hypervisor): - - router = C2600(hypervisor, "2620 chassis", chassis="2620") - assert router.chassis == "2620" - assert isinstance(router.slots[0], router.integrated_adapters["2620"]) - router.delete() - - -def test_chassis_change_to_2620(router_c2600): - - assert router_c2600.chassis == "2610" # default chassis - router_c2600.chassis = "2620" - assert router_c2600.chassis == "2620" - - -def test_chassis_2621(hypervisor): - - router = C2600(hypervisor, "2621 chassis", chassis="2621") - assert router.chassis == "2621" - assert isinstance(router.slots[0], router.integrated_adapters["2621"]) - router.delete() - - -def test_chassis_change_to_2621(router_c2600): - - assert router_c2600.chassis == "2610" # default chassis - router_c2600.chassis = "2621" - assert router_c2600.chassis == "2621" - - -def test_chassis_2610XM(hypervisor): - - router = C2600(hypervisor, "2610XM chassis", chassis="2610XM") - assert router.chassis == "2610XM" - assert isinstance(router.slots[0], router.integrated_adapters["2610XM"]) - router.delete() - - -def test_chassis_change_to_2610XM(router_c2600): - - assert router_c2600.chassis == "2610" # default chassis - router_c2600.chassis = "2610XM" - assert router_c2600.chassis == "2610XM" - - -def test_chassis_2611XM(hypervisor): - - router = C2600(hypervisor, "2611XM chassis", chassis="2611XM") - assert router.chassis == "2611XM" - assert isinstance(router.slots[0], router.integrated_adapters["2611XM"]) - router.delete() - - -def test_chassis_change_to_2611XM(router_c2600): - - assert router_c2600.chassis == "2610" # default chassis - router_c2600.chassis = "2611XM" - assert router_c2600.chassis == "2611XM" - - -def test_chassis_2620XM(hypervisor): - - router = C2600(hypervisor, "2620XM chassis", chassis="2620XM") - assert router.chassis == "2620XM" - assert isinstance(router.slots[0], router.integrated_adapters["2620XM"]) - router.delete() - - -def test_chassis_change_to_2620XM(router_c2600): - - assert router_c2600.chassis == "2610" # default chassis - router_c2600.chassis = "2620XM" - assert router_c2600.chassis == "2620XM" - - -def test_chassis_2621XM(hypervisor): - - router = C2600(hypervisor, "2621XM chassis", chassis="2621XM") - assert router.chassis == "2621XM" - assert isinstance(router.slots[0], router.integrated_adapters["2621XM"]) - router.delete() - - -def test_chassis_change_to_2621XM(router_c2600): - - assert router_c2600.chassis == "2610" # default chassis - router_c2600.chassis = "2621XM" - assert router_c2600.chassis == "2621XM" - - -def test_chassis_2650XM(hypervisor): - - router = C2600(hypervisor, "2650XM chassis", chassis="2650XM") - assert router.chassis == "2650XM" - assert isinstance(router.slots[0], router.integrated_adapters["2650XM"]) - router.delete() - - -def test_chassis_change_to_2650XM(router_c2600): - - assert router_c2600.chassis == "2610" # default chassis - router_c2600.chassis = "2650XM" - assert router_c2600.chassis == "2650XM" - - -def test_chassis_2651XM(hypervisor): - - router = C2600(hypervisor, "2651XM chassis", chassis="2651XM") - assert router.chassis == "2651XM" - assert isinstance(router.slots[0], router.integrated_adapters["2651XM"]) - router.delete() - - -def test_chassis_change_to_2651XM(router_c2600): - - assert router_c2600.chassis == "2610" # default chassis - router_c2600.chassis = "2651XM" - assert router_c2600.chassis == "2651XM" - - -def test_iomem(router_c2600): - - assert router_c2600.iomem == 15 # default value - router_c2600.iomem = 20 - assert router_c2600.iomem == 20 - - -def test_mac_addr(router_c2600): - - assert router_c2600.mac_addr is not None - router_c2600.mac_addr = "aa:aa:aa:aa:aa:aa" - assert router_c2600.mac_addr == "aa:aa:aa:aa:aa:aa" - - -def test_bogus_mac_addr(router_c2600): - - with pytest.raises(DynamipsError): - router_c2600.mac_addr = "zz:zz:zz:zz:zz:zz" - - -def test_system_id(router_c2600): - - assert router_c2600.system_id == "FTX0945W0MY" # default value - router_c2600.system_id = "FTX0945W0MO" - assert router_c2600.system_id == "FTX0945W0MO" - - -def test_get_hardware_info(router_c2600): - - router_c2600.get_hardware_info() # FIXME: Dynamips doesn't return anything - - -def test_slot_add_NM_1E(router_c2600): - - adapter = NM_1E() - router_c2600.slot_add_binding(1, adapter) - assert router_c2600.slots[1] == adapter - - -def test_slot_add_NM_4E(router_c2600): - - adapter = NM_4E() - router_c2600.slot_add_binding(1, adapter) - assert router_c2600.slots[1] == adapter - - -def test_slot_add_NM_1FE_TX(router_c2600): - - adapter = NM_1FE_TX() - router_c2600.slot_add_binding(1, adapter) - assert router_c2600.slots[1] == adapter - - -def test_slot_add_NM_16ESW(router_c2600): - - adapter = NM_16ESW() - router_c2600.slot_add_binding(1, adapter) - assert router_c2600.slots[1] == adapter diff --git a/old_tests/dynamips/test_c2691.py b/old_tests/dynamips/test_c2691.py deleted file mode 100644 index 282b183b..00000000 --- a/old_tests/dynamips/test_c2691.py +++ /dev/null @@ -1,73 +0,0 @@ -from gns3server.modules.dynamips import C2691 -from gns3server.modules.dynamips import DynamipsError -from gns3server.modules.dynamips import NM_1FE_TX -from gns3server.modules.dynamips import NM_4T -from gns3server.modules.dynamips import NM_16ESW -import pytest - - -@pytest.fixture -def router_c2691(request, hypervisor): - - router = C2691(hypervisor, "c2691 router") - request.addfinalizer(router.delete) - return router - - -def test_router_exists(router_c2691): - - assert router_c2691.platform == "c2691" - assert router_c2691.list() - - -def test_iomem(router_c2691): - - assert router_c2691.iomem == 5 # default value - router_c2691.iomem = 10 - assert router_c2691.iomem == 10 - - -def test_mac_addr(router_c2691): - - assert router_c2691.mac_addr is not None - router_c2691.mac_addr = "aa:aa:aa:aa:aa:aa" - assert router_c2691.mac_addr == "aa:aa:aa:aa:aa:aa" - - -def test_bogus_mac_addr(router_c2691): - - with pytest.raises(DynamipsError): - router_c2691.mac_addr = "zz:zz:zz:zz:zz:zz" - - -def test_system_id(router_c2691): - - assert router_c2691.system_id == "FTX0945W0MY" # default value - router_c2691.system_id = "FTX0945W0MO" - assert router_c2691.system_id == "FTX0945W0MO" - - -def test_get_hardware_info(router_c2691): - - router_c2691.get_hardware_info() # FIXME: Dynamips doesn't return anything - - -def test_slot_add_NM_1FE_TX(router_c2691): - - adapter = NM_1FE_TX() - router_c2691.slot_add_binding(1, adapter) - assert router_c2691.slots[1] == adapter - - -def test_slot_add_NM_4T(router_c2691): - - adapter = NM_4T() - router_c2691.slot_add_binding(1, adapter) - assert router_c2691.slots[1] == adapter - - -def test_slot_add_NM_16ESW(router_c2691): - - adapter = NM_16ESW() - router_c2691.slot_add_binding(1, adapter) - assert router_c2691.slots[1] == adapter diff --git a/old_tests/dynamips/test_c3600.py b/old_tests/dynamips/test_c3600.py deleted file mode 100644 index cd05add3..00000000 --- a/old_tests/dynamips/test_c3600.py +++ /dev/null @@ -1,118 +0,0 @@ -from gns3server.modules.dynamips import C3600 -from gns3server.modules.dynamips import DynamipsError -from gns3server.modules.dynamips import NM_1E -from gns3server.modules.dynamips import NM_4E -from gns3server.modules.dynamips import NM_1FE_TX -from gns3server.modules.dynamips import NM_16ESW -from gns3server.modules.dynamips import NM_4T -import pytest - - -@pytest.fixture -def router_c3600(request, hypervisor): - - router = C3600(hypervisor, "c3600 router") - request.addfinalizer(router.delete) - return router - - -def test_router_exist(router_c3600): - - assert router_c3600.platform == "c3600" - assert router_c3600.list() - - -def test_chassis_3620(hypervisor): - - router = C3600(hypervisor, "3620 chassis", chassis="3620") - assert router.chassis == "3620" - router.delete() - - -def test_chassis_change_to_3620(router_c3600): - - assert router_c3600.chassis == "3640" # default chassis - router_c3600.chassis = "3620" - assert router_c3600.chassis == "3620" - - -def test_chassis_3660(hypervisor): - - router = C3600(hypervisor, "3660 chassis", chassis="3660") - assert router.chassis == "3660" - assert str(router.slots[0]) == "Leopard-2FE" - router.delete() - - -def test_chassis_change_to_3660(router_c3600): - - assert router_c3600.chassis == "3640" # default chassis - router_c3600.chassis = "3660" - assert router_c3600.chassis == "3660" - - -def test_iomem(router_c3600): - - assert router_c3600.iomem == 5 # default value - router_c3600.iomem = 10 - assert router_c3600.iomem == 10 - - -def test_mac_addr(router_c3600): - - assert router_c3600.mac_addr is not None - router_c3600.mac_addr = "aa:aa:aa:aa:aa:aa" - assert router_c3600.mac_addr == "aa:aa:aa:aa:aa:aa" - - -def test_bogus_mac_addr(router_c3600): - - with pytest.raises(DynamipsError): - router_c3600.mac_addr = "zz:zz:zz:zz:zz:zz" - - -def test_system_id(router_c3600): - - assert router_c3600.system_id == "FTX0945W0MY" # default value - router_c3600.system_id = "FTX0945W0MO" - assert router_c3600.system_id == "FTX0945W0MO" - - -def test_get_hardware_info(router_c3600): - - router_c3600.get_hardware_info() # FIXME: Dynamips doesn't return anything - - -def test_slot_add_NM_1E(router_c3600): - - adapter = NM_1E() - router_c3600.slot_add_binding(1, adapter) - assert router_c3600.slots[1] == adapter - - -def test_slot_add_NM_4E(router_c3600): - - adapter = NM_4E() - router_c3600.slot_add_binding(1, adapter) - assert router_c3600.slots[1] == adapter - - -def test_slot_add_NM_1FE_TX(router_c3600): - - adapter = NM_1FE_TX() - router_c3600.slot_add_binding(1, adapter) - assert router_c3600.slots[1] == adapter - - -def test_slot_add_NM_16ESW(router_c3600): - - adapter = NM_16ESW() - router_c3600.slot_add_binding(1, adapter) - assert router_c3600.slots[1] == adapter - - -def test_slot_add_NM_4T(router_c3600): - - adapter = NM_4T() - router_c3600.slot_add_binding(1, adapter) - assert router_c3600.slots[1] == adapter diff --git a/old_tests/dynamips/test_c3725.py b/old_tests/dynamips/test_c3725.py deleted file mode 100644 index bc3ffcbf..00000000 --- a/old_tests/dynamips/test_c3725.py +++ /dev/null @@ -1,73 +0,0 @@ -from gns3server.modules.dynamips import C3725 -from gns3server.modules.dynamips import DynamipsError -from gns3server.modules.dynamips import NM_1FE_TX -from gns3server.modules.dynamips import NM_4T -from gns3server.modules.dynamips import NM_16ESW -import pytest - - -@pytest.fixture -def router_c3725(request, hypervisor): - - router = C3725(hypervisor, "c3725 router") - request.addfinalizer(router.delete) - return router - - -def test_router_exists(router_c3725): - - assert router_c3725.platform == "c3725" - assert router_c3725.list() - - -def test_iomem(router_c3725): - - assert router_c3725.iomem == 5 # default value - router_c3725.iomem = 10 - assert router_c3725.iomem == 10 - - -def test_mac_addr(router_c3725): - - assert router_c3725.mac_addr is not None - router_c3725.mac_addr = "aa:aa:aa:aa:aa:aa" - assert router_c3725.mac_addr == "aa:aa:aa:aa:aa:aa" - - -def test_bogus_mac_addr(router_c3725): - - with pytest.raises(DynamipsError): - router_c3725.mac_addr = "zz:zz:zz:zz:zz:zz" - - -def test_system_id(router_c3725): - - assert router_c3725.system_id == "FTX0945W0MY" # default value - router_c3725.system_id = "FTX0945W0MO" - assert router_c3725.system_id == "FTX0945W0MO" - - -def test_get_hardware_info(router_c3725): - - router_c3725.get_hardware_info() # FIXME: Dynamips doesn't return anything - - -def test_slot_add_NM_1FE_TX(router_c3725): - - adapter = NM_1FE_TX() - router_c3725.slot_add_binding(1, adapter) - assert router_c3725.slots[1] == adapter - - -def test_slot_add_NM_4T(router_c3725): - - adapter = NM_4T() - router_c3725.slot_add_binding(1, adapter) - assert router_c3725.slots[1] == adapter - - -def test_slot_add_NM_16ESW(router_c3725): - - adapter = NM_16ESW() - router_c3725.slot_add_binding(1, adapter) - assert router_c3725.slots[1] == adapter diff --git a/old_tests/dynamips/test_c3745.py b/old_tests/dynamips/test_c3745.py deleted file mode 100644 index 13d88583..00000000 --- a/old_tests/dynamips/test_c3745.py +++ /dev/null @@ -1,73 +0,0 @@ -from gns3server.modules.dynamips import C3745 -from gns3server.modules.dynamips import DynamipsError -from gns3server.modules.dynamips import NM_1FE_TX -from gns3server.modules.dynamips import NM_4T -from gns3server.modules.dynamips import NM_16ESW -import pytest - - -@pytest.fixture -def router_c3745(request, hypervisor): - - router = C3745(hypervisor, "c3745 router") - request.addfinalizer(router.delete) - return router - - -def test_router_exists(router_c3745): - - assert router_c3745.platform == "c3745" - assert router_c3745.list() - - -def test_iomem(router_c3745): - - assert router_c3745.iomem == 5 # default value - router_c3745.iomem = 10 - assert router_c3745.iomem == 10 - - -def test_mac_addr(router_c3745): - - assert router_c3745.mac_addr is not None - router_c3745.mac_addr = "aa:aa:aa:aa:aa:aa" - assert router_c3745.mac_addr == "aa:aa:aa:aa:aa:aa" - - -def test_bogus_mac_addr(router_c3745): - - with pytest.raises(DynamipsError): - router_c3745.mac_addr = "zz:zz:zz:zz:zz:zz" - - -def test_system_id(router_c3745): - - assert router_c3745.system_id == "FTX0945W0MY" # default value - router_c3745.system_id = "FTX0945W0MO" - assert router_c3745.system_id == "FTX0945W0MO" - - -def test_get_hardware_info(router_c3745): - - router_c3745.get_hardware_info() # FIXME: Dynamips doesn't return anything - - -def test_slot_add_NM_1FE_TX(router_c3745): - - adapter = NM_1FE_TX() - router_c3745.slot_add_binding(1, adapter) - assert router_c3745.slots[1] == adapter - - -def test_slot_add_NM_4T(router_c3745): - - adapter = NM_4T() - router_c3745.slot_add_binding(1, adapter) - assert router_c3745.slots[1] == adapter - - -def test_slot_add_NM_16ESW(router_c3745): - - adapter = NM_16ESW() - router_c3745.slot_add_binding(1, adapter) - assert router_c3745.slots[1] == adapter diff --git a/old_tests/dynamips/test_c7200.py b/old_tests/dynamips/test_c7200.py deleted file mode 100644 index 48f1eb00..00000000 --- a/old_tests/dynamips/test_c7200.py +++ /dev/null @@ -1,188 +0,0 @@ -from gns3server.modules.dynamips import C7200 -from gns3server.modules.dynamips import DynamipsError -from gns3server.modules.dynamips import PA_2FE_TX -from gns3server.modules.dynamips import PA_4E -from gns3server.modules.dynamips import PA_4T -from gns3server.modules.dynamips import PA_8E -from gns3server.modules.dynamips import PA_8T -from gns3server.modules.dynamips import PA_A1 -from gns3server.modules.dynamips import PA_FE_TX -from gns3server.modules.dynamips import PA_GE -from gns3server.modules.dynamips import PA_POS_OC3 -from gns3server.modules.dynamips import NIO_Null -import pytest - - -@pytest.fixture -def router_c7200(request, hypervisor): - - router = C7200(hypervisor, "c7200 router") - request.addfinalizer(router.delete) - return router - - -def test_router_exists(router_c7200): - - assert router_c7200.platform == "c7200" - assert router_c7200.list() - - -def test_npe(router_c7200): - - assert router_c7200.npe == "npe-400" # default value - router_c7200.npe = "npe-200" - assert router_c7200.npe == "npe-200" - - -def test_midplane(router_c7200): - - assert router_c7200.midplane == "vxr" # default value - router_c7200.midplane = "std" - assert router_c7200.midplane == "std" - - -def test_sensors(router_c7200): - - assert router_c7200.sensors == [22, 22, 22, 22] # default values (everything at 22C) - router_c7200.sensors = [25, 25, 25, 25] - assert router_c7200.sensors == [25, 25, 25, 25] - - -def test_power_supplies(router_c7200): - - assert router_c7200.power_supplies == [1, 1] # default values (1 = powered on) - router_c7200.power_supplies = [0, 0] - assert router_c7200.power_supplies == [0, 0] - - -def test_mac_addr(router_c7200): - - assert router_c7200.mac_addr is not None - router_c7200.mac_addr = "aa:aa:aa:aa:aa:aa" - assert router_c7200.mac_addr == "aa:aa:aa:aa:aa:aa" - - -def test_bogus_mac_addr(router_c7200): - - with pytest.raises(DynamipsError): - router_c7200.mac_addr = "zz:zz:zz:zz:zz:zz" - - -def test_system_id(router_c7200): - - assert router_c7200.system_id == "FTX0945W0MY" # default value - router_c7200.system_id = "FTX0945W0MO" - assert router_c7200.system_id == "FTX0945W0MO" - - -def test_get_hardware_info(router_c7200): - - router_c7200.get_hardware_info() # FIXME: Dynamips doesn't return anything - - -def test_slot_add_PA_2FE_TX(router_c7200): - - adapter = PA_2FE_TX() - router_c7200.slot_add_binding(1, adapter) - assert router_c7200.slots[1] == adapter - - -def test_slot_add_PA_4E(router_c7200): - - adapter = PA_4E() - router_c7200.slot_add_binding(2, adapter) - assert router_c7200.slots[2] == adapter - - -def test_slot_add_PA_4T(router_c7200): - - adapter = PA_4T() - router_c7200.slot_add_binding(3, adapter) - assert router_c7200.slots[3] == adapter - - -def test_slot_add_PA_8E(router_c7200): - - adapter = PA_8E() - router_c7200.slot_add_binding(4, adapter) - assert router_c7200.slots[4] == adapter - - -def test_slot_add_PA_8T(router_c7200): - - adapter = PA_8T() - router_c7200.slot_add_binding(5, adapter) - assert router_c7200.slots[5] == adapter - - -def test_slot_add_PA_A1(router_c7200): - - adapter = PA_A1() - router_c7200.slot_add_binding(1, adapter) - assert router_c7200.slots[1] == adapter - - -def test_slot_add_PA_FE_TX(router_c7200): - - adapter = PA_FE_TX() - router_c7200.slot_add_binding(2, adapter) - assert router_c7200.slots[2] == adapter - - -def test_slot_add_PA_GE(router_c7200): - - adapter = PA_GE() - router_c7200.slot_add_binding(3, adapter) - assert router_c7200.slots[3] == adapter - - -def test_slot_add_PA_POS_OC3(router_c7200): - - adapter = PA_POS_OC3() - router_c7200.slot_add_binding(4, adapter) - assert router_c7200.slots[4] == adapter - - -def test_slot_add_into_already_occupied_slot(router_c7200): - - adapter = PA_FE_TX() - with pytest.raises(DynamipsError): - router_c7200.slot_add_binding(0, adapter) - - -def test_slot_add_into_wrong_slot(router_c7200): - - adapter = PA_FE_TX() - with pytest.raises(DynamipsError): - router_c7200.slot_add_binding(10, adapter) - - -def test_slot_remove_adapter(router_c7200): - - adapter = PA_FE_TX() - router_c7200.slot_add_binding(1, adapter) - router_c7200.slot_remove_binding(1) - assert router_c7200.slots[1] is None - - -def test_slot_add_remove_nio_binding(router_c7200): - - adapter = PA_FE_TX() - router_c7200.slot_add_binding(1, adapter) - nio = NIO_Null(router_c7200.hypervisor) - router_c7200.slot_add_nio_binding(1, 0, nio) # slot 1/0 - assert router_c7200.get_slot_nio_bindings(slot_id=1) - assert router_c7200.slots[1].ports[0] == nio - router_c7200.slot_remove_nio_binding(1, 0) # slot 1/0 - assert not router_c7200.get_slot_nio_bindings(slot_id=0) - nio.delete() - - -def test_slot_add_nio_to_wrong_port(router_c7200): - - adapter = PA_FE_TX() - router_c7200.slot_add_binding(1, adapter) - nio = NIO_Null(router_c7200.hypervisor) - with pytest.raises(DynamipsError): - router_c7200.slot_add_nio_binding(1, 1, nio) # slot 1/1 - nio.delete() diff --git a/old_tests/dynamips/test_ethernet_switch.py b/old_tests/dynamips/test_ethernet_switch.py deleted file mode 100644 index 0f435f38..00000000 --- a/old_tests/dynamips/test_ethernet_switch.py +++ /dev/null @@ -1,87 +0,0 @@ -from gns3server.modules.dynamips import EthernetSwitch -from gns3server.modules.dynamips import NIO_Null -from gns3server.modules.dynamips import DynamipsError -import pytest - - -@pytest.fixture -def ethsw(request, hypervisor): - - ethsw = EthernetSwitch(hypervisor, "Ethernet switch") - request.addfinalizer(ethsw.delete) - return ethsw - - -def test_ethsw_exists(ethsw): - - assert ethsw.list() - - -def test_rename_ethsw(ethsw): - - ethsw.name = "new Ethernet switch" - assert ethsw.name == "new Ethernet switch" - - -def test_add_remove_nio(ethsw): - - nio = NIO_Null(ethsw.hypervisor) - ethsw.add_nio(nio, 0) # add NIO on port 0 - assert ethsw.nios - ethsw.remove_nio(0) # remove NIO from port 0 - nio.delete() - - -def test_add_nio_already_allocated_port(ethsw): - - nio = NIO_Null(ethsw.hypervisor) - ethsw.add_nio(nio, 0) # add NIO on port 0 - with pytest.raises(DynamipsError): - ethsw.add_nio(nio, 0) - nio.delete() - - -def test_remove_nio_non_allocated_port(ethsw): - - with pytest.raises(DynamipsError): - ethsw.remove_nio(0) # remove NIO from port 0 - - -def test_set_access_port(ethsw): - - nio = NIO_Null(ethsw.hypervisor) - ethsw.add_nio(nio, 0) # add NIO on port 0 - ethsw.set_access_port(0, 10) # set port 0 as access in VLAN 10 - assert ethsw.mapping[0] == ("access", 10) - ethsw.remove_nio(0) # remove NIO from port 0 - nio.delete() - - -def test_set_dot1q_port(ethsw): - - nio = NIO_Null(ethsw.hypervisor) - ethsw.add_nio(nio, 0) # add NIO on port 0 - ethsw.set_dot1q_port(0, 1) # set port 0 as 802.1Q trunk with native VLAN 1 - assert ethsw.mapping[0] == ("dot1q", 1) - ethsw.remove_nio(0) # remove NIO from port 0 - nio.delete() - - -def test_set_qinq_port(ethsw): - - nio = NIO_Null(ethsw.hypervisor) - ethsw.add_nio(nio, 0) # add NIO on port 0 - ethsw.set_qinq_port(0, 100) # set port 0 as QinQ trunk with outer VLAN 100 - assert ethsw.mapping[0] == ("qinq", 100) - ethsw.remove_nio(0) # remove NIO from port 0 - nio.delete() - - -def test_get_mac_addr_table(ethsw): - - assert not ethsw.get_mac_addr_table() # MAC address table should be empty - - -def test_clear_mac_addr_table(ethsw): - - ethsw.clear_mac_addr_table() diff --git a/old_tests/dynamips/test_frame_relay_switch.py b/old_tests/dynamips/test_frame_relay_switch.py deleted file mode 100644 index b6dde5eb..00000000 --- a/old_tests/dynamips/test_frame_relay_switch.py +++ /dev/null @@ -1,65 +0,0 @@ -from gns3server.modules.dynamips import FrameRelaySwitch -from gns3server.modules.dynamips import NIO_Null -from gns3server.modules.dynamips import DynamipsError -import pytest - - -@pytest.fixture -def frsw(request, hypervisor): - - frsw = FrameRelaySwitch(hypervisor, "Frane Relay switch") - request.addfinalizer(frsw.delete) - return frsw - - -def test_frsw_exists(frsw): - - assert frsw.list() - - -def test_rename_frsw(frsw): - - frsw.name = "new Frame Relay switch" - assert frsw.name == "new Frame Relay switch" - - -def test_add_remove_nio(frsw): - - nio = NIO_Null(frsw.hypervisor) - frsw.add_nio(nio, 0) # add NIO on port 0 - assert frsw.nios - frsw.remove_nio(0) # remove NIO from port 0 - nio.delete() - - -def test_add_nio_already_allocated_port(frsw): - - nio = NIO_Null(frsw.hypervisor) - frsw.add_nio(nio, 0) # add NIO on port 0 - with pytest.raises(DynamipsError): - frsw.add_nio(nio, 0) - nio.delete() - - -def test_remove_nio_non_allocated_port(frsw): - - with pytest.raises(DynamipsError): - frsw.remove_nio(0) # remove NIO from port 0 - - -def test_vc(frsw): - - nio1 = NIO_Null(frsw.hypervisor) - frsw.add_nio(nio1, 0) # add NIO on port 0 - nio2 = NIO_Null(frsw.hypervisor) - frsw.add_nio(nio1, 1) # add NIO on port 1 - frsw.map_vc(0, 10, 1, 20) # port 0 DLCI 10 to port 1 DLCI 20 (unidirectional) - frsw.map_vc(1, 20, 0, 10) # port 1 DLCI 20 to port 0 DLCI 10 (unidirectional) - assert frsw.mapping[(0, 10)] == (1, 20) - assert frsw.mapping[(1, 20)] == (0, 10) - frsw.unmap_vc(0, 10, 1, 20) # port 0 DLCI 10 to port 1 DLCI 20 (unidirectional) - frsw.unmap_vc(1, 20, 0, 10) # port 1 DLCI 20 to port 0 DLCI 10 (unidirectional) - frsw.remove_nio(0) - frsw.remove_nio(1) - nio1.delete() - nio2.delete() diff --git a/old_tests/dynamips/test_hub.py b/old_tests/dynamips/test_hub.py deleted file mode 100644 index d490cb11..00000000 --- a/old_tests/dynamips/test_hub.py +++ /dev/null @@ -1,25 +0,0 @@ -from gns3server.modules.dynamips import Hub -from gns3server.modules.dynamips import NIO_Null -import pytest - - -@pytest.fixture -def hub(request, hypervisor): - - hub = Hub(hypervisor, "hub") - request.addfinalizer(hub.delete) - return hub - - -def test_hub_exists(hub): - - assert hub.list() - - -def test_add_remove_nio(hub): - - nio = NIO_Null(hub.hypervisor) - hub.add_nio(nio, 0) # add NIO to port 0 - assert hub.mapping[0] == nio - hub.remove_nio(0) # remove NIO from port 0 - nio.delete() diff --git a/old_tests/dynamips/test_hypervisor.py b/old_tests/dynamips/test_hypervisor.py deleted file mode 100644 index 81a8176e..00000000 --- a/old_tests/dynamips/test_hypervisor.py +++ /dev/null @@ -1,41 +0,0 @@ -from gns3server.modules.dynamips import Hypervisor -import time - - -def test_is_started(hypervisor): - - assert hypervisor.is_running() - - -def test_port(hypervisor): - - assert hypervisor.port == 7200 - - -def test_host(hypervisor): - - assert hypervisor.host == "0.0.0.0" - - -def test_working_dir(hypervisor): - - assert hypervisor.working_dir == "/tmp" - - -def test_path(hypervisor): - - dynamips_path = '/usr/bin/dynamips' - assert hypervisor.path == dynamips_path - - -def test_stdout(): - - # try to launch Dynamips on the same port - # this will fail so that we can read its stdout/stderr - dynamips_path = '/usr/bin/dynamips' - hypervisor = Hypervisor(dynamips_path, "/tmp", "127.0.0.1", 7200) - hypervisor.start() - # give some time for Dynamips to start - time.sleep(0.1) - output = hypervisor.read_stdout() - assert output diff --git a/old_tests/dynamips/test_hypervisor_manager.py b/old_tests/dynamips/test_hypervisor_manager.py deleted file mode 100644 index c7e42734..00000000 --- a/old_tests/dynamips/test_hypervisor_manager.py +++ /dev/null @@ -1,52 +0,0 @@ -from gns3server.modules.dynamips import Router -from gns3server.modules.dynamips import HypervisorManager -import pytest -import os - - -@pytest.fixture(scope="module") -def hypervisor_manager(request): - - dynamips_path = '/usr/bin/dynamips' - print("\nStarting Dynamips Hypervisor: {}".format(dynamips_path)) - manager = HypervisorManager(dynamips_path, "/tmp", "127.0.0.1") - - # manager.start_new_hypervisor() - - def stop(): - print("\nStopping Dynamips Hypervisor") - manager.stop_all_hypervisors() - - request.addfinalizer(stop) - return manager - - -def test_allocate_hypervisor_for_router(hypervisor_manager): - - hypervisor_manager.allocate_hypervisor_per_device = False - # default of 1GB of RAM per hypervisor instance - assert hypervisor_manager.memory_usage_limit_per_hypervisor == 1024 - hypervisor = hypervisor_manager.allocate_hypervisor_for_router("c3725.image", 512) - assert hypervisor.is_running() - hypervisor = hypervisor_manager.allocate_hypervisor_for_router("c3725.image", 256) - assert hypervisor.memory_load == 768 - hypervisor = hypervisor_manager.allocate_hypervisor_for_router("c3725.image", 512) - assert hypervisor.memory_load == 512 - assert len(hypervisor_manager.hypervisors) == 2 - - -def test_unallocate_hypervisor_for_router(hypervisor_manager): - - assert len(hypervisor_manager.hypervisors) == 2 - hypervisor = hypervisor_manager.hypervisors[0] - assert hypervisor.memory_load == 768 - router = Router(hypervisor, "router", "c3725") # default is 128MB of RAM - hypervisor_manager.unallocate_hypervisor_for_router(router) - assert hypervisor.memory_load == 640 - hypervisor.decrease_memory_load(512) # forces memory load down to 128 - assert hypervisor.memory_load == 128 - router.delete() - hypervisor_manager.unallocate_hypervisor_for_router(router) - # router is deleted and memory load to 0 now, one hypervisor must - # have been shutdown - assert len(hypervisor_manager.hypervisors) == 1 diff --git a/old_tests/dynamips/test_nios.py b/old_tests/dynamips/test_nios.py deleted file mode 100644 index 691dac49..00000000 --- a/old_tests/dynamips/test_nios.py +++ /dev/null @@ -1,139 +0,0 @@ -from gns3server.modules.dynamips import NIO_UDP -from gns3server.modules.dynamips import NIO_UDP_auto -from gns3server.modules.dynamips import NIO_FIFO -from gns3server.modules.dynamips import NIO_Mcast -from gns3server.modules.dynamips import NIO_Null -from gns3server.modules.dynamips import DynamipsError -import pytest - -# TODO: test UNIX, TAP, VDE, generic Ethernet and Linux Ethernet NIOs - - -def test_nio_udp(hypervisor): - - nio1 = NIO_UDP(hypervisor, 10001, "127.0.0.1", 10002) - assert nio1.lport == 10001 - nio2 = NIO_UDP(hypervisor, 10002, "127.0.0.1", 10001) - assert nio2.lport == 10002 - nio1.delete() - nio2.delete() - - -def test_nio_udp_auto(hypervisor): - - nio1 = NIO_UDP_auto(hypervisor, "127.0.0.1", 10001, 10010) - assert nio1.lport == 10001 - nio2 = NIO_UDP_auto(hypervisor, "127.0.0.1", 10001, 10010) - assert nio2.lport == 10002 - nio1.connect("127.0.0.1", nio2.lport) - nio2.connect("127.0.0.1", nio1.lport) - nio1.delete() - nio2.delete() - - -def test_nio_fifo(hypervisor): - - nio1 = NIO_FIFO(hypervisor) - nio2 = NIO_FIFO(hypervisor) - nio1.crossconnect(nio2) - assert nio1.list() - nio1.delete() - nio2.delete() - - -def test_nio_mcast(hypervisor): - - nio1 = NIO_Mcast(hypervisor, "232.0.0.1", 10001) - assert nio1.group == "232.0.0.1" - assert nio1.port == 10001 - nio1.ttl = 254 - assert nio1.ttl == 254 - nio2 = NIO_UDP(hypervisor, 10002, "232.0.0.1", 10001) - nio1.delete() - nio2.delete() - - -def test_nio_null(hypervisor): - - nio = NIO_Null(hypervisor) - assert nio.list() - nio.delete() - - -def test_rename_nio(hypervisor): - - nio = NIO_Null(hypervisor) - assert nio.name.startswith("nio_null") - nio.rename("test") - assert nio.name == "test" - nio.delete() - - -def test_debug_nio(hypervisor): - - nio = NIO_Null(hypervisor) - nio.debug(1) - nio.debug(0) - nio.delete() - - -def test_bind_unbind_filter(hypervisor): - - nio = NIO_Null(hypervisor) - nio.bind_filter("both", "freq_drop") - assert nio.input_filter == ("freq_drop", None) - assert nio.output_filter == ("freq_drop", None) - nio.unbind_filter("both") - nio.bind_filter("in", "capture") - assert nio.input_filter == ("capture", None) - nio.unbind_filter("in") - nio.delete() - - -def test_bind_unknown_filter(hypervisor): - - nio = NIO_Null(hypervisor) - with pytest.raises(DynamipsError): - nio.bind_filter("both", "my_filter") - nio.delete() - - -def test_unbind_with_no_filter_applied(hypervisor): - - nio = NIO_Null(hypervisor) - with pytest.raises(DynamipsError): - nio.unbind_filter("out") - nio.delete() - - -def test_setup_filter(hypervisor): - - nio = NIO_Null(hypervisor) - nio.bind_filter("in", "freq_drop") - nio.setup_filter("in", "5") # drop every 5th packet - assert nio.input_filter == ("freq_drop", "5") - nio.unbind_filter("in") - nio.delete() - - -def test_get_stats(hypervisor): - - nio = NIO_Null(hypervisor) - assert nio.get_stats() == "0 0 0 0" # nothing has been transmitted or received - nio.delete() - - -def test_reset_stats(hypervisor): - - nio = NIO_Null(hypervisor) - nio.reset_stats() - nio.delete() - - -def test_set_bandwidth(hypervisor): - - nio = NIO_Null(hypervisor) - assert nio.bandwidth is None # no constraint by default - nio.set_bandwidth(1000) # bandwidth = 1000 Kb/s - assert nio.bandwidth == 1000 - nio.delete() diff --git a/old_tests/dynamips/test_router.py b/old_tests/dynamips/test_router.py deleted file mode 100644 index 4b0fd3db..00000000 --- a/old_tests/dynamips/test_router.py +++ /dev/null @@ -1,232 +0,0 @@ -from gns3server.modules.dynamips import Router -from gns3server.modules.dynamips import DynamipsError -import sys -import pytest -import tempfile -import base64 - - -@pytest.fixture -def router(request, hypervisor): - - router = Router(hypervisor, "router", platform="c3725") - request.addfinalizer(router.delete) - return router - - -def test_hypervisor_is_started(hypervisor): - - assert hypervisor.is_running() - - -def test_create_and_delete_router(hypervisor): - - router = Router(hypervisor, "test my router") - assert router.id >= 0 - assert router.name == "test my router" - assert router.platform == "c7200" # default platform - assert not router.is_running() - router.delete() - with pytest.raises(DynamipsError): - router.get_status() - - -def test_rename_router(hypervisor): - - router = Router(hypervisor, "my router to rename") - assert router.name == "my router to rename" - router.name = "my_router" - assert router.name == "my_router" - router.delete() - - -def test_image(router): - - # let's pretend this file is an IOS image - with tempfile.NamedTemporaryFile() as ios_image: - router.image = ios_image.name - assert router.image == ios_image.name - - -def test_set_config(router): - - with tempfile.NamedTemporaryFile() as startup_config: - startup_config.write(b"hostname test_config\n") - router.set_config(startup_config.name) - - -def test_push_config(router): - - startup_config = base64.b64encode(b"hostname test_config\n").decode("utf-8") - private_config = base64.b64encode(b"private config\n").decode("utf-8") - router.push_config(startup_config, private_config) - router_startup_config, router_private_config = router.extract_config() - assert startup_config == router_startup_config - assert private_config == router_private_config - - -def test_status(router, image): - # don't test if we have no IOS image - if not image: - return - - assert router.get_status() == "inactive" - router.ram = 256 - router.image = image - router.start() - assert router.is_running() - router.suspend() - assert router.get_status() == "suspended" - router.resume() - assert router.is_running() - router.stop() - assert router.get_status() == "inactive" - - -def test_ram(router): - - assert router.ram == 128 # default ram - router.ram = 256 - assert router.ram == 256 - - -def test_nvram(router): - - assert router.nvram == 128 # default nvram - router.nvram = 256 - assert router.nvram == 256 - - -def test_mmap(router): - - assert router.mmap # default value - router.mmap = False - assert router.mmap == False - - -def test_sparsemem(router): - - assert router.sparsemem # default value - router.sparsemem = False - assert router.sparsemem == False - - -def test_clock_divisor(router): - - assert router.clock_divisor == 8 # default value - router.clock_divisor = 4 - assert router.clock_divisor == 4 - - -def test_idlepc(router): - - assert router.idlepc == "" # no default value - router.idlepc = "0x60c086a8" - assert router.idlepc == "0x60c086a8" - - -def test_idlemax(router): - - assert router.idlemax == 500 # default value - router.idlemax = 1500 - assert router.idlemax == 1500 - - -def test_idlesleep(router): - - assert router.idlesleep == 30 # default value - router.idlesleep = 15 - assert router.idlesleep == 15 - - -def test_exec_area(router): - - if sys.platform.startswith("win"): - assert router.exec_area == 16 # default value - else: - assert router.exec_area == 64 # default value - router.exec_area = 48 - assert router.exec_area == 48 - - -def test_disk0(router): - - assert router.disk0 == 0 # default value - router.disk0 = 16 - assert router.disk0 == 16 - - -def test_disk1(router): - - assert router.disk1 == 0 # default value - router.disk1 = 16 - assert router.disk1 == 16 - - -def test_confreg(router): - - assert router.confreg == "0x2102" # default value - router.confreg = "0x2142" - assert router.confreg == "0x2142" - - -def test_console(router): - - assert router.console == 2001 - new_console_port = router.console + 100 - router.console = new_console_port - assert router.console == new_console_port - - -def test_aux(router): - - assert router.aux == 2501 - new_aux_port = router.aux + 100 - router.aux = new_aux_port - assert router.aux == new_aux_port - - -def test_cpu_info(router): - - router.get_cpu_info() # nothing is returned by the hypervisor, cannot test? - - -def test_cpu_usage(router): - - usage = router.get_cpu_usage() - assert usage == 0 # router isn't running, so usage must be 0 - - -def test_get_slot_bindings(router): - - assert router.get_slot_bindings()[0] == "0/0: GT96100-FE" - - -def test_get_slot_nio_bindings(router): - - router.get_slot_nio_bindings(slot_id=0) - - -def test_mac_addr(router): - - assert router.mac_addr is not None - router.mac_addr = "aa:aa:aa:aa:aa:aa" - assert router.mac_addr == "aa:aa:aa:aa:aa:aa" - - -def test_bogus_mac_addr(router): - - with pytest.raises(DynamipsError): - router.mac_addr = "zz:zz:zz:zz:zz:zz" - - -def test_system_id(router): - - assert router.system_id == "FTX0945W0MY" # default value - router.system_id = "FTX0945W0MO" - assert router.system_id == "FTX0945W0MO" - - -def test_get_hardware_info(router): - - router.get_hardware_info() diff --git a/old_tests/dynamips/test_vmhandler.py b/old_tests/dynamips/test_vmhandler.py deleted file mode 100644 index e639d59f..00000000 --- a/old_tests/dynamips/test_vmhandler.py +++ /dev/null @@ -1,65 +0,0 @@ -from tornado.testing import AsyncHTTPTestCase -#from gns3server.plugins.dynamips import Dynamips -#from gns3server._compat import urlencode -from functools import partial -import tornado.web -import json -import tempfile - - -# class TestVMHandler(AsyncHTTPTestCase): -# -# def setUp(self): -# -# AsyncHTTPTestCase.setUp(self) -# self.post_request = partial(self.http_client.fetch, -# self.get_url("/api/vms/dynamips"), -# self.stop, -# method="POST") -# -# def get_app(self): -# return tornado.web.Application(Dynamips().handlers()) -# -# def test_endpoint(self): -# self.http_client.fetch(self.get_url("/api/vms/dynamips"), self.stop) -# response = self.wait() -# assert response.code == 200 -# -# def test_upload(self): -# -# try: -# from poster.encode import multipart_encode -# except ImportError: -# # poster isn't available for Python 3, let's just ignore the test -# return -# -# file_to_upload = tempfile.NamedTemporaryFile() -# data, headers = multipart_encode({"file1": file_to_upload}) -# body = "" -# for d in data: -# body += d -# -# response = self.fetch('/api/vms/dynamips/storage/upload', -# headers=headers, -# body=body, -# method='POST') -# -# assert response.code == 200 -# -# def get_new_ioloop(self): -# return tornado.ioloop.IOLoop.instance() -# -# def test_create_vm(self): -# -# post_data = {"name": "R1", -# "platform": "c3725", -# "console": 2000, -# "aux": 3000, -# "image": "c3725.bin", -# "ram": 128} -# -# self.post_request(body=json.dumps(post_data)) -# response = self.wait() -# assert(response.headers['Content-Type'].startswith('application/json')) -# expected = {"success": True} -# assert response.body.decode("utf-8") == json.dumps(expected) diff --git a/old_tests/test_jsonrpc.py b/old_tests/test_jsonrpc.py deleted file mode 100644 index eb6920a6..00000000 --- a/old_tests/test_jsonrpc.py +++ /dev/null @@ -1,92 +0,0 @@ -import uuid -from tornado.testing import AsyncTestCase -from tornado.escape import json_encode, json_decode -from ws4py.client.tornadoclient import TornadoWebSocketClient -import gns3server.jsonrpc as jsonrpc - -""" -Tests for JSON-RPC protocol over Websockets -""" - - -class JSONRPC(AsyncTestCase): - - URL = "ws://127.0.0.1:8000/" - - def test_request(self): - - params = {"echo": "test"} - request = jsonrpc.JSONRPCRequest("dynamips.echo", params) - AsyncWSRequest(self.URL, self.io_loop, self.stop, str(request)) - response = self.wait() - json_response = json_decode(response) - assert json_response["jsonrpc"] == 2.0 - assert json_response["id"] == request.id - assert json_response["result"] == params - - def test_request_with_invalid_method(self): - - message = {"echo": "test"} - request = jsonrpc.JSONRPCRequest("dynamips.non_existent", message) - AsyncWSRequest(self.URL, self.io_loop, self.stop, str(request)) - response = self.wait() - json_response = json_decode(response) - assert json_response["error"].get("code") == -32601 - assert json_response["id"] == request.id - - def test_request_with_invalid_version(self): - - request = {"jsonrpc": 1.0, "method": "dynamips.echo", "id": 1} - AsyncWSRequest(self.URL, self.io_loop, self.stop, json_encode(request)) - response = self.wait() - json_response = json_decode(response) - assert json_response["id"] is None - assert json_response["error"].get("code") == -32600 - - def test_request_with_invalid_json(self): - - request = "my non JSON request" - AsyncWSRequest(self.URL, self.io_loop, self.stop, request) - response = self.wait() - json_response = json_decode(response) - assert json_response["id"] is None - assert json_response["error"].get("code") == -32700 - - def test_request_with_invalid_jsonrpc_field(self): - - request = {"jsonrpc": "2.0", "method_bogus": "dynamips.echo", "id": 1} - AsyncWSRequest(self.URL, self.io_loop, self.stop, json_encode(request)) - response = self.wait() - json_response = json_decode(response) - assert json_response["id"] is None - assert json_response["error"].get("code") == -32700 - - def test_request_with_no_params(self): - - request = jsonrpc.JSONRPCRequest("dynamips.echo") - AsyncWSRequest(self.URL, self.io_loop, self.stop, str(request)) - response = self.wait() - json_response = json_decode(response) - assert json_response["id"] == request.id - assert json_response["error"].get("code") == -32602 - - -class AsyncWSRequest(TornadoWebSocketClient): - - """ - Very basic Websocket client for tests - """ - - def __init__(self, url, io_loop, callback, message): - TornadoWebSocketClient.__init__(self, url, io_loop=io_loop) - self._callback = callback - self._message = message - self.connect() - - def opened(self): - self.send(self._message, binary=False) - - def received_message(self, message): - self.close() - if self._callback: - self._callback(message.data) diff --git a/tests/api/test_dynamips.py b/tests/api/test_dynamips.py new file mode 100644 index 00000000..3f18c6ad --- /dev/null +++ b/tests/api/test_dynamips.py @@ -0,0 +1,125 @@ +# -*- 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 pytest +from tests.utils import asyncio_patch + + +# @pytest.yield_fixture(scope="module") +# def vm(server, project): +# +# dynamips_path = "/fake/dynamips" +# with asyncio_patch("gns3server.modules.dynamips.nodes.router.Router.create", return_value=True) as mock: +# response = server.post("/projects/{project_id}/dynamips/vms".format(project_id=project.id), {"name": "My router", +# "platform": "c3745", +# "image": "somewhere", +# "ram": 128}) +# assert mock.called +# assert response.status == 201 +# +# with asyncio_patch("gns3server.modules.dynamips.Dynamips.find_dynamips", return_value=dynamips_path): +# yield response.json +# +# +# def test_dynamips_vm_create(server, project): +# +# with asyncio_patch("gns3server.modules.dynamips.nodes.router.Router.create", return_value=True): +# response = server.post("/projects/{project_id}/dynamips/vms".format(project_id=project.id), {"name": "My router", +# "platform": "c3745", +# "image": "somewhere", +# "ram": 128}, +# example=True) +# assert response.status == 201 +# assert response.json["name"] == "My router" +# assert response.json["project_id"] == project.id +# assert response.json["dynamips_id"] +# +# +# def test_dynamips_vm_get(server, project, vm): +# response = server.get("/projects/{project_id}/dynamips/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) +# assert response.status == 200 +# assert response.route == "/projects/{project_id}/dynamips/vms/{vm_id}" +# assert response.json["name"] == "My router" +# assert response.json["project_id"] == project.id +# +# +# def test_dynamips_vm_start(server, vm): +# with asyncio_patch("gns3server.modules.dynamips.nodes.router.Router.start", return_value=True) as mock: +# response = server.post("/projects/{project_id}/dynamips/vms/{vm_id}/start".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) +# assert mock.called +# assert response.status == 204 +# +# +# def test_dynamips_vm_stop(server, vm): +# with asyncio_patch("gns3server.modules.dynamips.nodes.router.Router.stop", return_value=True) as mock: +# response = server.post("/projects/{project_id}/dynamips/vms/{vm_id}/stop".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) +# assert mock.called +# assert response.status == 204 +# +# +# def test_dynamips_vm_suspend(server, vm): +# with asyncio_patch("gns3server.modules.dynamips.nodes.router.Router.suspend", return_value=True) as mock: +# response = server.post("/projects/{project_id}/dynamips/vms/{vm_id}/suspend".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) +# assert mock.called +# assert response.status == 204 +# +# +# def test_dynamips_vm_resume(server, vm): +# with asyncio_patch("gns3server.modules.dynamips.nodes.router.Router.resume", return_value=True) as mock: +# response = server.post("/projects/{project_id}/dynamips/vms/{vm_id}/resume".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) +# assert mock.called +# assert response.status == 204 + + +# def test_vbox_nio_create_udp(server, vm): +# +# with asyncio_patch('gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.adapter_add_nio_binding') as mock: +# response = server.post("/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/0/nio".format(project_id=vm["project_id"], +# vm_id=vm["vm_id"]), {"type": "nio_udp", +# "lport": 4242, +# "rport": 4343, +# "rhost": "127.0.0.1"}, +# example=True) +# +# assert mock.called +# args, kwgars = mock.call_args +# assert args[0] == 0 +# +# assert response.status == 201 +# assert response.route == "/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/nio" +# assert response.json["type"] == "nio_udp" +# +# +# def test_vbox_delete_nio(server, vm): +# +# with asyncio_patch('gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.adapter_remove_nio_binding') as mock: +# response = server.delete("/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) +# +# assert mock.called +# args, kwgars = mock.call_args +# assert args[0] == 0 +# +# assert response.status == 204 +# assert response.route == "/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/nio" +# +# +# def test_vbox_update(server, vm, free_console_port): +# response = server.put("/projects/{project_id}/virtualbox/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"name": "test", +# "console": free_console_port}) +# assert response.status == 200 +# assert response.json["name"] == "test" +# assert response.json["console"] == free_console_port diff --git a/tests/modules/dynamips/test_dynamips_manager.py b/tests/modules/dynamips/test_dynamips_manager.py new file mode 100644 index 00000000..e0ddf9e1 --- /dev/null +++ b/tests/modules/dynamips/test_dynamips_manager.py @@ -0,0 +1,44 @@ +# -*- 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 pytest +import tempfile + +from gns3server.modules.dynamips import Dynamips +from gns3server.modules.dynamips.dynamips_error import DynamipsError +from unittest.mock import patch + + +@pytest.fixture(scope="module") +def manager(port_manager): + m = Dynamips.instance() + m.port_manager = port_manager + return m + + +def test_vm_invalid_dynamips_path(manager): + with patch("gns3server.config.Config.get_section_config", return_value={"dynamips_path": "/bin/test_fake"}): + with pytest.raises(DynamipsError): + manager.find_dynamips() + + +def test_vm_non_executable_dynamips_path(manager): + tmpfile = tempfile.NamedTemporaryFile() + with patch("gns3server.config.Config.get_section_config", return_value={"dynamips_path": tmpfile.name}): + with pytest.raises(DynamipsError): + manager.find_dynamips() diff --git a/tests/modules/dynamips/test_dynamips_router.py b/tests/modules/dynamips/test_dynamips_router.py new file mode 100644 index 00000000..c7fbb236 --- /dev/null +++ b/tests/modules/dynamips/test_dynamips_router.py @@ -0,0 +1,51 @@ +# -*- 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 pytest +import asyncio + +from unittest.mock import patch +from gns3server.modules.dynamips.nodes.router import Router +from gns3server.modules.dynamips.dynamips_error import DynamipsError +from gns3server.modules.dynamips import Dynamips + + +@pytest.fixture(scope="module") +def manager(port_manager): + m = Dynamips.instance() + m.port_manager = port_manager + return m + + +@pytest.fixture(scope="function") +def router(project, manager): + return Router("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + + +def test_router(project, manager): + router = Router("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + assert router.name == "test" + assert router.id == "00010203-0405-0607-0809-0a0b0c0d0e0f" + + +def test_router_invalid_dynamips_path(project, manager, loop): + with patch("gns3server.config.Config.get_section_config", return_value={"dynamips_path": "/bin/test_fake"}): + with pytest.raises(DynamipsError): + router = Router("test", "00010203-0405-0607-0809-0a0b0c0d0e0e", project, manager) + loop.run_until_complete(asyncio.async(router.create())) + assert router.name == "test" + assert router.id == "00010203-0405-0607-0809-0a0b0c0d0e0e" diff --git a/tests/modules/virtualbox/test_virtualbox_manager.py b/tests/modules/virtualbox/test_virtualbox_manager.py index fde3ca5b..1dc647d6 100644 --- a/tests/modules/virtualbox/test_virtualbox_manager.py +++ b/tests/modules/virtualbox/test_virtualbox_manager.py @@ -31,13 +31,13 @@ def manager(port_manager): return m -@patch("gns3server.config.Config.get_section_config", return_value={"vboxmanage_path": "/bin/test_fake"}) -def test_vm_invalid_vboxmanage_path(project, manager): - with pytest.raises(VirtualBoxError): - manager.find_vboxmanage() +def test_vm_invalid_vboxmanage_path(manager): + with patch("gns3server.config.Config.get_section_config", return_value={"vboxmanage_path": "/bin/test_fake"}): + with pytest.raises(VirtualBoxError): + manager.find_vboxmanage() -def test_vm_non_executable_vboxmanage_path(project, manager): +def test_vm_non_executable_vboxmanage_path(manager): tmpfile = tempfile.NamedTemporaryFile() with patch("gns3server.config.Config.get_section_config", return_value={"vboxmanage_path": tmpfile.name}): with pytest.raises(VirtualBoxError): From 2e99ef69a9e550cba6bceaa15a5ba48c336a9853 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 11 Feb 2015 14:31:21 +0100 Subject: [PATCH 220/485] Modules support start iou process (not ioucon and iouyap) --- gns3server/modules/__init__.py | 3 +- .../modules/adapters/ethernet_adapter.py | 2 +- gns3server/modules/adapters/serial_adapter.py | 32 ++ gns3server/modules/dynamips/dynamips_error.py | 16 +- gns3server/modules/iou/__init__.py | 64 +++ gns3server/modules/iou/iou_error.py | 26 + gns3server/modules/iou/iou_vm.py | 460 ++++++++++++++++++ .../modules/virtualbox/virtualbox_error.py | 16 +- gns3server/modules/vm_error.py | 17 +- gns3server/modules/vpcs/vpcs_error.py | 16 +- tests/modules/iou/test_iou_manager.py | 73 +++ tests/modules/iou/test_iou_vm.py | 164 +++++++ 12 files changed, 841 insertions(+), 48 deletions(-) create mode 100644 gns3server/modules/adapters/serial_adapter.py create mode 100644 gns3server/modules/iou/__init__.py create mode 100644 gns3server/modules/iou/iou_error.py create mode 100644 gns3server/modules/iou/iou_vm.py create mode 100644 tests/modules/iou/test_iou_manager.py create mode 100644 tests/modules/iou/test_iou_vm.py diff --git a/gns3server/modules/__init__.py b/gns3server/modules/__init__.py index 4e2f51bb..25c1012f 100644 --- a/gns3server/modules/__init__.py +++ b/gns3server/modules/__init__.py @@ -18,5 +18,6 @@ from .vpcs import VPCS from .virtualbox import VirtualBox from .dynamips import Dynamips +from .iou import IOU -MODULES = [VPCS, VirtualBox, Dynamips] +MODULES = [VPCS, VirtualBox, Dynamips, IOU] diff --git a/gns3server/modules/adapters/ethernet_adapter.py b/gns3server/modules/adapters/ethernet_adapter.py index 9d3ee003..f1a06c63 100644 --- a/gns3server/modules/adapters/ethernet_adapter.py +++ b/gns3server/modules/adapters/ethernet_adapter.py @@ -29,4 +29,4 @@ class EthernetAdapter(Adapter): def __str__(self): - return "VPCS Ethernet adapter" + return "Ethernet adapter" diff --git a/gns3server/modules/adapters/serial_adapter.py b/gns3server/modules/adapters/serial_adapter.py new file mode 100644 index 00000000..5bb00dc1 --- /dev/null +++ b/gns3server/modules/adapters/serial_adapter.py @@ -0,0 +1,32 @@ +# -*- 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 . + +from .adapter import Adapter + + +class SerialAdapter(Adapter): + + """ + VPCS Ethernet adapter. + """ + + def __init__(self): + Adapter.__init__(self, interfaces=1) + + def __str__(self): + + return "Serial adapter" diff --git a/gns3server/modules/dynamips/dynamips_error.py b/gns3server/modules/dynamips/dynamips_error.py index 58c306ee..15e20796 100644 --- a/gns3server/modules/dynamips/dynamips_error.py +++ b/gns3server/modules/dynamips/dynamips_error.py @@ -22,18 +22,4 @@ Custom exceptions for Dynamips module. class DynamipsError(Exception): - def __init__(self, message, original_exception=None): - - Exception.__init__(self, message) - if isinstance(message, Exception): - message = str(message) - self._message = message - self._original_exception = original_exception - - def __repr__(self): - - return self._message - - def __str__(self): - - return self._message + pass diff --git a/gns3server/modules/iou/__init__.py b/gns3server/modules/iou/__init__.py new file mode 100644 index 00000000..c3ba15b4 --- /dev/null +++ b/gns3server/modules/iou/__init__.py @@ -0,0 +1,64 @@ +# -*- 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 . + +""" +IOU server module. +""" + +import asyncio + +from ..base_manager import BaseManager +from .iou_error import IOUError +from .iou_vm import IOUVM + + +class IOU(BaseManager): + _VM_CLASS = IOUVM + + def __init__(self): + super().__init__() + self._free_application_ids = list(range(1, 512)) + self._used_application_ids = {} + + @asyncio.coroutine + def create_vm(self, *args, **kwargs): + + vm = yield from super().create_vm(*args, **kwargs) + try: + self._used_application_ids[vm.id] = self._free_application_ids.pop(0) + except IndexError: + raise IOUError("No mac address available") + return vm + + @asyncio.coroutine + def delete_vm(self, vm_id, *args, **kwargs): + + vm = self.get_vm(vm_id) + i = self._used_application_ids[vm_id] + self._free_application_ids.insert(0, i) + del self._used_application_ids[vm_id] + yield from super().delete_vm(vm_id, *args, **kwargs) + + def get_application_id(self, vm_id): + """ + Get an unique IOU mac id + + :param vm_id: ID of the IOU VM + :returns: IOU MAC id + """ + + return self._used_application_ids.get(vm_id, 1) diff --git a/gns3server/modules/iou/iou_error.py b/gns3server/modules/iou/iou_error.py new file mode 100644 index 00000000..cd43bdb9 --- /dev/null +++ b/gns3server/modules/iou/iou_error.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2013 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Custom exceptions for IOU module. +""" + +from ..vm_error import VMError + + +class IOUError(VMError): + pass diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py new file mode 100644 index 00000000..5abf0b44 --- /dev/null +++ b/gns3server/modules/iou/iou_vm.py @@ -0,0 +1,460 @@ +# -*- 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 . + +""" +IOU VM management (creates command line, processes, files etc.) in +order to run an IOU instance. +""" + +import os +import sys +import subprocess +import signal +import re +import asyncio +import shutil + +from pkg_resources import parse_version +from .iou_error import IOUError +from ..adapters.ethernet_adapter import EthernetAdapter +from ..adapters.serial_adapter import SerialAdapter +from ..base_vm import BaseVM + + +import logging +log = logging.getLogger(__name__) + + +class IOUVM(BaseVM): + module_name = 'iou' + + """ + IOU vm implementation. + + :param name: name of this IOU vm + :param vm_id: IOU instance identifier + :param project: Project instance + :param manager: parent VM Manager + :param console: TCP console port + """ + + def __init__(self, name, vm_id, project, manager, console=None): + + super().__init__(name, vm_id, project, manager) + + self._console = console + self._command = [] + self._iouyap_process = None + self._iou_process = None + self._iou_stdout_file = "" + self._started = False + self._iou_path = None + self._iourc = None + self._ioucon_thread = None + + # IOU settings + self._ethernet_adapters = [EthernetAdapter(), EthernetAdapter()] # one adapter = 4 interfaces + self._serial_adapters = [SerialAdapter(), SerialAdapter()] # one adapter = 4 interfaces + self._slots = self._ethernet_adapters + self._serial_adapters + self._use_default_iou_values = True # for RAM & NVRAM values + self._nvram = 128 # Kilobytes + self._initial_config = "" + self._ram = 256 # Megabytes + self._l1_keepalives = False # used to overcome the always-up Ethernet interfaces (not supported by all IOSes). + + if self._console is not None: + self._console = self._manager.port_manager.reserve_console_port(self._console) + else: + self._console = self._manager.port_manager.get_free_console_port() + + def close(self): + + if self._console: + self._manager.port_manager.release_console_port(self._console) + self._console = None + + @property + def iou_path(self): + """Path of the iou binary""" + + return self._iou_path + + @iou_path.setter + def iou_path(self, path): + """ + Path of the iou binary + + :params path: Path to the binary + """ + + self._iou_path = path + if not os.path.isfile(self._iou_path) or not os.path.exists(self._iou_path): + if os.path.islink(self._iou_path): + raise IOUError("IOU image '{}' linked to '{}' is not accessible".format(self._iou_path, os.path.realpath(self._iou_path))) + else: + raise IOUError("IOU image '{}' is not accessible".format(self._iou_path)) + + try: + with open(self._iou_path, "rb") as f: + # read the first 7 bytes of the file. + elf_header_start = f.read(7) + except OSError as e: + raise IOUError("Cannot read ELF header for IOU image '{}': {}".format(self._iou_path, e)) + + # IOU images must start with the ELF magic number, be 32-bit, little endian + # and have an ELF version of 1 normal IOS image are big endian! + if elf_header_start != b'\x7fELF\x01\x01\x01': + raise IOUError("'{}' is not a valid IOU image".format(self._iou_path)) + + if not os.access(self._iou_path, os.X_OK): + raise IOUError("IOU image '{}' is not executable".format(self._iou_path)) + + @property + def iourc(self): + """ + Returns the path to the iourc file. + :returns: path to the iourc file + """ + + return self._iourc + + @property + def use_default_iou_values(self): + """ + Returns if this device uses the default IOU image values. + :returns: boolean + """ + + return self._use_default_iou_values + + @use_default_iou_values.setter + def use_default_iou_values(self, state): + """ + Sets if this device uses the default IOU image values. + :param state: boolean + """ + + self._use_default_iou_values = state + if state: + log.info("IOU {name} [id={id}]: uses the default IOU image values".format(name=self._name, id=self._id)) + else: + log.info("IOU {name} [id={id}]: does not use the default IOU image values".format(name=self._name, id=self._id)) + + @iourc.setter + def iourc(self, iourc): + """ + Sets the path to the iourc file. + :param iourc: path to the iourc file. + """ + + self._iourc = iourc + log.info("IOU {name} [id={id}]: iourc file path set to {path}".format(name=self._name, + id=self._id, + path=self._iourc)) + + def _check_requirements(self): + """ + Check if IOUYAP is available + """ + path = self.iouyap_path + if not path: + raise IOUError("No path to a IOU executable has been set") + + if not os.path.isfile(path): + raise IOUError("IOU program '{}' is not accessible".format(path)) + + if not os.access(path, os.X_OK): + raise IOUError("IOU program '{}' is not executable".format(path)) + + def __json__(self): + + return {"name": self.name, + "vm_id": self.id, + "console": self._console, + "project_id": self.project.id, + } + + @property + def iouyap_path(self): + """ + Returns the IOUYAP executable path. + + :returns: path to IOUYAP + """ + + path = self._manager.config.get_section_config("IOU").get("iouyap_path", "iouyap") + if path == "iouyap": + path = shutil.which("iouyap") + return path + + @property + def console(self): + """ + Returns the console port of this IOU vm. + + :returns: console port + """ + + return self._console + + @console.setter + def console(self, console): + """ + Change console port + + :params console: Console port (integer) + """ + + if console == self._console: + return + if self._console: + self._manager.port_manager.release_console_port(self._console) + self._console = self._manager.port_manager.reserve_console_port(console) + + @property + def application_id(self): + return self._manager.get_application_id(self.id) + + #TODO: ASYNCIO + def _library_check(self): + """ + Checks for missing shared library dependencies in the IOU image. + """ + + try: + output = subprocess.check_output(["ldd", self._iou_path]) + except (FileNotFoundError, subprocess.SubprocessError) as e: + log.warn("could not determine the shared library dependencies for {}: {}".format(self._path, e)) + return + + p = re.compile("([\.\w]+)\s=>\s+not found") + missing_libs = p.findall(output.decode("utf-8")) + if missing_libs: + raise IOUError("The following shared library dependencies cannot be found for IOU image {}: {}".format(self._path, + ", ".join(missing_libs))) + + @asyncio.coroutine + def start(self): + """ + Starts the IOU process. + """ + + self._check_requirements() + if not self.is_running(): + + # TODO: ASYNC + #self._library_check() + + if not self._iourc or not os.path.isfile(self._iourc): + raise IOUError("A valid iourc file is necessary to start IOU") + + iouyap_path = self.iouyap_path + if not iouyap_path or not os.path.isfile(iouyap_path): + raise IOUError("iouyap is necessary to start IOU") + + self._create_netmap_config() + # created a environment variable pointing to the iourc file. + env = os.environ.copy() + env["IOURC"] = self._iourc + self._command = self._build_command() + try: + log.info("Starting IOU: {}".format(self._command)) + self._iou_stdout_file = os.path.join(self.working_dir, "iou.log") + log.info("Logging to {}".format(self._iou_stdout_file)) + with open(self._iou_stdout_file, "w") as fd: + self._iou_process = yield from asyncio.create_subprocess_exec(self._command, + stdout=fd, + stderr=subprocess.STDOUT, + cwd=self.working_dir, + env=env) + log.info("IOU instance {} started PID={}".format(self._id, self._iou_process.pid)) + self._started = True + except FileNotFoundError as e: + raise IOUError("could not start IOU: {}: 32-bit binary support is probably not installed".format(e)) + except (OSError, subprocess.SubprocessError) as e: + iou_stdout = self.read_iou_stdout() + log.error("could not start IOU {}: {}\n{}".format(self._iou_path, e, iou_stdout)) + raise IOUError("could not start IOU {}: {}\n{}".format(self._iou_path, e, iou_stdout)) + + # start console support + #self._start_ioucon() + # connections support + #self._start_iouyap() + + + @asyncio.coroutine + def stop(self): + """ + Stops the IOU process. + """ + + # stop console support + if self._ioucon_thread: + self._ioucon_thread_stop_event.set() + if self._ioucon_thread.is_alive(): + self._ioucon_thread.join(timeout=3.0) # wait for the thread to free the console port + self._ioucon_thread = None + + if self.is_running(): + self._terminate_process() + try: + yield from asyncio.wait_for(self._iou_process.wait(), timeout=3) + except asyncio.TimeoutError: + self._iou_process.kill() + if self._iou_process.returncode is None: + log.warn("IOU process {} is still running".format(self._iou_process.pid)) + + self._iou_process = None + self._started = False + + def _terminate_process(self): + """Terminate the process if running""" + + if self._iou_process: + log.info("Stopping IOU instance {} PID={}".format(self.name, self._iou_process.pid)) + try: + self._iou_process.terminate() + # Sometime the process can already be dead when we garbage collect + except ProcessLookupError: + pass + + @asyncio.coroutine + def reload(self): + """ + Reload the IOU process. (Stop / Start) + """ + + yield from self.stop() + yield from self.start() + + def is_running(self): + """ + Checks if the IOU process is running + + :returns: True or False + """ + + if self._iou_process: + return True + return False + + def _create_netmap_config(self): + """ + Creates the NETMAP file. + """ + + netmap_path = os.path.join(self.working_dir, "NETMAP") + try: + with open(netmap_path, "w") as f: + for bay in range(0, 16): + for unit in range(0, 4): + f.write("{iouyap_id}:{bay}/{unit}{iou_id:>5d}:{bay}/{unit}\n".format(iouyap_id=str(self.application_id + 512), + bay=bay, + unit=unit, + iou_id=self.application_id)) + log.info("IOU {name} [id={id}]: NETMAP file created".format(name=self._name, + id=self._id)) + except OSError as e: + raise IOUError("Could not create {}: {}".format(netmap_path, e)) + + def _build_command(self): + """ + Command to start the IOU process. + (to be passed to subprocess.Popen()) + IOU command line: + Usage: [options] + : unix-js-m | unix-is-m | unix-i-m | ... + : instance identifier (0 < id <= 1024) + Options: + -e Number of Ethernet interfaces (default 2) + -s Number of Serial interfaces (default 2) + -n Size of nvram in Kb (default 64KB) + -b IOS debug string + -c Configuration file name + -d Generate debug information + -t Netio message trace + -q Suppress informational messages + -h Display this help + -C Turn off use of host clock + -m Megabytes of router memory (default 256MB) + -L Disable local console, use remote console + -l Enable Layer 1 keepalive messages + -u UDP port base for distributed networks + -R Ignore options from the IOURC file + -U Disable unix: file system location + -W Disable watchdog timer + -N Ignore the NETMAP file + """ + + command = [self._iou_path] + if len(self._ethernet_adapters) != 2: + command.extend(["-e", str(len(self._ethernet_adapters))]) + if len(self._serial_adapters) != 2: + command.extend(["-s", str(len(self._serial_adapters))]) + if not self.use_default_iou_values: + command.extend(["-n", str(self._nvram)]) + command.extend(["-m", str(self._ram)]) + command.extend(["-L"]) # disable local console, use remote console + if self._initial_config: + command.extend(["-c", self._initial_config]) + if self._l1_keepalives: + self._enable_l1_keepalives(command) + command.extend([str(self.application_id)]) + return command + + def read_iou_stdout(self): + """ + Reads the standard output of the IOU process. + Only use when the process has been stopped or has crashed. + """ + + output = "" + if self._iou_stdout_file: + try: + with open(self._iou_stdout_file, errors="replace") as file: + output = file.read() + except OSError as e: + log.warn("could not read {}: {}".format(self._iou_stdout_file, e)) + return output + + def read_iouyap_stdout(self): + """ + Reads the standard output of the iouyap process. + Only use when the process has been stopped or has crashed. + """ + + output = "" + if self._iouyap_stdout_file: + try: + with open(self._iouyap_stdout_file, errors="replace") as file: + output = file.read() + except OSError as e: + log.warn("could not read {}: {}".format(self._iouyap_stdout_file, e)) + return output + + def _start_ioucon(self): + """ + Starts ioucon thread (for console connections). + """ + + if not self._ioucon_thread: + telnet_server = "{}:{}".format(self._console_host, self.console) + log.info("Starting ioucon for IOU instance {} to accept Telnet connections on {}".format(self._name, telnet_server)) + args = argparse.Namespace(appl_id=str(self.application_id), debug=False, escape='^^', telnet_limit=0, telnet_server=telnet_server) + self._ioucon_thread_stop_event = threading.Event() + self._ioucon_thread = threading.Thread(target=start_ioucon, args=(args, self._ioucon_thread_stop_event)) + self._ioucon_thread.start() diff --git a/gns3server/modules/virtualbox/virtualbox_error.py b/gns3server/modules/virtualbox/virtualbox_error.py index ec05bfb6..df481c21 100644 --- a/gns3server/modules/virtualbox/virtualbox_error.py +++ b/gns3server/modules/virtualbox/virtualbox_error.py @@ -24,18 +24,4 @@ from ..vm_error import VMError class VirtualBoxError(VMError): - def __init__(self, message, original_exception=None): - - Exception.__init__(self, message) - if isinstance(message, Exception): - message = str(message) - self._message = message - self._original_exception = original_exception - - def __repr__(self): - - return self._message - - def __str__(self): - - return self._message + pass diff --git a/gns3server/modules/vm_error.py b/gns3server/modules/vm_error.py index d7b71e14..55cfc4cf 100644 --- a/gns3server/modules/vm_error.py +++ b/gns3server/modules/vm_error.py @@ -17,4 +17,19 @@ class VMError(Exception): - pass + + def __init__(self, message, original_exception=None): + + Exception.__init__(self, message) + if isinstance(message, Exception): + message = str(message) + self._message = message + self._original_exception = original_exception + + def __repr__(self): + + return self._message + + def __str__(self): + + return self._message diff --git a/gns3server/modules/vpcs/vpcs_error.py b/gns3server/modules/vpcs/vpcs_error.py index f32afdaa..b8e99d4b 100644 --- a/gns3server/modules/vpcs/vpcs_error.py +++ b/gns3server/modules/vpcs/vpcs_error.py @@ -24,18 +24,4 @@ from ..vm_error import VMError class VPCSError(VMError): - def __init__(self, message, original_exception=None): - - Exception.__init__(self, message) - if isinstance(message, Exception): - message = str(message) - self._message = message - self._original_exception = original_exception - - def __repr__(self): - - return self._message - - def __str__(self): - - return self._message + pass diff --git a/tests/modules/iou/test_iou_manager.py b/tests/modules/iou/test_iou_manager.py new file mode 100644 index 00000000..7817a297 --- /dev/null +++ b/tests/modules/iou/test_iou_manager.py @@ -0,0 +1,73 @@ +# -*- 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 pytest +import uuid + + +from gns3server.modules.iou import IOU +from gns3server.modules.iou.iou_error import IOUError +from gns3server.modules.project_manager import ProjectManager + + +def test_get_application_id(loop, project, port_manager): + # Cleanup the IOU object + IOU._instance = None + iou = IOU.instance() + iou.port_manager = port_manager + vm1_id = str(uuid.uuid4()) + vm2_id = str(uuid.uuid4()) + vm3_id = str(uuid.uuid4()) + loop.run_until_complete(iou.create_vm("PC 1", project.id, vm1_id)) + loop.run_until_complete(iou.create_vm("PC 2", project.id, vm2_id)) + assert iou.get_application_id(vm1_id) == 1 + assert iou.get_application_id(vm1_id) == 1 + assert iou.get_application_id(vm2_id) == 2 + loop.run_until_complete(iou.delete_vm(vm1_id)) + loop.run_until_complete(iou.create_vm("PC 3", project.id, vm3_id)) + assert iou.get_application_id(vm3_id) == 1 + + +def test_get_application_id_multiple_project(loop, port_manager): + # Cleanup the IOU object + IOU._instance = None + iou = IOU.instance() + iou.port_manager = port_manager + vm1_id = str(uuid.uuid4()) + vm2_id = str(uuid.uuid4()) + vm3_id = str(uuid.uuid4()) + project1 = ProjectManager.instance().create_project() + project2 = ProjectManager.instance().create_project() + loop.run_until_complete(iou.create_vm("PC 1", project1.id, vm1_id)) + loop.run_until_complete(iou.create_vm("PC 2", project1.id, vm2_id)) + loop.run_until_complete(iou.create_vm("PC 2", project2.id, vm3_id)) + assert iou.get_application_id(vm1_id) == 1 + assert iou.get_application_id(vm2_id) == 2 + assert iou.get_application_id(vm3_id) == 3 + + +def test_get_application_id_no_id_available(loop, project, port_manager): + # Cleanup the IOU object + IOU._instance = None + iou = IOU.instance() + iou.port_manager = port_manager + with pytest.raises(IOUError): + for i in range(1, 513): + vm_id = str(uuid.uuid4()) + loop.run_until_complete(iou.create_vm("PC {}".format(i), project.id, vm_id)) + assert iou.get_application_id(vm_id) == i diff --git a/tests/modules/iou/test_iou_vm.py b/tests/modules/iou/test_iou_vm.py new file mode 100644 index 00000000..5f5d4093 --- /dev/null +++ b/tests/modules/iou/test_iou_vm.py @@ -0,0 +1,164 @@ +# -*- 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 pytest +import aiohttp +import asyncio +import os +import stat +from tests.utils import asyncio_patch + + +from unittest.mock import patch, MagicMock +from gns3server.modules.iou.iou_vm import IOUVM +from gns3server.modules.iou.iou_error import IOUError +from gns3server.modules.iou import IOU + + +@pytest.fixture(scope="module") +def manager(port_manager): + m = IOU.instance() + m.port_manager = port_manager + return m + + +@pytest.fixture(scope="function") +def vm(project, manager, tmpdir, fake_iou_bin): + fake_file = str(tmpdir / "iourc") + with open(fake_file, "w+") as f: + f.write("1") + + vm = IOUVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + config = manager.config.get_section_config("IOU") + config["iouyap_path"] = fake_file + manager.config.set_section_config("IOU", config) + + vm.iou_path = fake_iou_bin + vm.iourc = fake_file + return vm + + +@pytest.fixture +def fake_iou_bin(tmpdir): + """Create a fake IOU image on disk""" + + path = str(tmpdir / "iou.bin") + with open(path, "w+") as f: + f.write('\x7fELF\x01\x01\x01') + os.chmod(path, stat.S_IREAD | stat.S_IEXEC) + return path + + +def test_vm(project, manager): + vm = IOUVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + assert vm.name == "test" + assert vm.id == "00010203-0405-0607-0809-0a0b0c0d0e0f" + + +@patch("gns3server.config.Config.get_section_config", return_value={"iouyap_path": "/bin/test_fake"}) +def test_vm_invalid_iouyap_path(project, manager, loop): + with pytest.raises(IOUError): + vm = IOUVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0e", project, manager) + loop.run_until_complete(asyncio.async(vm.start())) + + +def test_start(loop, vm): + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._check_requirements", return_value=True): + with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): + loop.run_until_complete(asyncio.async(vm.start())) + assert vm.is_running() + + +def test_stop(loop, vm): + process = MagicMock() + + # Wait process kill success + future = asyncio.Future() + future.set_result(True) + process.wait.return_value = future + + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._check_requirements", return_value=True): + with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): + loop.run_until_complete(asyncio.async(vm.start())) + assert vm.is_running() + loop.run_until_complete(asyncio.async(vm.stop())) + assert vm.is_running() is False + process.terminate.assert_called_with() + + +def test_reload(loop, vm, fake_iou_bin): + process = MagicMock() + + # Wait process kill success + future = asyncio.Future() + future.set_result(True) + process.wait.return_value = future + + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._check_requirements", return_value=True): + with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): + loop.run_until_complete(asyncio.async(vm.start())) + assert vm.is_running() + loop.run_until_complete(asyncio.async(vm.reload())) + assert vm.is_running() is True + process.terminate.assert_called_with() + + +def test_close(vm, port_manager): + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._check_requirements", return_value=True): + with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): + vm.start() + port = vm.console + vm.close() + # Raise an exception if the port is not free + port_manager.reserve_console_port(port) + assert vm.is_running() is False + + +def test_iou_path(vm, fake_iou_bin): + + vm.iou_path = fake_iou_bin + assert vm.iou_path == fake_iou_bin + + +def test_path_invalid_bin(vm, tmpdir): + + iou_path = str(tmpdir / "test.bin") + with pytest.raises(IOUError): + vm.iou_path = iou_path + + with open(iou_path, "w+") as f: + f.write("BUG") + + with pytest.raises(IOUError): + vm.iou_path = iou_path + + +def test_create_netmap_config(vm): + + vm._create_netmap_config() + netmap_path = os.path.join(vm.working_dir, "NETMAP") + + with open(netmap_path) as f: + content = f.read() + + assert "513:0/0 1:0/0" in content + assert "513:15/3 1:15/3" in content + + +def test_build_command(vm): + + assert vm._build_command() == [vm.iou_path, '-L', str(vm.application_id)] From 986c63f3446dae84802729996fc0e8ef006f18d5 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 11 Feb 2015 15:37:05 +0100 Subject: [PATCH 221/485] HTTP api start iou process Now we need to start ioucon --- gns3server/handlers/__init__.py | 3 +- gns3server/handlers/iou_handler.py | 180 +++++++++++++++++++++++++++++ gns3server/modules/iou/iou_vm.py | 54 ++++----- gns3server/schemas/iou.py | 127 ++++++++++++++++++++ tests/api/test_iou.py | 97 ++++++++++++++++ 5 files changed, 433 insertions(+), 28 deletions(-) create mode 100644 gns3server/handlers/iou_handler.py create mode 100644 gns3server/schemas/iou.py create mode 100644 tests/api/test_iou.py diff --git a/gns3server/handlers/__init__.py b/gns3server/handlers/__init__.py index 727b4053..78bb3ca6 100644 --- a/gns3server/handlers/__init__.py +++ b/gns3server/handlers/__init__.py @@ -3,4 +3,5 @@ __all__ = ["version_handler", "vpcs_handler", "project_handler", "virtualbox_handler", - "dynamips_handler"] + "dynamips_handler", + "iou_handler"] diff --git a/gns3server/handlers/iou_handler.py b/gns3server/handlers/iou_handler.py new file mode 100644 index 00000000..70ccc6b6 --- /dev/null +++ b/gns3server/handlers/iou_handler.py @@ -0,0 +1,180 @@ +# -*- 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 ..schemas.iou import IOU_CREATE_SCHEMA +from ..schemas.iou import IOU_UPDATE_SCHEMA +from ..schemas.iou import IOU_OBJECT_SCHEMA +from ..modules.iou import IOU + + +class IOUHandler: + + """ + API entry points for IOU. + """ + + @classmethod + @Route.post( + r"/projects/{project_id}/iou/vms", + parameters={ + "project_id": "UUID for the project" + }, + status_codes={ + 201: "Instance created", + 400: "Invalid request", + 409: "Conflict" + }, + description="Create a new IOU instance", + input=IOU_CREATE_SCHEMA, + output=IOU_OBJECT_SCHEMA) + def create(request, response): + + iou = IOU.instance() + vm = yield from iou.create_vm(request.json["name"], + request.match_info["project_id"], + request.json.get("vm_id"), + console=request.json.get("console"), + ) + vm.iou_path = request.json.get("iou_path", vm.iou_path) + vm.iourc_path = request.json.get("iourc_path", vm.iourc_path) + response.set_status(201) + response.json(vm) + + @classmethod + @Route.get( + r"/projects/{project_id}/iou/vms/{vm_id}", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 200: "Success", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Get a IOU instance", + output=IOU_OBJECT_SCHEMA) + def show(request, response): + + iou_manager = IOU.instance() + vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + response.json(vm) + + @classmethod + @Route.put( + r"/projects/{project_id}/iou/vms/{vm_id}", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 200: "Instance updated", + 400: "Invalid request", + 404: "Instance doesn't exist", + 409: "Conflict" + }, + description="Update a IOU instance", + input=IOU_UPDATE_SCHEMA, + output=IOU_OBJECT_SCHEMA) + def update(request, response): + + iou_manager = IOU.instance() + vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + vm.name = request.json.get("name", vm.name) + vm.console = request.json.get("console", vm.console) + vm.iou_path = request.json.get("iou_path", vm.iou_path) + vm.iourc_path = request.json.get("iourc_path", vm.iourc_path) + response.json(vm) + + @classmethod + @Route.delete( + r"/projects/{project_id}/iou/vms/{vm_id}", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 204: "Instance deleted", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Delete a IOU instance") + def delete(request, response): + + yield from IOU.instance().delete_vm(request.match_info["vm_id"]) + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/iou/vms/{vm_id}/start", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 204: "Instance started", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Start a IOU instance") + def start(request, response): + + iou_manager = IOU.instance() + vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + yield from vm.start() + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/iou/vms/{vm_id}/stop", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 204: "Instance stopped", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Stop a IOU instance") + def stop(request, response): + + iou_manager = IOU.instance() + vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + yield from vm.stop() + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/iou/vms/{vm_id}/reload", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance", + }, + status_codes={ + 204: "Instance reloaded", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Reload a IOU instance") + def reload(request, response): + + iou_manager = IOU.instance() + vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + yield from vm.reload() + response.set_status(204) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 5abf0b44..9651e210 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -63,7 +63,7 @@ class IOUVM(BaseVM): self._iou_stdout_file = "" self._started = False self._iou_path = None - self._iourc = None + self._iourc_path = None self._ioucon_thread = None # IOU settings @@ -124,13 +124,24 @@ class IOUVM(BaseVM): raise IOUError("IOU image '{}' is not executable".format(self._iou_path)) @property - def iourc(self): + def iourc_path(self): """ Returns the path to the iourc file. :returns: path to the iourc file """ - return self._iourc + return self._iourc_path + + @iourc_path.setter + def iourc_path(self, path): + """ + Set path to IOURC file + """ + + self._iourc_path = path + log.info("IOU {name} [id={id}]: iourc file path set to {path}".format(name=self._name, + id=self._id, + path=self._iourc_path)) @property def use_default_iou_values(self): @@ -154,18 +165,6 @@ class IOUVM(BaseVM): else: log.info("IOU {name} [id={id}]: does not use the default IOU image values".format(name=self._name, id=self._id)) - @iourc.setter - def iourc(self, iourc): - """ - Sets the path to the iourc file. - :param iourc: path to the iourc file. - """ - - self._iourc = iourc - log.info("IOU {name} [id={id}]: iourc file path set to {path}".format(name=self._name, - id=self._id, - path=self._iourc)) - def _check_requirements(self): """ Check if IOUYAP is available @@ -186,6 +185,8 @@ class IOUVM(BaseVM): "vm_id": self.id, "console": self._console, "project_id": self.project.id, + "iourc_path": self._iourc_path, + "iou_path": self.iou_path } @property @@ -229,7 +230,7 @@ class IOUVM(BaseVM): def application_id(self): return self._manager.get_application_id(self.id) - #TODO: ASYNCIO + # TODO: ASYNCIO def _library_check(self): """ Checks for missing shared library dependencies in the IOU image. @@ -257,9 +258,9 @@ class IOUVM(BaseVM): if not self.is_running(): # TODO: ASYNC - #self._library_check() + # self._library_check() - if not self._iourc or not os.path.isfile(self._iourc): + if not self._iourc_path or not os.path.isfile(self._iourc_path): raise IOUError("A valid iourc file is necessary to start IOU") iouyap_path = self.iouyap_path @@ -269,18 +270,18 @@ class IOUVM(BaseVM): self._create_netmap_config() # created a environment variable pointing to the iourc file. env = os.environ.copy() - env["IOURC"] = self._iourc + env["IOURC"] = self._iourc_path self._command = self._build_command() try: log.info("Starting IOU: {}".format(self._command)) self._iou_stdout_file = os.path.join(self.working_dir, "iou.log") log.info("Logging to {}".format(self._iou_stdout_file)) with open(self._iou_stdout_file, "w") as fd: - self._iou_process = yield from asyncio.create_subprocess_exec(self._command, - stdout=fd, - stderr=subprocess.STDOUT, - cwd=self.working_dir, - env=env) + self._iou_process = yield from asyncio.create_subprocess_exec(*self._command, + stdout=fd, + stderr=subprocess.STDOUT, + cwd=self.working_dir, + env=env) log.info("IOU instance {} started PID={}".format(self._id, self._iou_process.pid)) self._started = True except FileNotFoundError as e: @@ -291,10 +292,9 @@ class IOUVM(BaseVM): raise IOUError("could not start IOU {}: {}\n{}".format(self._iou_path, e, iou_stdout)) # start console support - #self._start_ioucon() + # self._start_ioucon() # connections support - #self._start_iouyap() - + # self._start_iouyap() @asyncio.coroutine def stop(self): diff --git a/gns3server/schemas/iou.py b/gns3server/schemas/iou.py new file mode 100644 index 00000000..562ac0f7 --- /dev/null +++ b/gns3server/schemas/iou.py @@ -0,0 +1,127 @@ +# -*- 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 . + + +IOU_CREATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to create a new IOU instance", + "type": "object", + "properties": { + "name": { + "description": "IOU VM name", + "type": "string", + "minLength": 1, + }, + "vm_id": { + "description": "IOU VM identifier", + "oneOf": [ + {"type": "string", + "minLength": 36, + "maxLength": 36, + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$"}, + {"type": "integer"} # for legacy projects + ] + }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": ["integer", "null"] + }, + "iou_path": { + "description": "Path of iou binary", + "type": "string" + }, + "iourc_path": { + "description": "Path of iourc", + "type": "string" + }, + }, + "additionalProperties": False, + "required": ["name", "iou_path", "iourc_path"] +} + +IOU_UPDATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to update a IOU instance", + "type": "object", + "properties": { + "name": { + "description": "IOU VM name", + "type": ["string", "null"], + "minLength": 1, + }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": ["integer", "null"] + }, + "iou_path": { + "description": "Path of iou binary", + "type": "string" + }, + "iourc_path": { + "description": "Path of iourc", + "type": "string" + }, + }, + "additionalProperties": False, +} + +IOU_OBJECT_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "IOU instance", + "type": "object", + "properties": { + "name": { + "description": "IOU VM name", + "type": "string", + "minLength": 1, + }, + "vm_id": { + "description": "IOU VM UUID", + "type": "string", + "minLength": 36, + "maxLength": 36, + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, + "project_id": { + "description": "Project UUID", + "type": "string", + "minLength": 36, + "maxLength": 36, + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + }, + "iou_path": { + "description": "Path of iou binary", + "type": "string" + }, + "iourc_path": { + "description": "Path of iourc", + "type": "string" + }, + }, + "additionalProperties": False, + "required": ["name", "vm_id", "console", "project_id", "iou_path", "iourc_path"] +} diff --git a/tests/api/test_iou.py b/tests/api/test_iou.py new file mode 100644 index 00000000..5dd3268c --- /dev/null +++ b/tests/api/test_iou.py @@ -0,0 +1,97 @@ +# -*- 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 pytest +import os +import stat +from tests.utils import asyncio_patch +from unittest.mock import patch + + +@pytest.fixture +def fake_iou_bin(tmpdir): + """Create a fake IOU image on disk""" + + path = str(tmpdir / "iou.bin") + with open(path, "w+") as f: + f.write('\x7fELF\x01\x01\x01') + os.chmod(path, stat.S_IREAD | stat.S_IEXEC) + return path + +@pytest.fixture +def base_params(tmpdir, fake_iou_bin): + """Return standard parameters""" + return {"name": "PC TEST 1", "iou_path": fake_iou_bin, "iourc_path": str(tmpdir / "iourc")} + + +@pytest.fixture +def vm(server, project, base_params): + response = server.post("/projects/{project_id}/iou/vms".format(project_id=project.id), base_params) + assert response.status == 201 + return response.json + + +def test_iou_create(server, project, base_params): + response = server.post("/projects/{project_id}/iou/vms".format(project_id=project.id), base_params, example=True) + assert response.status == 201 + assert response.route == "/projects/{project_id}/iou/vms" + assert response.json["name"] == "PC TEST 1" + assert response.json["project_id"] == project.id + + +def test_iou_get(server, project, vm): + response = server.get("/projects/{project_id}/iou/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) + assert response.status == 200 + assert response.route == "/projects/{project_id}/iou/vms/{vm_id}" + assert response.json["name"] == "PC TEST 1" + assert response.json["project_id"] == project.id + + +def test_iou_start(server, vm): + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM.start", return_value=True) as mock: + response = server.post("/projects/{project_id}/iou/vms/{vm_id}/start".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + assert mock.called + assert response.status == 204 + + +def test_iou_stop(server, vm): + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM.stop", return_value=True) as mock: + response = server.post("/projects/{project_id}/iou/vms/{vm_id}/stop".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + assert mock.called + assert response.status == 204 + + +def test_iou_reload(server, vm): + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM.reload", return_value=True) as mock: + response = server.post("/projects/{project_id}/iou/vms/{vm_id}/reload".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + assert mock.called + assert response.status == 204 + + +def test_iou_delete(server, vm): + with asyncio_patch("gns3server.modules.iou.IOU.delete_vm", return_value=True) as mock: + response = server.delete("/projects/{project_id}/iou/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + assert mock.called + assert response.status == 204 + + +def test_iou_update(server, vm, tmpdir, free_console_port): + response = server.put("/projects/{project_id}/iou/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"name": "test", + "console": free_console_port}) + assert response.status == 200 + assert response.json["name"] == "test" + assert response.json["console"] == free_console_port From faa7472670c62e5e314047025f4b2943dd854094 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 11 Feb 2015 15:57:02 +0100 Subject: [PATCH 222/485] IOUCON start when vm start --- gns3server/handlers/iou_handler.py | 8 ++++---- gns3server/modules/iou/iou_vm.py | 11 +++++++++-- gns3server/{old_modules => modules}/iou/ioucon.py | 4 ++-- tests/api/test_iou.py | 3 ++- 4 files changed, 17 insertions(+), 9 deletions(-) rename gns3server/{old_modules => modules}/iou/ioucon.py (99%) diff --git a/gns3server/handlers/iou_handler.py b/gns3server/handlers/iou_handler.py index 70ccc6b6..a606efff 100644 --- a/gns3server/handlers/iou_handler.py +++ b/gns3server/handlers/iou_handler.py @@ -46,10 +46,10 @@ class IOUHandler: iou = IOU.instance() vm = yield from iou.create_vm(request.json["name"], - request.match_info["project_id"], - request.json.get("vm_id"), - console=request.json.get("console"), - ) + request.match_info["project_id"], + request.json.get("vm_id"), + console=request.json.get("console"), + ) vm.iou_path = request.json.get("iou_path", vm.iou_path) vm.iourc_path = request.json.get("iourc_path", vm.iourc_path) response.set_status(201) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 9651e210..d173f973 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -27,12 +27,15 @@ import signal import re import asyncio import shutil +import argparse +import threading from pkg_resources import parse_version from .iou_error import IOUError from ..adapters.ethernet_adapter import EthernetAdapter from ..adapters.serial_adapter import SerialAdapter from ..base_vm import BaseVM +from .ioucon import start_ioucon import logging @@ -50,9 +53,12 @@ class IOUVM(BaseVM): :param project: Project instance :param manager: parent VM Manager :param console: TCP console port + :params console_host: TCP console host IP """ - def __init__(self, name, vm_id, project, manager, console=None): + def __init__(self, name, vm_id, project, manager, + console=None, + console_host="0.0.0.0"): super().__init__(name, vm_id, project, manager) @@ -65,6 +71,7 @@ class IOUVM(BaseVM): self._iou_path = None self._iourc_path = None self._ioucon_thread = None + self._console_host = console_host # IOU settings self._ethernet_adapters = [EthernetAdapter(), EthernetAdapter()] # one adapter = 4 interfaces @@ -292,7 +299,7 @@ class IOUVM(BaseVM): raise IOUError("could not start IOU {}: {}\n{}".format(self._iou_path, e, iou_stdout)) # start console support - # self._start_ioucon() + self._start_ioucon() # connections support # self._start_iouyap() diff --git a/gns3server/old_modules/iou/ioucon.py b/gns3server/modules/iou/ioucon.py similarity index 99% rename from gns3server/old_modules/iou/ioucon.py rename to gns3server/modules/iou/ioucon.py index 9a0e980e..6dbd782d 100644 --- a/gns3server/old_modules/iou/ioucon.py +++ b/gns3server/modules/iou/ioucon.py @@ -55,7 +55,7 @@ EXIT_ABORT = 2 # Mostly from: # https://code.google.com/p/miniboa/source/browse/trunk/miniboa/telnet.py -#--[ Telnet Commands ]--------------------------------------------------------- +# --[ Telnet Commands ]--------------------------------------------------------- SE = 240 # End of sub-negotiation parameters NOP = 241 # No operation DATMK = 242 # Data stream portion of a sync. @@ -74,7 +74,7 @@ DONT = 254 # Don't = Demand or confirm option halt IAC = 255 # Interpret as Command SEND = 1 # Sub-process negotiation SEND command IS = 0 # Sub-process negotiation IS command -#--[ Telnet Options ]---------------------------------------------------------- +# --[ Telnet Options ]---------------------------------------------------------- BINARY = 0 # Transmit Binary ECHO = 1 # Echo characters back to sender RECON = 2 # Reconnection diff --git a/tests/api/test_iou.py b/tests/api/test_iou.py index 5dd3268c..2e0a6fb7 100644 --- a/tests/api/test_iou.py +++ b/tests/api/test_iou.py @@ -32,6 +32,7 @@ def fake_iou_bin(tmpdir): os.chmod(path, stat.S_IREAD | stat.S_IEXEC) return path + @pytest.fixture def base_params(tmpdir, fake_iou_bin): """Return standard parameters""" @@ -91,7 +92,7 @@ def test_iou_delete(server, vm): def test_iou_update(server, vm, tmpdir, free_console_port): response = server.put("/projects/{project_id}/iou/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"name": "test", - "console": free_console_port}) + "console": free_console_port}) assert response.status == 200 assert response.json["name"] == "test" assert response.json["console"] == free_console_port From fb69c693f6668055242633e68bdd24041e5c19a0 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 11 Feb 2015 17:11:18 +0100 Subject: [PATCH 223/485] Start iouyap --- gns3server/modules/iou/iou_vm.py | 111 ++++++++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 3 deletions(-) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index d173f973..f243f418 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -29,6 +29,7 @@ import asyncio import shutil import argparse import threading +import configparser from pkg_resources import parse_version from .iou_error import IOUError @@ -301,7 +302,91 @@ class IOUVM(BaseVM): # start console support self._start_ioucon() # connections support - # self._start_iouyap() + self._start_iouyap() + + def _start_iouyap(self): + """ + Starts iouyap (handles connections to and from this IOU device). + """ + + try: + self._update_iouyap_config() + command = [self.iouyap_path, "-q", str(self.application_id + 512)] # iouyap has always IOU ID + 512 + log.info("starting iouyap: {}".format(command)) + self._iouyap_stdout_file = os.path.join(self.working_dir, "iouyap.log") + log.info("logging to {}".format(self._iouyap_stdout_file)) + with open(self._iouyap_stdout_file, "w") as fd: + self._iouyap_process = subprocess.Popen(command, + stdout=fd, + stderr=subprocess.STDOUT, + cwd=self.working_dir) + + log.info("iouyap started PID={}".format(self._iouyap_process.pid)) + except (OSError, subprocess.SubprocessError) as e: + iouyap_stdout = self.read_iouyap_stdout() + log.error("could not start iouyap: {}\n{}".format(e, iouyap_stdout)) + raise IOUError("Could not start iouyap: {}\n{}".format(e, iouyap_stdout)) + + def _update_iouyap_config(self): + """ + Updates the iouyap.ini file. + """ + + iouyap_ini = os.path.join(self.working_dir, "iouyap.ini") + + config = configparser.ConfigParser() + config["default"] = {"netmap": "NETMAP", + "base_port": "49000"} + + bay_id = 0 + for adapter in self._slots: + unit_id = 0 + for unit in adapter.ports.keys(): + nio = adapter.get_nio(unit) + if nio: + connection = None + if isinstance(nio, NIO_UDP): + # UDP tunnel + connection = {"tunnel_udp": "{lport}:{rhost}:{rport}".format(lport=nio.lport, + rhost=nio.rhost, + rport=nio.rport)} + elif isinstance(nio, NIO_TAP): + # TAP interface + connection = {"tap_dev": "{tap_device}".format(tap_device=nio.tap_device)} + + elif isinstance(nio, NIO_GenericEthernet): + # Ethernet interface + connection = {"eth_dev": "{ethernet_device}".format(ethernet_device=nio.ethernet_device)} + + if connection: + interface = "{iouyap_id}:{bay}/{unit}".format(iouyap_id=str(self.application_id + 512), bay=bay_id, unit=unit_id) + config[interface] = connection + + if nio.capturing: + pcap_data_link_type = nio.pcap_data_link_type.upper() + if pcap_data_link_type == "DLT_PPP_SERIAL": + pcap_protocol = "ppp" + elif pcap_data_link_type == "DLT_C_HDLC": + pcap_protocol = "hdlc" + elif pcap_data_link_type == "DLT_FRELAY": + pcap_protocol = "fr" + else: + pcap_protocol = "ethernet" + capture_info = {"pcap_file": "{pcap_file}".format(pcap_file=nio.pcap_output_file), + "pcap_protocol": pcap_protocol, + "pcap_overwrite": "y"} + config[interface].update(capture_info) + + unit_id += 1 + bay_id += 1 + + try: + with open(iouyap_ini, "w") as config_file: + config.write(config_file) + log.info("IOU {name} [id={id}]: iouyap.ini updated".format(name=self._name, + id=self._id)) + except OSError as e: + raise IOUError("Could not create {}: {}".format(iouyap_ini, e)) @asyncio.coroutine def stop(self): @@ -317,7 +402,7 @@ class IOUVM(BaseVM): self._ioucon_thread = None if self.is_running(): - self._terminate_process() + self._terminate_process_iou() try: yield from asyncio.wait_for(self._iou_process.wait(), timeout=3) except asyncio.TimeoutError: @@ -326,9 +411,29 @@ class IOUVM(BaseVM): log.warn("IOU process {} is still running".format(self._iou_process.pid)) self._iou_process = None + + self._terminate_process_iouyap() + try: + yield from asyncio.wait_for(self._iouyap_process.wait(), timeout=3) + except asyncio.TimeoutError: + self._iou_process.kill() + if self._iouyap_process.returncode is None: + log.warn("IOUYAP process {} is still running".format(self._iou_process.pid)) + self._started = False - def _terminate_process(self): + def _terminate_process_iouyap(self): + """Terminate the process if running""" + + if self._iou_process: + log.info("Stopping IOUYAP instance {} PID={}".format(self.name, self._iouyap_process.pid)) + try: + self._iouyap_process.terminate() + # Sometime the process can already be dead when we garbage collect + except ProcessLookupError: + pass + + def _terminate_process_iou(self): """Terminate the process if running""" if self._iou_process: From ebc214d6fa8d4cf7f5d13d799851f62b8787a426 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 12 Feb 2015 15:20:47 +0100 Subject: [PATCH 224/485] Fix tests and rename path to iou_path --- gns3server/handlers/iou_handler.py | 4 +-- gns3server/modules/iou/iou_vm.py | 55 ++++++++++++++-------------- gns3server/schemas/iou.py | 10 +++--- tests/api/test_iou.py | 2 +- tests/modules/iou/test_iou_vm.py | 58 ++++++++++++++++-------------- 5 files changed, 68 insertions(+), 61 deletions(-) diff --git a/gns3server/handlers/iou_handler.py b/gns3server/handlers/iou_handler.py index a606efff..ab39622f 100644 --- a/gns3server/handlers/iou_handler.py +++ b/gns3server/handlers/iou_handler.py @@ -50,7 +50,7 @@ class IOUHandler: request.json.get("vm_id"), console=request.json.get("console"), ) - vm.iou_path = request.json.get("iou_path", vm.iou_path) + vm.path = request.json.get("path", vm.path) vm.iourc_path = request.json.get("iourc_path", vm.iourc_path) response.set_status(201) response.json(vm) @@ -97,7 +97,7 @@ class IOUHandler: vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) vm.name = request.json.get("name", vm.name) vm.console = request.json.get("console", vm.console) - vm.iou_path = request.json.get("iou_path", vm.iou_path) + vm.path = request.json.get("path", vm.path) vm.iourc_path = request.json.get("iourc_path", vm.iourc_path) response.json(vm) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index f243f418..8beb127c 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -69,7 +69,7 @@ class IOUVM(BaseVM): self._iou_process = None self._iou_stdout_file = "" self._started = False - self._iou_path = None + self._path = None self._iourc_path = None self._ioucon_thread = None self._console_host = console_host @@ -96,40 +96,40 @@ class IOUVM(BaseVM): self._console = None @property - def iou_path(self): + def path(self): """Path of the iou binary""" - return self._iou_path + return self._path - @iou_path.setter - def iou_path(self, path): + @path.setter + def path(self, path): """ Path of the iou binary :params path: Path to the binary """ - self._iou_path = path - if not os.path.isfile(self._iou_path) or not os.path.exists(self._iou_path): - if os.path.islink(self._iou_path): - raise IOUError("IOU image '{}' linked to '{}' is not accessible".format(self._iou_path, os.path.realpath(self._iou_path))) + self._path = path + if not os.path.isfile(self._path) or not os.path.exists(self._path): + if os.path.islink(self._path): + raise IOUError("IOU image '{}' linked to '{}' is not accessible".format(self._path, os.path.realpath(self._path))) else: - raise IOUError("IOU image '{}' is not accessible".format(self._iou_path)) + raise IOUError("IOU image '{}' is not accessible".format(self._path)) try: - with open(self._iou_path, "rb") as f: + with open(self._path, "rb") as f: # read the first 7 bytes of the file. elf_header_start = f.read(7) except OSError as e: - raise IOUError("Cannot read ELF header for IOU image '{}': {}".format(self._iou_path, e)) + raise IOUError("Cannot read ELF header for IOU image '{}': {}".format(self._path, e)) # IOU images must start with the ELF magic number, be 32-bit, little endian # and have an ELF version of 1 normal IOS image are big endian! if elf_header_start != b'\x7fELF\x01\x01\x01': - raise IOUError("'{}' is not a valid IOU image".format(self._iou_path)) + raise IOUError("'{}' is not a valid IOU image".format(self._path)) - if not os.access(self._iou_path, os.X_OK): - raise IOUError("IOU image '{}' is not executable".format(self._iou_path)) + if not os.access(self._path, os.X_OK): + raise IOUError("IOU image '{}' is not executable".format(self._path)) @property def iourc_path(self): @@ -194,7 +194,7 @@ class IOUVM(BaseVM): "console": self._console, "project_id": self.project.id, "iourc_path": self._iourc_path, - "iou_path": self.iou_path + "path": self.path } @property @@ -245,7 +245,7 @@ class IOUVM(BaseVM): """ try: - output = subprocess.check_output(["ldd", self._iou_path]) + output = subprocess.check_output(["ldd", self._path]) except (FileNotFoundError, subprocess.SubprocessError) as e: log.warn("could not determine the shared library dependencies for {}: {}".format(self._path, e)) return @@ -296,8 +296,8 @@ class IOUVM(BaseVM): raise IOUError("could not start IOU: {}: 32-bit binary support is probably not installed".format(e)) except (OSError, subprocess.SubprocessError) as e: iou_stdout = self.read_iou_stdout() - log.error("could not start IOU {}: {}\n{}".format(self._iou_path, e, iou_stdout)) - raise IOUError("could not start IOU {}: {}\n{}".format(self._iou_path, e, iou_stdout)) + log.error("could not start IOU {}: {}\n{}".format(self._path, e, iou_stdout)) + raise IOUError("could not start IOU {}: {}\n{}".format(self._path, e, iou_stdout)) # start console support self._start_ioucon() @@ -412,13 +412,14 @@ class IOUVM(BaseVM): self._iou_process = None - self._terminate_process_iouyap() - try: - yield from asyncio.wait_for(self._iouyap_process.wait(), timeout=3) - except asyncio.TimeoutError: - self._iou_process.kill() - if self._iouyap_process.returncode is None: - log.warn("IOUYAP process {} is still running".format(self._iou_process.pid)) + if self._iouyap_process is not None: + self._terminate_process_iouyap() + try: + yield from asyncio.wait_for(self._iouyap_process.wait(), timeout=3) + except asyncio.TimeoutError: + self._iou_process.kill() + if self._iouyap_process.returncode is None: + log.warn("IOUYAP process {} is still running".format(self._iou_process.pid)) self._started = False @@ -512,7 +513,7 @@ class IOUVM(BaseVM): -N Ignore the NETMAP file """ - command = [self._iou_path] + command = [self._path] if len(self._ethernet_adapters) != 2: command.extend(["-e", str(len(self._ethernet_adapters))]) if len(self._serial_adapters) != 2: diff --git a/gns3server/schemas/iou.py b/gns3server/schemas/iou.py index 562ac0f7..5c3434b6 100644 --- a/gns3server/schemas/iou.py +++ b/gns3server/schemas/iou.py @@ -42,7 +42,7 @@ IOU_CREATE_SCHEMA = { "maximum": 65535, "type": ["integer", "null"] }, - "iou_path": { + "path": { "description": "Path of iou binary", "type": "string" }, @@ -52,7 +52,7 @@ IOU_CREATE_SCHEMA = { }, }, "additionalProperties": False, - "required": ["name", "iou_path", "iourc_path"] + "required": ["name", "path", "iourc_path"] } IOU_UPDATE_SCHEMA = { @@ -71,7 +71,7 @@ IOU_UPDATE_SCHEMA = { "maximum": 65535, "type": ["integer", "null"] }, - "iou_path": { + "path": { "description": "Path of iou binary", "type": "string" }, @@ -113,7 +113,7 @@ IOU_OBJECT_SCHEMA = { "maxLength": 36, "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" }, - "iou_path": { + "path": { "description": "Path of iou binary", "type": "string" }, @@ -123,5 +123,5 @@ IOU_OBJECT_SCHEMA = { }, }, "additionalProperties": False, - "required": ["name", "vm_id", "console", "project_id", "iou_path", "iourc_path"] + "required": ["name", "vm_id", "console", "project_id", "path", "iourc_path"] } diff --git a/tests/api/test_iou.py b/tests/api/test_iou.py index 2e0a6fb7..75188cb3 100644 --- a/tests/api/test_iou.py +++ b/tests/api/test_iou.py @@ -36,7 +36,7 @@ def fake_iou_bin(tmpdir): @pytest.fixture def base_params(tmpdir, fake_iou_bin): """Return standard parameters""" - return {"name": "PC TEST 1", "iou_path": fake_iou_bin, "iourc_path": str(tmpdir / "iourc")} + return {"name": "PC TEST 1", "path": fake_iou_bin, "iourc_path": str(tmpdir / "iourc")} @pytest.fixture diff --git a/tests/modules/iou/test_iou_vm.py b/tests/modules/iou/test_iou_vm.py index 5f5d4093..8f8a0181 100644 --- a/tests/modules/iou/test_iou_vm.py +++ b/tests/modules/iou/test_iou_vm.py @@ -47,8 +47,8 @@ def vm(project, manager, tmpdir, fake_iou_bin): config["iouyap_path"] = fake_file manager.config.set_section_config("IOU", config) - vm.iou_path = fake_iou_bin - vm.iourc = fake_file + vm.path = fake_iou_bin + vm.iourc_path = fake_file return vm @@ -76,11 +76,13 @@ def test_vm_invalid_iouyap_path(project, manager, loop): loop.run_until_complete(asyncio.async(vm.start())) -def test_start(loop, vm): +def test_start(loop, vm, monkeypatch): with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._check_requirements", return_value=True): - with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): - loop.run_until_complete(asyncio.async(vm.start())) - assert vm.is_running() + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._start_ioucon", return_value=True): + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._start_iouyap", return_value=True): + with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): + loop.run_until_complete(asyncio.async(vm.start())) + assert vm.is_running() def test_stop(loop, vm): @@ -92,12 +94,14 @@ def test_stop(loop, vm): process.wait.return_value = future with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._check_requirements", return_value=True): - with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): - loop.run_until_complete(asyncio.async(vm.start())) - assert vm.is_running() - loop.run_until_complete(asyncio.async(vm.stop())) - assert vm.is_running() is False - process.terminate.assert_called_with() + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._start_ioucon", return_value=True): + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._start_iouyap", return_value=True): + with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): + loop.run_until_complete(asyncio.async(vm.start())) + assert vm.is_running() + loop.run_until_complete(asyncio.async(vm.stop())) + assert vm.is_running() is False + process.terminate.assert_called_with() def test_reload(loop, vm, fake_iou_bin): @@ -109,12 +113,14 @@ def test_reload(loop, vm, fake_iou_bin): process.wait.return_value = future with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._check_requirements", return_value=True): - with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): - loop.run_until_complete(asyncio.async(vm.start())) - assert vm.is_running() - loop.run_until_complete(asyncio.async(vm.reload())) - assert vm.is_running() is True - process.terminate.assert_called_with() + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._start_ioucon", return_value=True): + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._start_iouyap", return_value=True): + with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): + loop.run_until_complete(asyncio.async(vm.start())) + assert vm.is_running() + loop.run_until_complete(asyncio.async(vm.reload())) + assert vm.is_running() is True + process.terminate.assert_called_with() def test_close(vm, port_manager): @@ -128,23 +134,23 @@ def test_close(vm, port_manager): assert vm.is_running() is False -def test_iou_path(vm, fake_iou_bin): +def test_path(vm, fake_iou_bin): - vm.iou_path = fake_iou_bin - assert vm.iou_path == fake_iou_bin + vm.path = fake_iou_bin + assert vm.path == fake_iou_bin def test_path_invalid_bin(vm, tmpdir): - iou_path = str(tmpdir / "test.bin") + path = str(tmpdir / "test.bin") with pytest.raises(IOUError): - vm.iou_path = iou_path + vm.path = path - with open(iou_path, "w+") as f: + with open(path, "w+") as f: f.write("BUG") with pytest.raises(IOUError): - vm.iou_path = iou_path + vm.path = path def test_create_netmap_config(vm): @@ -161,4 +167,4 @@ def test_create_netmap_config(vm): def test_build_command(vm): - assert vm._build_command() == [vm.iou_path, '-L', str(vm.application_id)] + assert vm._build_command() == [vm.path, '-L', str(vm.application_id)] From 4689024b50c98e98a31998c43ac765c5813af938 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 12 Feb 2015 16:25:55 +0100 Subject: [PATCH 225/485] Add a --live options to control livereload Because the livereload bug due to timezone issues with Vagrant --- gns3server/main.py | 5 ++++- gns3server/server.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/gns3server/main.py b/gns3server/main.py index 4810fd68..991f8cce 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -91,6 +91,7 @@ def parse_arguments(argv, config): "allow": config.getboolean("allow_remote_console", False), "quiet": config.getboolean("quiet", False), "debug": config.getboolean("debug", False), + "live": config.getboolean("live", False), } parser = argparse.ArgumentParser(description="GNS3 server version {}".format(__version__)) @@ -104,7 +105,9 @@ def parse_arguments(argv, config): parser.add_argument("-L", "--local", action="store_true", help="local mode (allows some insecure operations)") parser.add_argument("-A", "--allow", action="store_true", help="allow remote connections to local console ports") parser.add_argument("-q", "--quiet", action="store_true", help="do not show logs on stdout") - parser.add_argument("-d", "--debug", action="store_true", help="show debug logs and enable code live reload") + parser.add_argument("-d", "--debug", action="store_true", help="show debug logs") + parser.add_argument("--live", action="store_true", help="enable code live reload") + return parser.parse_args(argv) diff --git a/gns3server/server.py b/gns3server/server.py index 349880e5..baf7e1a9 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -173,7 +173,7 @@ class Server: self._loop.run_until_complete(self._run_application(app, ssl_context)) self._signal_handling() - if server_config.getboolean("debug"): + if server_config.getboolean("live"): log.info("Code live reload is enabled, watching for file changes") self._loop.call_later(1, self._reload_hook) self._loop.run_forever() From 05df7001a393327d8bfc7345f2011ff46f32a1b2 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 12 Feb 2015 17:03:01 +0100 Subject: [PATCH 226/485] Successfully create an iou device from the GUI via HTTP --- gns3server/main.py | 1 - gns3server/modules/iou/iou_vm.py | 6 +++--- gns3server/schemas/iou.py | 24 ++++++++++++++++-------- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/gns3server/main.py b/gns3server/main.py index 991f8cce..f25ce6d9 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -108,7 +108,6 @@ def parse_arguments(argv, config): parser.add_argument("-d", "--debug", action="store_true", help="show debug logs") parser.add_argument("--live", action="store_true", help="enable code live reload") - return parser.parse_args(argv) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 8beb127c..11481c7a 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -193,7 +193,6 @@ class IOUVM(BaseVM): "vm_id": self.id, "console": self._console, "project_id": self.project.id, - "iourc_path": self._iourc_path, "path": self.path } @@ -268,7 +267,7 @@ class IOUVM(BaseVM): # TODO: ASYNC # self._library_check() - if not self._iourc_path or not os.path.isfile(self._iourc_path): + if self._iourc_path and not os.path.isfile(self._iourc_path): raise IOUError("A valid iourc file is necessary to start IOU") iouyap_path = self.iouyap_path @@ -278,7 +277,8 @@ class IOUVM(BaseVM): self._create_netmap_config() # created a environment variable pointing to the iourc file. env = os.environ.copy() - env["IOURC"] = self._iourc_path + if self._iourc_path: + env["IOURC"] = self._iourc_path self._command = self._build_command() try: log.info("Starting IOU: {}".format(self._command)) diff --git a/gns3server/schemas/iou.py b/gns3server/schemas/iou.py index 5c3434b6..bc4f1ad4 100644 --- a/gns3server/schemas/iou.py +++ b/gns3server/schemas/iou.py @@ -52,7 +52,7 @@ IOU_CREATE_SCHEMA = { }, }, "additionalProperties": False, - "required": ["name", "path", "iourc_path"] + "required": ["name", "path"] } IOU_UPDATE_SCHEMA = { @@ -73,12 +73,24 @@ IOU_UPDATE_SCHEMA = { }, "path": { "description": "Path of iou binary", - "type": "string" + "type": ["string", "null"] }, "iourc_path": { "description": "Path of iourc", - "type": "string" + "type": ["string", "null"] + }, + "initial_config": { + "description": "Initial configuration path", + "type": ["string", "null"] + }, + "serial_adapters": { + "description": "How many serial adapters are connected to the IOU", + "type": ["integer", "null"] }, + "ethernet_adapters": { + "description": "How many ethernet adapters are connected to the IOU", + "type": ["integer", "null"] + } }, "additionalProperties": False, } @@ -117,11 +129,7 @@ IOU_OBJECT_SCHEMA = { "description": "Path of iou binary", "type": "string" }, - "iourc_path": { - "description": "Path of iourc", - "type": "string" - }, }, "additionalProperties": False, - "required": ["name", "vm_id", "console", "project_id", "path", "iourc_path"] + "required": ["name", "vm_id", "console", "project_id", "path"] } From 8b61aa9ae7ca3a078a04b19c5dc0a35d495e4d75 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 12 Feb 2015 21:02:52 +0100 Subject: [PATCH 227/485] Set ram, ethernet adapters, serial adapters --- gns3server/handlers/iou_handler.py | 9 ++ gns3server/modules/iou/iou_vm.py | 130 +++++++++++++++++++++++++++-- gns3server/schemas/iou.py | 42 +++++++++- tests/api/test_iou.py | 43 +++++++++- 4 files changed, 213 insertions(+), 11 deletions(-) diff --git a/gns3server/handlers/iou_handler.py b/gns3server/handlers/iou_handler.py index ab39622f..d73a0fb1 100644 --- a/gns3server/handlers/iou_handler.py +++ b/gns3server/handlers/iou_handler.py @@ -49,6 +49,10 @@ class IOUHandler: request.match_info["project_id"], request.json.get("vm_id"), console=request.json.get("console"), + serial_adapters=request.json.get("serial_adapters"), + ethernet_adapters=request.json.get("ethernet_adapters"), + ram=request.json.get("ram"), + nvram=request.json.get("nvram") ) vm.path = request.json.get("path", vm.path) vm.iourc_path = request.json.get("iourc_path", vm.iourc_path) @@ -99,6 +103,11 @@ class IOUHandler: vm.console = request.json.get("console", vm.console) vm.path = request.json.get("path", vm.path) vm.iourc_path = request.json.get("iourc_path", vm.iourc_path) + vm.ethernet_adapters = request.json.get("ethernet_adapters", vm.ethernet_adapters) + vm.serial_adapters = request.json.get("serial_adapters", vm.serial_adapters) + vm.ram = request.json.get("ram", vm.ram) + vm.nvram = request.json.get("nvram", vm.nvram) + response.json(vm) @classmethod diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 11481c7a..6fb7db94 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -55,11 +55,19 @@ class IOUVM(BaseVM): :param manager: parent VM Manager :param console: TCP console port :params console_host: TCP console host IP + :params ethernet_adapters: Number of ethernet adapters + :params serial_adapters: Number of serial adapters + :params ram: Ram MB + :params nvram: Nvram KB """ def __init__(self, name, vm_id, project, manager, console=None, - console_host="0.0.0.0"): + console_host="0.0.0.0", + ram=None, + nvram=None, + ethernet_adapters=None, + serial_adapters=None): super().__init__(name, vm_id, project, manager) @@ -75,13 +83,14 @@ class IOUVM(BaseVM): self._console_host = console_host # IOU settings - self._ethernet_adapters = [EthernetAdapter(), EthernetAdapter()] # one adapter = 4 interfaces - self._serial_adapters = [SerialAdapter(), SerialAdapter()] # one adapter = 4 interfaces - self._slots = self._ethernet_adapters + self._serial_adapters + self._ethernet_adapters = [] + self._serial_adapters = [] + self.ethernet_adapters = 2 if ethernet_adapters is None else ethernet_adapters # one adapter = 4 interfaces + self.serial_adapters = 2 if serial_adapters is None else serial_adapters # one adapter = 4 interfaces self._use_default_iou_values = True # for RAM & NVRAM values - self._nvram = 128 # Kilobytes + self._nvram = 128 if nvram is None else nvram # Kilobytes self._initial_config = "" - self._ram = 256 # Megabytes + self._ram = 256 if ram is None else ram # Megabytes self._l1_keepalives = False # used to overcome the always-up Ethernet interfaces (not supported by all IOSes). if self._console is not None: @@ -193,7 +202,11 @@ class IOUVM(BaseVM): "vm_id": self.id, "console": self._console, "project_id": self.project.id, - "path": self.path + "path": self.path, + "ethernet_adapters": len(self._ethernet_adapters), + "serial_adapters": len(self._serial_adapters), + "ram": self._ram, + "nvram": self._nvram } @property @@ -233,6 +246,57 @@ class IOUVM(BaseVM): self._manager.port_manager.release_console_port(self._console) self._console = self._manager.port_manager.reserve_console_port(console) + @property + def ram(self): + """ + Returns the amount of RAM allocated to this IOU instance. + :returns: amount of RAM in Mbytes (integer) + """ + + return self._ram + + @ram.setter + def ram(self, ram): + """ + Sets amount of RAM allocated to this IOU instance. + :param ram: amount of RAM in Mbytes (integer) + """ + + if self._ram == ram: + return + + log.info("IOU {name} [id={id}]: RAM updated from {old_ram}MB to {new_ram}MB".format(name=self._name, + id=self._id, + old_ram=self._ram, + new_ram=ram)) + + self._ram = ram + + @property + def nvram(self): + """ + Returns the mount of NVRAM allocated to this IOU instance. + :returns: amount of NVRAM in Kbytes (integer) + """ + + return self._nvram + + @nvram.setter + def nvram(self, nvram): + """ + Sets amount of NVRAM allocated to this IOU instance. + :param nvram: amount of NVRAM in Kbytes (integer) + """ + + if self._nvram == nvram: + return + + log.info("IOU {name} [id={id}]: NVRAM updated from {old_nvram}KB to {new_nvram}KB".format(name=self._name, + id=self._id, + old_nvram=self._nvram, + new_nvram=nvram)) + self._nvram = nvram + @property def application_id(self): return self._manager.get_application_id(self.id) @@ -571,3 +635,55 @@ class IOUVM(BaseVM): self._ioucon_thread_stop_event = threading.Event() self._ioucon_thread = threading.Thread(target=start_ioucon, args=(args, self._ioucon_thread_stop_event)) self._ioucon_thread.start() + + @property + def ethernet_adapters(self): + """ + Returns the number of Ethernet adapters for this IOU instance. + :returns: number of adapters + """ + + return len(self._ethernet_adapters) + + @ethernet_adapters.setter + def ethernet_adapters(self, ethernet_adapters): + """ + Sets the number of Ethernet adapters for this IOU instance. + :param ethernet_adapters: number of adapters + """ + + self._ethernet_adapters.clear() + for _ in range(0, ethernet_adapters): + self._ethernet_adapters.append(EthernetAdapter()) + + log.info("IOU {name} [id={id}]: number of Ethernet adapters changed to {adapters}".format(name=self._name, + id=self._id, + adapters=len(self._ethernet_adapters))) + + self._slots = self._ethernet_adapters + self._serial_adapters + + @property + def serial_adapters(self): + """ + Returns the number of Serial adapters for this IOU instance. + :returns: number of adapters + """ + + return len(self._serial_adapters) + + @serial_adapters.setter + def serial_adapters(self, serial_adapters): + """ + Sets the number of Serial adapters for this IOU instance. + :param serial_adapters: number of adapters + """ + + self._serial_adapters.clear() + for _ in range(0, serial_adapters): + self._serial_adapters.append(SerialAdapter()) + + log.info("IOU {name} [id={id}]: number of Serial adapters changed to {adapters}".format(name=self._name, + id=self._id, + adapters=len(self._serial_adapters))) + + self._slots = self._ethernet_adapters + self._serial_adapters diff --git a/gns3server/schemas/iou.py b/gns3server/schemas/iou.py index bc4f1ad4..387874cf 100644 --- a/gns3server/schemas/iou.py +++ b/gns3server/schemas/iou.py @@ -50,6 +50,22 @@ IOU_CREATE_SCHEMA = { "description": "Path of iourc", "type": "string" }, + "serial_adapters": { + "description": "How many serial adapters are connected to the IOU", + "type": "integer" + }, + "ethernet_adapters": { + "description": "How many ethernet adapters are connected to the IOU", + "type": "integer" + }, + "ram": { + "description": "Allocated RAM MB", + "type": ["integer", "null"] + }, + "nvram": { + "description": "Allocated NVRAM KB", + "type": ["integer", "null"] + } }, "additionalProperties": False, "required": ["name", "path"] @@ -90,6 +106,14 @@ IOU_UPDATE_SCHEMA = { "ethernet_adapters": { "description": "How many ethernet adapters are connected to the IOU", "type": ["integer", "null"] + }, + "ram": { + "description": "Allocated RAM MB", + "type": ["integer", "null"] + }, + "nvram": { + "description": "Allocated NVRAM KB", + "type": ["integer", "null"] } }, "additionalProperties": False, @@ -129,7 +153,23 @@ IOU_OBJECT_SCHEMA = { "description": "Path of iou binary", "type": "string" }, + "serial_adapters": { + "description": "How many serial adapters are connected to the IOU", + "type": "integer" + }, + "ethernet_adapters": { + "description": "How many ethernet adapters are connected to the IOU", + "type": "integer" + }, + "ram": { + "description": "Allocated RAM MB", + "type": "integer" + }, + "nvram": { + "description": "Allocated NVRAM KB", + "type": "integer" + } }, "additionalProperties": False, - "required": ["name", "vm_id", "console", "project_id", "path"] + "required": ["name", "vm_id", "console", "project_id", "path", "serial_adapters", "ethernet_adapters", "ram", "nvram"] } diff --git a/tests/api/test_iou.py b/tests/api/test_iou.py index 75188cb3..6ae3f715 100644 --- a/tests/api/test_iou.py +++ b/tests/api/test_iou.py @@ -47,11 +47,33 @@ def vm(server, project, base_params): def test_iou_create(server, project, base_params): - response = server.post("/projects/{project_id}/iou/vms".format(project_id=project.id), base_params, example=True) + response = server.post("/projects/{project_id}/iou/vms".format(project_id=project.id), base_params) + assert response.status == 201 + assert response.route == "/projects/{project_id}/iou/vms" + assert response.json["name"] == "PC TEST 1" + assert response.json["project_id"] == project.id + assert response.json["serial_adapters"] == 2 + assert response.json["ethernet_adapters"] == 2 + assert response.json["ram"] == 256 + assert response.json["nvram"] == 128 + + +def test_iou_create_with_params(server, project, base_params): + params = base_params + params["ram"] = 1024 + params["nvram"] = 512 + params["serial_adapters"] = 4 + params["ethernet_adapters"] = 0 + + response = server.post("/projects/{project_id}/iou/vms".format(project_id=project.id), params, example=True) assert response.status == 201 assert response.route == "/projects/{project_id}/iou/vms" assert response.json["name"] == "PC TEST 1" assert response.json["project_id"] == project.id + assert response.json["serial_adapters"] == 4 + assert response.json["ethernet_adapters"] == 0 + assert response.json["ram"] == 1024 + assert response.json["nvram"] == 512 def test_iou_get(server, project, vm): @@ -60,6 +82,10 @@ def test_iou_get(server, project, vm): assert response.route == "/projects/{project_id}/iou/vms/{vm_id}" assert response.json["name"] == "PC TEST 1" assert response.json["project_id"] == project.id + assert response.json["serial_adapters"] == 2 + assert response.json["ethernet_adapters"] == 2 + assert response.json["ram"] == 256 + assert response.json["nvram"] == 128 def test_iou_start(server, vm): @@ -91,8 +117,19 @@ def test_iou_delete(server, vm): def test_iou_update(server, vm, tmpdir, free_console_port): - response = server.put("/projects/{project_id}/iou/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"name": "test", - "console": free_console_port}) + params = { + "name": "test", + "console": free_console_port, + "ram": 512, + "nvram": 2048, + "ethernet_adapters": 4, + "serial_adapters": 0 + } + response = server.put("/projects/{project_id}/iou/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), params) assert response.status == 200 assert response.json["name"] == "test" assert response.json["console"] == free_console_port + assert response.json["ethernet_adapters"] == 4 + assert response.json["serial_adapters"] == 0 + assert response.json["ram"] == 512 + assert response.json["nvram"] == 2048 From 3471b03ef92ef1f7ac84ba9631ccc603c5838c26 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 12 Feb 2015 21:39:24 +0100 Subject: [PATCH 228/485] Clarify JSON schema validation errors --- gns3server/web/response.py | 4 +--- gns3server/web/route.py | 5 +---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/gns3server/web/response.py b/gns3server/web/response.py index 9ae3c1a6..9241d0c9 100644 --- a/gns3server/web/response.py +++ b/gns3server/web/response.py @@ -62,8 +62,6 @@ class Response(aiohttp.web.Response): try: jsonschema.validate(answer, self._output_schema) except jsonschema.ValidationError as e: - log.error("Invalid output schema {} '{}' in schema: {}".format(e.validator, - e.validator_value, - json.dumps(e.schema))) + log.error("Invalid output query. JSON schema error: {}".format(e.message)) 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 index 10c28205..d63701fc 100644 --- a/gns3server/web/route.py +++ b/gns3server/web/route.py @@ -28,7 +28,6 @@ log = logging.getLogger(__name__) from ..modules.vm_error import VMError from .response import Response - @asyncio.coroutine def parse_request(request, input_schema): """Parse body of request and raise HTTP errors in case of problems""" @@ -42,9 +41,7 @@ def parse_request(request, input_schema): try: jsonschema.validate(request.json, input_schema) except jsonschema.ValidationError as e: - log.error("Invalid input schema {} '{}' in schema: {}".format(e.validator, - e.validator_value, - json.dumps(e.schema))) + log.error("Invalid input query. JSON schema error: {}".format(e.message)) raise aiohttp.web.HTTPBadRequest(text="Request is not {} '{}' in schema: {}".format( e.validator, e.validator_value, From 9160d3caf46722d6449e832274aedcdb5852aed9 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 12 Feb 2015 21:44:43 +0100 Subject: [PATCH 229/485] Remove old directories to avoid editing them by mistake... --- gns3server/old_modules/iou/__init__.py | 843 ----------- .../old_modules/iou/adapters/__init__.py | 0 .../old_modules/iou/adapters/adapter.py | 105 -- .../iou/adapters/ethernet_adapter.py | 32 - .../iou/adapters/serial_adapter.py | 32 - gns3server/old_modules/iou/iou_device.py | 1069 -------------- gns3server/old_modules/iou/iou_error.py | 39 - gns3server/old_modules/iou/nios/__init__.py | 0 gns3server/old_modules/iou/nios/nio.py | 80 -- .../iou/nios/nio_generic_ethernet.py | 50 - gns3server/old_modules/iou/nios/nio_tap.py | 50 - gns3server/old_modules/iou/nios/nio_udp.py | 76 - gns3server/old_modules/iou/schemas.py | 472 ------- gns3server/old_modules/qemu/__init__.py | 687 --------- .../old_modules/qemu/adapters/__init__.py | 0 .../old_modules/qemu/adapters/adapter.py | 105 -- .../qemu/adapters/ethernet_adapter.py | 32 - gns3server/old_modules/qemu/nios/__init__.py | 0 gns3server/old_modules/qemu/nios/nio.py | 66 - gns3server/old_modules/qemu/nios/nio_udp.py | 76 - gns3server/old_modules/qemu/qemu_error.py | 39 - gns3server/old_modules/qemu/qemu_vm.py | 1244 ----------------- gns3server/old_modules/qemu/schemas.py | 423 ------ 23 files changed, 5520 deletions(-) delete mode 100644 gns3server/old_modules/iou/__init__.py delete mode 100644 gns3server/old_modules/iou/adapters/__init__.py delete mode 100644 gns3server/old_modules/iou/adapters/adapter.py delete mode 100644 gns3server/old_modules/iou/adapters/ethernet_adapter.py delete mode 100644 gns3server/old_modules/iou/adapters/serial_adapter.py delete mode 100644 gns3server/old_modules/iou/iou_device.py delete mode 100644 gns3server/old_modules/iou/iou_error.py delete mode 100644 gns3server/old_modules/iou/nios/__init__.py delete mode 100644 gns3server/old_modules/iou/nios/nio.py delete mode 100644 gns3server/old_modules/iou/nios/nio_generic_ethernet.py delete mode 100644 gns3server/old_modules/iou/nios/nio_tap.py delete mode 100644 gns3server/old_modules/iou/nios/nio_udp.py delete mode 100644 gns3server/old_modules/iou/schemas.py delete mode 100644 gns3server/old_modules/qemu/__init__.py delete mode 100644 gns3server/old_modules/qemu/adapters/__init__.py delete mode 100644 gns3server/old_modules/qemu/adapters/adapter.py delete mode 100644 gns3server/old_modules/qemu/adapters/ethernet_adapter.py delete mode 100644 gns3server/old_modules/qemu/nios/__init__.py delete mode 100644 gns3server/old_modules/qemu/nios/nio.py delete mode 100644 gns3server/old_modules/qemu/nios/nio_udp.py delete mode 100644 gns3server/old_modules/qemu/qemu_error.py delete mode 100644 gns3server/old_modules/qemu/qemu_vm.py delete mode 100644 gns3server/old_modules/qemu/schemas.py diff --git a/gns3server/old_modules/iou/__init__.py b/gns3server/old_modules/iou/__init__.py deleted file mode 100644 index 04c7e4c0..00000000 --- a/gns3server/old_modules/iou/__init__.py +++ /dev/null @@ -1,843 +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 . - -""" -IOU server module. -""" - -import os -import base64 -import ntpath -import stat -import tempfile -import socket -import shutil - -from gns3server.modules import IModule -from gns3server.config import Config -from gns3dms.cloud.rackspace_ctrl import get_provider -from .iou_device import IOUDevice -from .iou_error import IOUError -from .nios.nio_udp import NIO_UDP -from .nios.nio_tap import NIO_TAP -from .nios.nio_generic_ethernet import NIO_GenericEthernet -from ..attic import find_unused_port -from ..attic import has_privileged_access - -from .schemas import IOU_CREATE_SCHEMA -from .schemas import IOU_DELETE_SCHEMA -from .schemas import IOU_UPDATE_SCHEMA -from .schemas import IOU_START_SCHEMA -from .schemas import IOU_STOP_SCHEMA -from .schemas import IOU_RELOAD_SCHEMA -from .schemas import IOU_ALLOCATE_UDP_PORT_SCHEMA -from .schemas import IOU_ADD_NIO_SCHEMA -from .schemas import IOU_DELETE_NIO_SCHEMA -from .schemas import IOU_START_CAPTURE_SCHEMA -from .schemas import IOU_STOP_CAPTURE_SCHEMA -from .schemas import IOU_EXPORT_CONFIG_SCHEMA - -import logging -log = logging.getLogger(__name__) - - -class IOU(IModule): - - """ - IOU 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 iouyap location - config = Config.instance() - iou_config = config.get_section_config(name.upper()) - self._iouyap = iou_config.get("iouyap_path") - if not self._iouyap or not os.path.isfile(self._iouyap): - paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep) - # look for iouyap in the current working directory and $PATH - for path in paths: - try: - if "iouyap" in os.listdir(path) and os.access(os.path.join(path, "iouyap"), os.X_OK): - self._iouyap = os.path.join(path, "iouyap") - break - except OSError: - continue - - if not self._iouyap: - log.warning("iouyap binary couldn't be found!") - elif not os.access(self._iouyap, os.X_OK): - log.warning("iouyap is not executable") - - # a new process start when calling IModule - IModule.__init__(self, name, *args, **kwargs) - self._iou_instances = {} - self._console_start_port_range = iou_config.get("console_start_port_range", 4001) - self._console_end_port_range = iou_config.get("console_end_port_range", 4500) - self._allocated_udp_ports = [] - self._udp_start_port_range = iou_config.get("udp_start_port_range", 30001) - self._udp_end_port_range = iou_config.get("udp_end_port_range", 35000) - self._host = iou_config.get("host", kwargs["host"]) - self._console_host = iou_config.get("console_host", kwargs["console_host"]) - self._projects_dir = kwargs["projects_dir"] - self._tempdir = kwargs["temp_dir"] - self._working_dir = self._projects_dir - self._server_iourc_path = iou_config.get("iourc", "") - self._iourc = "" - - # check every 5 seconds - self._iou_callback = self.add_periodic_callback(self._check_iou_is_alive, 5000) - self._iou_callback.start() - - def stop(self, signum=None): - """ - Properly stops the module. - - :param signum: signal number (if called by the signal handler) - """ - - self._iou_callback.stop() - - # delete all IOU instances - for iou_id in self._iou_instances: - iou_instance = self._iou_instances[iou_id] - iou_instance.delete() - - self.delete_iourc_file() - - IModule.stop(self, signum) # this will stop the I/O loop - - def _check_iou_is_alive(self): - """ - Periodic callback to check if IOU and iouyap are alive - for each IOU instance. - - Sends a notification to the client if not. - """ - - for iou_id in self._iou_instances: - iou_instance = self._iou_instances[iou_id] - if iou_instance.started and (not iou_instance.is_running() or not iou_instance.is_iouyap_running()): - notification = {"module": self.name, - "id": iou_id, - "name": iou_instance.name} - if not iou_instance.is_running(): - stdout = iou_instance.read_iou_stdout() - notification["message"] = "IOU has stopped running" - notification["details"] = stdout - self.send_notification("{}.iou_stopped".format(self.name), notification) - elif not iou_instance.is_iouyap_running(): - stdout = iou_instance.read_iouyap_stdout() - notification["message"] = "iouyap has stopped running" - notification["details"] = stdout - self.send_notification("{}.iouyap_stopped".format(self.name), notification) - iou_instance.stop() - - def get_iou_instance(self, iou_id): - """ - Returns an IOU device instance. - - :param iou_id: IOU device identifier - - :returns: IOUDevice instance - """ - - if iou_id not in self._iou_instances: - log.debug("IOU device ID {} doesn't exist".format(iou_id), exc_info=1) - self.send_custom_error("IOU device ID {} doesn't exist".format(iou_id)) - return None - return self._iou_instances[iou_id] - - def delete_iourc_file(self): - """ - Deletes the IOURC file. - """ - - if self._iourc and os.path.isfile(self._iourc): - try: - log.info("deleting iourc file {}".format(self._iourc)) - os.remove(self._iourc) - except OSError as e: - log.warn("could not delete iourc file {}: {}".format(self._iourc, e)) - - @IModule.route("iou.reset") - def reset(self, request=None): - """ - Resets the module (JSON-RPC notification). - - :param request: JSON request (not used) - """ - - # delete all IOU instances - for iou_id in self._iou_instances: - iou_instance = self._iou_instances[iou_id] - iou_instance.delete() - - # resets the instance IDs - IOUDevice.reset() - - self._iou_instances.clear() - self._allocated_udp_ports.clear() - self.delete_iourc_file() - - self._working_dir = self._projects_dir - log.info("IOU module has been reset") - - @IModule.route("iou.settings") - def settings(self, request): - """ - Set or update settings. - - Mandatory request parameters: - - iourc (base64 encoded iourc file) - - Optional request parameters: - - iouyap (path to iouyap) - - 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 "iourc" in request: - iourc_content = base64.decodebytes(request["iourc"].encode("utf-8")).decode("utf-8") - iourc_content = iourc_content.replace("\r\n", "\n") # dos2unix - try: - with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: - log.info("saving iourc file content to {}".format(f.name)) - f.write(iourc_content) - self._iourc = f.name - except OSError as e: - raise IOUError("Could not create the iourc file: {}".format(e)) - - if "iouyap" in request and request["iouyap"]: - self._iouyap = request["iouyap"] - log.info("iouyap path set to {}".format(self._iouyap)) - - 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 iou_id in self._iou_instances: - iou_instance = self._iou_instances[iou_id] - iou_instance.working_dir = os.path.join(self._working_dir, "iou", "device-{}".format(iou_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("iou.create") - def iou_create(self, request): - """ - Creates a new IOU instance. - - Mandatory request parameters: - - path (path to the IOU executable) - - Optional request parameters: - - name (IOU name) - - console (IOU console port) - - Response parameters: - - id (IOU instance identifier) - - name (IOU name) - - default settings - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, IOU_CREATE_SCHEMA): - return - - name = request["name"] - iou_path = request["path"] - console = request.get("console") - iou_id = request.get("iou_id") - - updated_iou_path = os.path.join(self.images_directory, iou_path) - if os.path.isfile(updated_iou_path): - iou_path = updated_iou_path - else: - if not os.path.exists(self.images_directory): - os.mkdir(self.images_directory) - cloud_path = request.get("cloud_path", None) - if cloud_path is not None: - # Download the image from cloud files - _, filename = ntpath.split(iou_path) - src = '{}/{}'.format(cloud_path, filename) - provider = get_provider(self._cloud_settings) - log.debug("Downloading file from {} to {}...".format(src, updated_iou_path)) - provider.download_file(src, updated_iou_path) - log.debug("Download of {} complete.".format(src)) - # Make file executable - st = os.stat(updated_iou_path) - os.chmod(updated_iou_path, st.st_mode | stat.S_IEXEC) - iou_path = updated_iou_path - - try: - iou_instance = IOUDevice(name, - iou_path, - self._working_dir, - iou_id, - console, - self._console_host, - self._console_start_port_range, - self._console_end_port_range) - - except IOUError as e: - self.send_custom_error(str(e)) - return - - response = {"name": iou_instance.name, - "id": iou_instance.id} - - defaults = iou_instance.defaults() - response.update(defaults) - self._iou_instances[iou_instance.id] = iou_instance - self.send_response(response) - - @IModule.route("iou.delete") - def iou_delete(self, request): - """ - Deletes an IOU instance. - - Mandatory request parameters: - - id (IOU instance identifier) - - Response parameter: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, IOU_DELETE_SCHEMA): - return - - # get the instance - iou_instance = self.get_iou_instance(request["id"]) - if not iou_instance: - return - - try: - iou_instance.clean_delete() - del self._iou_instances[request["id"]] - except IOUError as e: - self.send_custom_error(str(e)) - return - - self.send_response(True) - - @IModule.route("iou.update") - def iou_update(self, request): - """ - Updates an IOU instance - - Mandatory request parameters: - - id (IOU instance identifier) - - Optional request parameters: - - any setting to update - - initial_config_base64 (initial-config base64 encoded) - - Response parameters: - - updated settings - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, IOU_UPDATE_SCHEMA): - return - - # get the instance - iou_instance = self.get_iou_instance(request["id"]) - if not iou_instance: - return - - config_path = os.path.join(iou_instance.working_dir, "initial-config.cfg") - try: - if "initial_config_base64" in request: - # a new initial-config has been pushed - config = base64.decodebytes(request["initial_config_base64"].encode("utf-8")).decode("utf-8") - config = "!\n" + config.replace("\r", "") - config = config.replace('%h', iou_instance.name) - try: - with open(config_path, "w") as f: - log.info("saving initial-config to {}".format(config_path)) - f.write(config) - except OSError as e: - raise IOUError("Could not save the configuration {}: {}".format(config_path, e)) - # update the request with the new local initial-config path - request["initial_config"] = os.path.basename(config_path) - elif "initial_config" in request: - if os.path.isfile(request["initial_config"]) and request["initial_config"] != config_path: - # this is a local file set in the GUI - try: - with open(request["initial_config"], "r", errors="replace") as f: - config = f.read() - with open(config_path, "w") as f: - config = "!\n" + config.replace("\r", "") - config = config.replace('%h', iou_instance.name) - f.write(config) - request["initial_config"] = os.path.basename(config_path) - except OSError as e: - raise IOUError("Could not save the configuration from {} to {}: {}".format(request["initial_config"], config_path, e)) - elif not os.path.isfile(config_path): - raise IOUError("Startup-config {} could not be found on this server".format(request["initial_config"])) - except IOUError as e: - self.send_custom_error(str(e)) - return - - # update the IOU settings - response = {} - for name, value in request.items(): - if hasattr(iou_instance, name) and getattr(iou_instance, name) != value: - try: - setattr(iou_instance, name, value) - response[name] = value - except IOUError as e: - self.send_custom_error(str(e)) - return - - self.send_response(response) - - @IModule.route("iou.start") - def vm_start(self, request): - """ - Starts an IOU instance. - - Mandatory request parameters: - - id (IOU instance identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, IOU_START_SCHEMA): - return - - # get the instance - iou_instance = self.get_iou_instance(request["id"]) - if not iou_instance: - return - - try: - iou_instance.iouyap = self._iouyap - if self._iourc: - iou_instance.iourc = self._iourc - else: - # if there is no IOURC file pushed by the client then use the server IOURC file - iou_instance.iourc = self._server_iourc_path - iou_instance.start() - except IOUError as e: - self.send_custom_error(str(e)) - return - self.send_response(True) - - @IModule.route("iou.stop") - def vm_stop(self, request): - """ - Stops an IOU instance. - - Mandatory request parameters: - - id (IOU instance identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, IOU_STOP_SCHEMA): - return - - # get the instance - iou_instance = self.get_iou_instance(request["id"]) - if not iou_instance: - return - - try: - iou_instance.stop() - except IOUError as e: - self.send_custom_error(str(e)) - return - self.send_response(True) - - @IModule.route("iou.reload") - def vm_reload(self, request): - """ - Reloads an IOU instance. - - Mandatory request parameters: - - id (IOU identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, IOU_RELOAD_SCHEMA): - return - - # get the instance - iou_instance = self.get_iou_instance(request["id"]) - if not iou_instance: - return - - try: - if iou_instance.is_running(): - iou_instance.stop() - iou_instance.start() - except IOUError as e: - self.send_custom_error(str(e)) - return - self.send_response(True) - - @IModule.route("iou.allocate_udp_port") - def allocate_udp_port(self, request): - """ - Allocates a UDP port in order to create an UDP NIO. - - Mandatory request parameters: - - id (IOU 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, IOU_ALLOCATE_UDP_PORT_SCHEMA): - return - - # get the instance - iou_instance = self.get_iou_instance(request["id"]) - if not iou_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(iou_instance.name, - iou_instance.id, - port, - self._host)) - response = {"lport": port, - "port_id": request["port_id"]} - self.send_response(response) - - @IModule.route("iou.add_nio") - def add_nio(self, request): - """ - Adds an NIO (Network Input/Output) for an IOU instance. - - Mandatory request parameters: - - id (IOU instance identifier) - - slot (slot number) - - 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_generic_ethernet" - - ethernet_device (Ethernet device name e.g. eth0) - - 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, IOU_ADD_NIO_SCHEMA): - return - - # get the instance - iou_instance = self.get_iou_instance(request["id"]) - if not iou_instance: - return - - slot = request["slot"] - 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 IOUError("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 has_privileged_access(self._iouyap): - raise IOUError("{} has no privileged access to {}.".format(self._iouyap, tap_device)) - nio = NIO_TAP(tap_device) - elif request["nio"]["type"] == "nio_generic_ethernet": - ethernet_device = request["nio"]["ethernet_device"] - if not has_privileged_access(self._iouyap): - raise IOUError("{} has no privileged access to {}.".format(self._iouyap, ethernet_device)) - nio = NIO_GenericEthernet(ethernet_device) - if not nio: - raise IOUError("Requested NIO does not exist or is not supported: {}".format(request["nio"]["type"])) - except IOUError as e: - self.send_custom_error(str(e)) - return - - try: - iou_instance.slot_add_nio_binding(slot, port, nio) - except IOUError as e: - self.send_custom_error(str(e)) - return - - self.send_response({"port_id": request["port_id"]}) - - @IModule.route("iou.delete_nio") - def delete_nio(self, request): - """ - Deletes an NIO (Network Input/Output). - - Mandatory request parameters: - - id (IOU instance identifier) - - slot (slot identifier) - - port (port identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, IOU_DELETE_NIO_SCHEMA): - return - - # get the instance - iou_instance = self.get_iou_instance(request["id"]) - if not iou_instance: - return - - slot = request["slot"] - port = request["port"] - try: - nio = iou_instance.slot_remove_nio_binding(slot, port) - if isinstance(nio, NIO_UDP) and nio.lport in self._allocated_udp_ports: - self._allocated_udp_ports.remove(nio.lport) - except IOUError as e: - self.send_custom_error(str(e)) - return - - self.send_response(True) - - @IModule.route("iou.start_capture") - def start_capture(self, request): - """ - Starts a packet capture. - - Mandatory request parameters: - - id (vm identifier) - - slot (slot number) - - port (port number) - - port_id (port identifier) - - capture_file_name - - Optional request parameters: - - data_link_type (PCAP DLT_* value) - - Response parameters: - - port_id (port identifier) - - capture_file_path (path to the capture file) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, IOU_START_CAPTURE_SCHEMA): - return - - # get the instance - iou_instance = self.get_iou_instance(request["id"]) - if not iou_instance: - return - - slot = request["slot"] - port = request["port"] - capture_file_name = request["capture_file_name"] - data_link_type = request.get("data_link_type") - - try: - capture_file_path = os.path.join(self._working_dir, "captures", capture_file_name) - iou_instance.start_capture(slot, port, capture_file_path, data_link_type) - except IOUError as e: - self.send_custom_error(str(e)) - return - - response = {"port_id": request["port_id"], - "capture_file_path": capture_file_path} - self.send_response(response) - - @IModule.route("iou.stop_capture") - def stop_capture(self, request): - """ - Stops a packet capture. - - Mandatory request parameters: - - id (vm identifier) - - slot (slot number) - - port (port number) - - port_id (port identifier) - - Response parameters: - - port_id (port identifier) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, IOU_STOP_CAPTURE_SCHEMA): - return - - # get the instance - iou_instance = self.get_iou_instance(request["id"]) - if not iou_instance: - return - - slot = request["slot"] - port = request["port"] - try: - iou_instance.stop_capture(slot, port) - except IOUError as e: - self.send_custom_error(str(e)) - return - - response = {"port_id": request["port_id"]} - self.send_response(response) - - @IModule.route("iou.export_config") - def export_config(self, request): - """ - Exports the initial-config from an IOU instance. - - Mandatory request parameters: - - id (vm identifier) - - Response parameters: - - initial_config_base64 (initial-config base64 encoded) - - False if no configuration can be exported - """ - - # validate the request - if not self.validate_request(request, IOU_EXPORT_CONFIG_SCHEMA): - return - - # get the instance - iou_instance = self.get_iou_instance(request["id"]) - if not iou_instance: - return - - if not iou_instance.initial_config: - self.send_custom_error("unable to export the initial-config because it doesn't exist") - return - - response = {} - initial_config_path = os.path.join(iou_instance.working_dir, iou_instance.initial_config) - try: - with open(initial_config_path, "rb") as f: - config = f.read() - response["initial_config_base64"] = base64.encodebytes(config).decode("utf-8") - except OSError as e: - self.send_custom_error("unable to export the initial-config: {}".format(e)) - return - - if not response: - self.send_response(False) - else: - self.send_response(response) - - @IModule.route("iou.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/old_modules/iou/adapters/__init__.py b/gns3server/old_modules/iou/adapters/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/gns3server/old_modules/iou/adapters/adapter.py b/gns3server/old_modules/iou/adapters/adapter.py deleted file mode 100644 index 06645e56..00000000 --- a/gns3server/old_modules/iou/adapters/adapter.py +++ /dev/null @@ -1,105 +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 . - - -class Adapter(object): - - """ - Base class for adapters. - - :param interfaces: number of interfaces supported by this adapter. - """ - - def __init__(self, interfaces=4): - - self._interfaces = interfaces - - self._ports = {} - for port_id in range(0, interfaces): - self._ports[port_id] = None - - def removable(self): - """ - Returns True if the adapter can be removed from a slot - and False if not. - - :returns: boolean - """ - - return True - - def port_exists(self, port_id): - """ - Checks if a port exists on this adapter. - - :returns: True is the port exists, - False otherwise. - """ - - if port_id in self._ports: - return True - return False - - def add_nio(self, port_id, nio): - """ - Adds a NIO to a port on this adapter. - - :param port_id: port ID (integer) - :param nio: NIO instance - """ - - self._ports[port_id] = nio - - def remove_nio(self, port_id): - """ - Removes a NIO from a port on this adapter. - - :param port_id: port ID (integer) - """ - - self._ports[port_id] = None - - def get_nio(self, port_id): - """ - Returns the NIO assigned to a port. - - :params port_id: port ID (integer) - - :returns: NIO instance - """ - - return self._ports[port_id] - - @property - def ports(self): - """ - Returns port to NIO mapping - - :returns: dictionary port -> NIO - """ - - return self._ports - - @property - def interfaces(self): - """ - Returns the number of interfaces supported by this adapter. - - :returns: number of interfaces - """ - - return self._interfaces diff --git a/gns3server/old_modules/iou/adapters/ethernet_adapter.py b/gns3server/old_modules/iou/adapters/ethernet_adapter.py deleted file mode 100644 index bf96362f..00000000 --- a/gns3server/old_modules/iou/adapters/ethernet_adapter.py +++ /dev/null @@ -1,32 +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 . - -from .adapter import Adapter - - -class EthernetAdapter(Adapter): - - """ - IOU Ethernet adapter. - """ - - def __init__(self): - Adapter.__init__(self, interfaces=4) - - def __str__(self): - - return "IOU Ethernet adapter" diff --git a/gns3server/old_modules/iou/adapters/serial_adapter.py b/gns3server/old_modules/iou/adapters/serial_adapter.py deleted file mode 100644 index ca7d3200..00000000 --- a/gns3server/old_modules/iou/adapters/serial_adapter.py +++ /dev/null @@ -1,32 +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 . - -from .adapter import Adapter - - -class SerialAdapter(Adapter): - - """ - IOU Serial adapter. - """ - - def __init__(self): - Adapter.__init__(self, interfaces=4) - - def __str__(self): - - return "IOU Serial adapter" diff --git a/gns3server/old_modules/iou/iou_device.py b/gns3server/old_modules/iou/iou_device.py deleted file mode 100644 index ff8ff2c3..00000000 --- a/gns3server/old_modules/iou/iou_device.py +++ /dev/null @@ -1,1069 +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 . - -""" -IOU device management (creates command line, processes, files etc.) in -order to run an IOU instance. -""" - -import os -import re -import signal -import subprocess -import argparse -import threading -import configparser -import shutil - -from .ioucon import start_ioucon -from .iou_error import IOUError -from .adapters.ethernet_adapter import EthernetAdapter -from .adapters.serial_adapter import SerialAdapter -from .nios.nio_udp import NIO_UDP -from .nios.nio_tap import NIO_TAP -from .nios.nio_generic_ethernet import NIO_GenericEthernet -from ..attic import find_unused_port - -import logging -log = logging.getLogger(__name__) - - -class IOUDevice(object): - - """ - IOU device implementation. - - :param name: name of this IOU device - :param path: path to IOU executable - :param working_dir: path to a working directory - :param iou_id: IOU instance ID - :param console: TCP console port - :param console_host: IP address to bind for console connections - :param console_start_port_range: TCP console port range start - :param console_end_port_range: TCP console port range end - """ - - _instances = [] - _allocated_console_ports = [] - - def __init__(self, - name, - path, - working_dir, - iou_id=None, - console=None, - console_host="0.0.0.0", - console_start_port_range=4001, - console_end_port_range=4512): - - if not iou_id: - # find an instance identifier if none is provided (0 < id <= 512) - self._id = 0 - for identifier in range(1, 513): - if identifier not in self._instances: - self._id = identifier - self._instances.append(self._id) - break - - if self._id == 0: - raise IOUError("Maximum number of IOU instances reached") - else: - if iou_id in self._instances: - raise IOUError("IOU identifier {} is already used by another IOU device".format(iou_id)) - self._id = iou_id - self._instances.append(self._id) - - self._name = name - self._path = path - self._iourc = "" - self._iouyap = "" - self._console = console - self._working_dir = None - self._command = [] - self._process = None - self._iouyap_process = None - self._iou_stdout_file = "" - self._iouyap_stdout_file = "" - self._ioucon_thead = None - self._ioucon_thread_stop_event = None - self._started = False - self._console_host = console_host - self._console_start_port_range = console_start_port_range - self._console_end_port_range = console_end_port_range - - # IOU settings - self._ethernet_adapters = [EthernetAdapter(), EthernetAdapter()] # one adapter = 4 interfaces - self._serial_adapters = [SerialAdapter(), SerialAdapter()] # one adapter = 4 interfaces - self._slots = self._ethernet_adapters + self._serial_adapters - self._use_default_iou_values = True # for RAM & NVRAM values - self._nvram = 128 # Kilobytes - self._initial_config = "" - self._ram = 256 # Megabytes - self._l1_keepalives = False # used to overcome the always-up Ethernet interfaces (not supported by all IOSes). - - working_dir_path = os.path.join(working_dir, "iou", "device-{}".format(self._id)) - - if iou_id and not os.path.isdir(working_dir_path): - raise IOUError("Working directory {} doesn't exist".format(working_dir_path)) - - # create the device own working directory - self.working_dir = working_dir_path - - if not self._console: - # allocate a console port - try: - self._console = find_unused_port(self._console_start_port_range, - self._console_end_port_range, - self._console_host, - ignore_ports=self._allocated_console_ports) - except Exception as e: - raise IOUError(e) - - if self._console in self._allocated_console_ports: - raise IOUError("Console port {} is already in used another IOU device".format(console)) - self._allocated_console_ports.append(self._console) - - log.info("IOU device {name} [id={id}] has been created".format(name=self._name, - id=self._id)) - - def defaults(self): - """ - Returns all the default attribute values for IOU. - - :returns: default values (dictionary) - """ - - iou_defaults = {"name": self._name, - "path": self._path, - "intial_config": self._initial_config, - "use_default_iou_values": self._use_default_iou_values, - "ram": self._ram, - "nvram": self._nvram, - "ethernet_adapters": len(self._ethernet_adapters), - "serial_adapters": len(self._serial_adapters), - "console": self._console, - "l1_keepalives": self._l1_keepalives} - - return iou_defaults - - @property - def id(self): - """ - Returns the unique ID for this IOU device. - - :returns: id (integer) - """ - - return self._id - - @classmethod - def reset(cls): - """ - Resets allocated instance list. - """ - - cls._instances.clear() - cls._allocated_console_ports.clear() - - @property - def name(self): - """ - Returns the name of this IOU device. - - :returns: name - """ - - return self._name - - @name.setter - def name(self, new_name): - """ - Sets the name of this IOU device. - - :param new_name: name - """ - - if self._initial_config: - # update the initial-config - config_path = os.path.join(self._working_dir, "initial-config.cfg") - if os.path.isfile(config_path): - try: - with open(config_path, "r+", errors="replace") as f: - old_config = f.read() - new_config = old_config.replace(self._name, new_name) - f.seek(0) - f.write(new_config) - except OSError as e: - raise IOUError("Could not amend the configuration {}: {}".format(config_path, e)) - - log.info("IOU {name} [id={id}]: renamed to {new_name}".format(name=self._name, - id=self._id, - new_name=new_name)) - self._name = new_name - - @property - def path(self): - """ - Returns the path to the IOU executable. - - :returns: path to IOU - """ - - return self._path - - @path.setter - def path(self, path): - """ - Sets the path to the IOU executable. - - :param path: path to IOU - """ - - self._path = path - log.info("IOU {name} [id={id}]: path changed to {path}".format(name=self._name, - id=self._id, - path=path)) - - @property - def iourc(self): - """ - Returns the path to the iourc file. - - :returns: path to the iourc file - """ - - return self._iourc - - @iourc.setter - def iourc(self, iourc): - """ - Sets the path to the iourc file. - - :param iourc: path to the iourc file. - """ - - self._iourc = iourc - log.info("IOU {name} [id={id}]: iourc file path set to {path}".format(name=self._name, - id=self._id, - path=self._iourc)) - - @property - def iouyap(self): - """ - Returns the path to iouyap - - :returns: path to iouyap - """ - - return self._iouyap - - @iouyap.setter - def iouyap(self, iouyap): - """ - Sets the path to iouyap. - - :param iouyap: path to iouyap - """ - - self._iouyap = iouyap - log.info("IOU {name} [id={id}]: iouyap path set to {path}".format(name=self._name, - id=self._id, - path=self._iouyap)) - - @property - def working_dir(self): - """ - Returns current working directory - - :returns: path to the working directory - """ - - return self._working_dir - - @working_dir.setter - def working_dir(self, working_dir): - """ - Sets the working directory for IOU. - - :param working_dir: path to the working directory - """ - - try: - os.makedirs(working_dir) - except FileExistsError: - pass - except OSError as e: - raise IOUError("Could not create working directory {}: {}".format(working_dir, e)) - - self._working_dir = working_dir - log.info("IOU {name} [id={id}]: working directory changed to {wd}".format(name=self._name, - id=self._id, - wd=self._working_dir)) - - @property - def console(self): - """ - Returns the TCP console port. - - :returns: console port (integer) - """ - - return self._console - - @console.setter - def console(self, console): - """ - Sets the TCP console port. - - :param console: console port (integer) - """ - - if console in self._allocated_console_ports: - raise IOUError("Console port {} is already used by another IOU device".format(console)) - - self._allocated_console_ports.remove(self._console) - self._console = console - self._allocated_console_ports.append(self._console) - log.info("IOU {name} [id={id}]: console port set to {port}".format(name=self._name, - id=self._id, - port=console)) - - def command(self): - """ - Returns the IOU command line. - - :returns: IOU command line (string) - """ - - return " ".join(self._build_command()) - - def delete(self): - """ - Deletes this IOU device. - """ - - self.stop() - if self._id in self._instances: - self._instances.remove(self._id) - - if self.console and self.console in self._allocated_console_ports: - self._allocated_console_ports.remove(self.console) - - log.info("IOU device {name} [id={id}] has been deleted".format(name=self._name, - id=self._id)) - - def clean_delete(self): - """ - Deletes this IOU device & all files (nvram, initial-config etc.) - """ - - self.stop() - if self._id in self._instances: - self._instances.remove(self._id) - - if self.console: - self._allocated_console_ports.remove(self.console) - - try: - shutil.rmtree(self._working_dir) - except OSError as e: - log.error("could not delete IOU device {name} [id={id}]: {error}".format(name=self._name, - id=self._id, - error=e)) - return - - log.info("IOU device {name} [id={id}] has been deleted (including associated files)".format(name=self._name, - id=self._id)) - - @property - def started(self): - """ - Returns either this IOU device has been started or not. - - :returns: boolean - """ - - return self._started - - def _update_iouyap_config(self): - """ - Updates the iouyap.ini file. - """ - - iouyap_ini = os.path.join(self._working_dir, "iouyap.ini") - - config = configparser.ConfigParser() - config["default"] = {"netmap": "NETMAP", - "base_port": "49000"} - - bay_id = 0 - for adapter in self._slots: - unit_id = 0 - for unit in adapter.ports.keys(): - nio = adapter.get_nio(unit) - if nio: - connection = None - if isinstance(nio, NIO_UDP): - # UDP tunnel - connection = {"tunnel_udp": "{lport}:{rhost}:{rport}".format(lport=nio.lport, - rhost=nio.rhost, - rport=nio.rport)} - elif isinstance(nio, NIO_TAP): - # TAP interface - connection = {"tap_dev": "{tap_device}".format(tap_device=nio.tap_device)} - - elif isinstance(nio, NIO_GenericEthernet): - # Ethernet interface - connection = {"eth_dev": "{ethernet_device}".format(ethernet_device=nio.ethernet_device)} - - if connection: - interface = "{iouyap_id}:{bay}/{unit}".format(iouyap_id=str(self._id + 512), bay=bay_id, unit=unit_id) - config[interface] = connection - - if nio.capturing: - pcap_data_link_type = nio.pcap_data_link_type.upper() - if pcap_data_link_type == "DLT_PPP_SERIAL": - pcap_protocol = "ppp" - elif pcap_data_link_type == "DLT_C_HDLC": - pcap_protocol = "hdlc" - elif pcap_data_link_type == "DLT_FRELAY": - pcap_protocol = "fr" - else: - pcap_protocol = "ethernet" - capture_info = {"pcap_file": "{pcap_file}".format(pcap_file=nio.pcap_output_file), - "pcap_protocol": pcap_protocol, - "pcap_overwrite": "y"} - config[interface].update(capture_info) - - unit_id += 1 - bay_id += 1 - - try: - with open(iouyap_ini, "w") as config_file: - config.write(config_file) - log.info("IOU {name} [id={id}]: iouyap.ini updated".format(name=self._name, - id=self._id)) - except OSError as e: - raise IOUError("Could not create {}: {}".format(iouyap_ini, e)) - - def _create_netmap_config(self): - """ - Creates the NETMAP file. - """ - - netmap_path = os.path.join(self._working_dir, "NETMAP") - try: - with open(netmap_path, "w") as f: - for bay in range(0, 16): - for unit in range(0, 4): - f.write("{iouyap_id}:{bay}/{unit}{iou_id:>5d}:{bay}/{unit}\n".format(iouyap_id=str(self._id + 512), - bay=bay, - unit=unit, - iou_id=self._id)) - log.info("IOU {name} [id={id}]: NETMAP file created".format(name=self._name, - id=self._id)) - except OSError as e: - raise IOUError("Could not create {}: {}".format(netmap_path, e)) - - def _start_ioucon(self): - """ - Starts ioucon thread (for console connections). - """ - - if not self._ioucon_thead: - telnet_server = "{}:{}".format(self._console_host, self.console) - log.info("starting ioucon for IOU instance {} to accept Telnet connections on {}".format(self._name, telnet_server)) - args = argparse.Namespace(appl_id=str(self._id), debug=False, escape='^^', telnet_limit=0, telnet_server=telnet_server) - self._ioucon_thread_stop_event = threading.Event() - self._ioucon_thead = threading.Thread(target=start_ioucon, args=(args, self._ioucon_thread_stop_event)) - self._ioucon_thead.start() - - def _start_iouyap(self): - """ - Starts iouyap (handles connections to and from this IOU device). - """ - - try: - self._update_iouyap_config() - command = [self._iouyap, "-q", str(self._id + 512)] # iouyap has always IOU ID + 512 - log.info("starting iouyap: {}".format(command)) - self._iouyap_stdout_file = os.path.join(self._working_dir, "iouyap.log") - log.info("logging to {}".format(self._iouyap_stdout_file)) - with open(self._iouyap_stdout_file, "w") as fd: - self._iouyap_process = subprocess.Popen(command, - stdout=fd, - stderr=subprocess.STDOUT, - cwd=self._working_dir) - - log.info("iouyap started PID={}".format(self._iouyap_process.pid)) - except (OSError, subprocess.SubprocessError) as e: - iouyap_stdout = self.read_iouyap_stdout() - log.error("could not start iouyap: {}\n{}".format(e, iouyap_stdout)) - raise IOUError("Could not start iouyap: {}\n{}".format(e, iouyap_stdout)) - - def _library_check(self): - """ - Checks for missing shared library dependencies in the IOU image. - """ - - try: - output = subprocess.check_output(["ldd", self._path]) - except (FileNotFoundError, subprocess.SubprocessError) as e: - log.warn("could not determine the shared library dependencies for {}: {}".format(self._path, e)) - return - - p = re.compile("([\.\w]+)\s=>\s+not found") - missing_libs = p.findall(output.decode("utf-8")) - if missing_libs: - raise IOUError("The following shared library dependencies cannot be found for IOU image {}: {}".format(self._path, - ", ".join(missing_libs))) - - def start(self): - """ - Starts the IOU process. - """ - - if not self.is_running(): - - if not os.path.isfile(self._path) or not os.path.exists(self._path): - if os.path.islink(self._path): - raise IOUError("IOU image '{}' linked to '{}' is not accessible".format(self._path, os.path.realpath(self._path))) - else: - raise IOUError("IOU image '{}' is not accessible".format(self._path)) - - try: - with open(self._path, "rb") as f: - # read the first 7 bytes of the file. - elf_header_start = f.read(7) - except OSError as e: - raise IOUError("Cannot read ELF header for IOU image '{}': {}".format(self._path, e)) - - # IOU images must start with the ELF magic number, be 32-bit, little endian - # and have an ELF version of 1 normal IOS image are big endian! - if elf_header_start != b'\x7fELF\x01\x01\x01': - raise IOUError("'{}' is not a valid IOU image".format(self._path)) - - if not os.access(self._path, os.X_OK): - raise IOUError("IOU image '{}' is not executable".format(self._path)) - - self._library_check() - - if not self._iourc or not os.path.isfile(self._iourc): - raise IOUError("A valid iourc file is necessary to start IOU") - - if not self._iouyap or not os.path.isfile(self._iouyap): - raise IOUError("iouyap is necessary to start IOU") - - self._create_netmap_config() - # created a environment variable pointing to the iourc file. - env = os.environ.copy() - env["IOURC"] = self._iourc - self._command = self._build_command() - try: - log.info("starting IOU: {}".format(self._command)) - self._iou_stdout_file = os.path.join(self._working_dir, "iou.log") - log.info("logging to {}".format(self._iou_stdout_file)) - with open(self._iou_stdout_file, "w") as fd: - self._process = subprocess.Popen(self._command, - stdout=fd, - stderr=subprocess.STDOUT, - cwd=self._working_dir, - env=env) - log.info("IOU instance {} started PID={}".format(self._id, self._process.pid)) - self._started = True - except FileNotFoundError as e: - raise IOUError("could not start IOU: {}: 32-bit binary support is probably not installed".format(e)) - except (OSError, subprocess.SubprocessError) as e: - iou_stdout = self.read_iou_stdout() - log.error("could not start IOU {}: {}\n{}".format(self._path, e, iou_stdout)) - raise IOUError("could not start IOU {}: {}\n{}".format(self._path, e, iou_stdout)) - - # start console support - self._start_ioucon() - # connections support - self._start_iouyap() - - def stop(self): - """ - Stops the IOU process. - """ - - # stop console support - if self._ioucon_thead: - self._ioucon_thread_stop_event.set() - if self._ioucon_thead.is_alive(): - self._ioucon_thead.join(timeout=3.0) # wait for the thread to free the console port - self._ioucon_thead = None - - # stop iouyap - if self.is_iouyap_running(): - log.info("stopping iouyap PID={} for IOU instance {}".format(self._iouyap_process.pid, self._id)) - try: - self._iouyap_process.terminate() - self._iouyap_process.wait(1) - except subprocess.TimeoutExpired: - self._iouyap_process.kill() - if self._iouyap_process.poll() is None: - log.warn("iouyap PID={} for IOU instance {} is still running".format(self._iouyap_process.pid, - self._id)) - self._iouyap_process = None - - # stop the IOU process - if self.is_running(): - log.info("stopping IOU instance {} PID={}".format(self._id, self._process.pid)) - try: - self._process.terminate() - self._process.wait(1) - except subprocess.TimeoutExpired: - self._process.kill() - if self._process.poll() is None: - log.warn("IOU instance {} PID={} is still running".format(self._id, - self._process.pid)) - self._process = None - self._started = False - - def read_iou_stdout(self): - """ - Reads the standard output of the IOU process. - Only use when the process has been stopped or has crashed. - """ - - output = "" - if self._iou_stdout_file: - try: - with open(self._iou_stdout_file, errors="replace") as file: - output = file.read() - except OSError as e: - log.warn("could not read {}: {}".format(self._iou_stdout_file, e)) - return output - - def read_iouyap_stdout(self): - """ - Reads the standard output of the iouyap process. - Only use when the process has been stopped or has crashed. - """ - - output = "" - if self._iouyap_stdout_file: - try: - with open(self._iouyap_stdout_file, errors="replace") as file: - output = file.read() - except OSError as e: - log.warn("could not read {}: {}".format(self._iouyap_stdout_file, e)) - return output - - def is_running(self): - """ - Checks if the IOU process is running - - :returns: True or False - """ - - if self._process and self._process.poll() is None: - return True - return False - - def is_iouyap_running(self): - """ - Checks if the iouyap process is running - - :returns: True or False - """ - - if self._iouyap_process and self._iouyap_process.poll() is None: - return True - return False - - def slot_add_nio_binding(self, slot_id, port_id, nio): - """ - Adds a slot NIO binding. - - :param slot_id: slot ID - :param port_id: port ID - :param nio: NIO instance to add to the slot/port - """ - - try: - adapter = self._slots[slot_id] - except IndexError: - raise IOUError("Slot {slot_id} doesn't exist on IOU {name}".format(name=self._name, - slot_id=slot_id)) - - if not adapter.port_exists(port_id): - raise IOUError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, - port_id=port_id)) - - adapter.add_nio(port_id, nio) - log.info("IOU {name} [id={id}]: {nio} added to {slot_id}/{port_id}".format(name=self._name, - id=self._id, - nio=nio, - slot_id=slot_id, - port_id=port_id)) - if self.is_iouyap_running(): - self._update_iouyap_config() - os.kill(self._iouyap_process.pid, signal.SIGHUP) - - def slot_remove_nio_binding(self, slot_id, port_id): - """ - Removes a slot NIO binding. - - :param slot_id: slot ID - :param port_id: port ID - - :returns: NIO instance - """ - - try: - adapter = self._slots[slot_id] - except IndexError: - raise IOUError("Slot {slot_id} doesn't exist on IOU {name}".format(name=self._name, - slot_id=slot_id)) - - if not adapter.port_exists(port_id): - raise IOUError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, - port_id=port_id)) - - nio = adapter.get_nio(port_id) - adapter.remove_nio(port_id) - log.info("IOU {name} [id={id}]: {nio} removed from {slot_id}/{port_id}".format(name=self._name, - id=self._id, - nio=nio, - slot_id=slot_id, - port_id=port_id)) - if self.is_iouyap_running(): - self._update_iouyap_config() - os.kill(self._iouyap_process.pid, signal.SIGHUP) - - return nio - - def _enable_l1_keepalives(self, command): - """ - Enables L1 keepalive messages if supported. - - :param command: command line - """ - - env = os.environ.copy() - env["IOURC"] = self._iourc - try: - output = subprocess.check_output([self._path, "-h"], stderr=subprocess.STDOUT, cwd=self._working_dir, env=env) - if re.search("-l\s+Enable Layer 1 keepalive messages", output.decode("utf-8")): - command.extend(["-l"]) - else: - raise IOUError("layer 1 keepalive messages are not supported by {}".format(os.path.basename(self._path))) - except (OSError, subprocess.SubprocessError) as e: - log.warn("could not determine if layer 1 keepalive messages are supported by {}: {}".format(os.path.basename(self._path), e)) - - def _build_command(self): - """ - Command to start the IOU process. - (to be passed to subprocess.Popen()) - - IOU command line: - Usage: [options] - : unix-js-m | unix-is-m | unix-i-m | ... - : instance identifier (0 < id <= 1024) - Options: - -e Number of Ethernet interfaces (default 2) - -s Number of Serial interfaces (default 2) - -n Size of nvram in Kb (default 64KB) - -b IOS debug string - -c Configuration file name - -d Generate debug information - -t Netio message trace - -q Suppress informational messages - -h Display this help - -C Turn off use of host clock - -m Megabytes of router memory (default 256MB) - -L Disable local console, use remote console - -l Enable Layer 1 keepalive messages - -u UDP port base for distributed networks - -R Ignore options from the IOURC file - -U Disable unix: file system location - -W Disable watchdog timer - -N Ignore the NETMAP file - """ - - command = [self._path] - if len(self._ethernet_adapters) != 2: - command.extend(["-e", str(len(self._ethernet_adapters))]) - if len(self._serial_adapters) != 2: - command.extend(["-s", str(len(self._serial_adapters))]) - if not self.use_default_iou_values: - command.extend(["-n", str(self._nvram)]) - command.extend(["-m", str(self._ram)]) - command.extend(["-L"]) # disable local console, use remote console - if self._initial_config: - command.extend(["-c", self._initial_config]) - if self._l1_keepalives: - self._enable_l1_keepalives(command) - command.extend([str(self._id)]) - return command - - @property - def use_default_iou_values(self): - """ - Returns if this device uses the default IOU image values. - - :returns: boolean - """ - - return self._use_default_iou_values - - @use_default_iou_values.setter - def use_default_iou_values(self, state): - """ - Sets if this device uses the default IOU image values. - - :param state: boolean - """ - - self._use_default_iou_values = state - if state: - log.info("IOU {name} [id={id}]: uses the default IOU image values".format(name=self._name, id=self._id)) - else: - log.info("IOU {name} [id={id}]: does not use the default IOU image values".format(name=self._name, id=self._id)) - - @property - def l1_keepalives(self): - """ - Returns either layer 1 keepalive messages option is enabled or disabled. - - :returns: boolean - """ - - return self._l1_keepalives - - @l1_keepalives.setter - def l1_keepalives(self, state): - """ - Enables or disables layer 1 keepalive messages. - - :param state: boolean - """ - - self._l1_keepalives = state - if state: - log.info("IOU {name} [id={id}]: has activated layer 1 keepalive messages".format(name=self._name, id=self._id)) - else: - log.info("IOU {name} [id={id}]: has deactivated layer 1 keepalive messages".format(name=self._name, id=self._id)) - - @property - def ram(self): - """ - Returns the amount of RAM allocated to this IOU instance. - - :returns: amount of RAM in Mbytes (integer) - """ - - return self._ram - - @ram.setter - def ram(self, ram): - """ - Sets amount of RAM allocated to this IOU instance. - - :param ram: amount of RAM in Mbytes (integer) - """ - - if self._ram == ram: - return - - log.info("IOU {name} [id={id}]: RAM updated from {old_ram}MB to {new_ram}MB".format(name=self._name, - id=self._id, - old_ram=self._ram, - new_ram=ram)) - - self._ram = ram - - @property - def nvram(self): - """ - Returns the mount of NVRAM allocated to this IOU instance. - - :returns: amount of NVRAM in Kbytes (integer) - """ - - return self._nvram - - @nvram.setter - def nvram(self, nvram): - """ - Sets amount of NVRAM allocated to this IOU instance. - - :param nvram: amount of NVRAM in Kbytes (integer) - """ - - if self._nvram == nvram: - return - - log.info("IOU {name} [id={id}]: NVRAM updated from {old_nvram}KB to {new_nvram}KB".format(name=self._name, - id=self._id, - old_nvram=self._nvram, - new_nvram=nvram)) - self._nvram = nvram - - @property - def initial_config(self): - """ - Returns the initial-config for this IOU instance. - - :returns: path to initial-config file - """ - - return self._initial_config - - @initial_config.setter - def initial_config(self, initial_config): - """ - Sets the initial-config for this IOU instance. - - :param initial_config: path to initial-config file - """ - - self._initial_config = initial_config - log.info("IOU {name} [id={id}]: initial_config set to {config}".format(name=self._name, - id=self._id, - config=self._initial_config)) - - @property - def ethernet_adapters(self): - """ - Returns the number of Ethernet adapters for this IOU instance. - - :returns: number of adapters - """ - - return len(self._ethernet_adapters) - - @ethernet_adapters.setter - def ethernet_adapters(self, ethernet_adapters): - """ - Sets the number of Ethernet adapters for this IOU instance. - - :param ethernet_adapters: number of adapters - """ - - self._ethernet_adapters.clear() - for _ in range(0, ethernet_adapters): - self._ethernet_adapters.append(EthernetAdapter()) - - log.info("IOU {name} [id={id}]: number of Ethernet adapters changed to {adapters}".format(name=self._name, - id=self._id, - adapters=len(self._ethernet_adapters))) - - self._slots = self._ethernet_adapters + self._serial_adapters - - @property - def serial_adapters(self): - """ - Returns the number of Serial adapters for this IOU instance. - - :returns: number of adapters - """ - - return len(self._serial_adapters) - - @serial_adapters.setter - def serial_adapters(self, serial_adapters): - """ - Sets the number of Serial adapters for this IOU instance. - - :param serial_adapters: number of adapters - """ - - self._serial_adapters.clear() - for _ in range(0, serial_adapters): - self._serial_adapters.append(SerialAdapter()) - - log.info("IOU {name} [id={id}]: number of Serial adapters changed to {adapters}".format(name=self._name, - id=self._id, - adapters=len(self._serial_adapters))) - - self._slots = self._ethernet_adapters + self._serial_adapters - - def start_capture(self, slot_id, port_id, output_file, data_link_type="DLT_EN10MB"): - """ - Starts a packet capture. - - :param slot_id: slot ID - :param port_id: port ID - :param port: allocated port - :param output_file: PCAP destination file for the capture - :param data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB - """ - - try: - adapter = self._slots[slot_id] - except IndexError: - raise IOUError("Slot {slot_id} doesn't exist on IOU {name}".format(name=self._name, - slot_id=slot_id)) - - if not adapter.port_exists(port_id): - raise IOUError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, - port_id=port_id)) - - nio = adapter.get_nio(port_id) - if nio.capturing: - raise IOUError("Packet capture is already activated on {slot_id}/{port_id}".format(slot_id=slot_id, - port_id=port_id)) - - try: - os.makedirs(os.path.dirname(output_file)) - except FileExistsError: - pass - except OSError as e: - raise IOUError("Could not create captures directory {}".format(e)) - - nio.startPacketCapture(output_file, data_link_type) - - log.info("IOU {name} [id={id}]: starting packet capture on {slot_id}/{port_id}".format(name=self._name, - id=self._id, - slot_id=slot_id, - port_id=port_id)) - - if self.is_iouyap_running(): - self._update_iouyap_config() - os.kill(self._iouyap_process.pid, signal.SIGHUP) - - def stop_capture(self, slot_id, port_id): - """ - Stops a packet capture. - - :param slot_id: slot ID - :param port_id: port ID - """ - - try: - adapter = self._slots[slot_id] - except IndexError: - raise IOUError("Slot {slot_id} doesn't exist on IOU {name}".format(name=self._name, - slot_id=slot_id)) - - if not adapter.port_exists(port_id): - raise IOUError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, - port_id=port_id)) - - nio = adapter.get_nio(port_id) - nio.stopPacketCapture() - log.info("IOU {name} [id={id}]: stopping packet capture on {slot_id}/{port_id}".format(name=self._name, - id=self._id, - slot_id=slot_id, - port_id=port_id)) - if self.is_iouyap_running(): - self._update_iouyap_config() - os.kill(self._iouyap_process.pid, signal.SIGHUP) diff --git a/gns3server/old_modules/iou/iou_error.py b/gns3server/old_modules/iou/iou_error.py deleted file mode 100644 index 8aac176f..00000000 --- a/gns3server/old_modules/iou/iou_error.py +++ /dev/null @@ -1,39 +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 . - -""" -Custom exceptions for IOU module. -""" - - -class IOUError(Exception): - - def __init__(self, message, original_exception=None): - - Exception.__init__(self, message) - if isinstance(message, Exception): - message = str(message) - self._message = message - self._original_exception = original_exception - - def __repr__(self): - - return self._message - - def __str__(self): - - return self._message diff --git a/gns3server/old_modules/iou/nios/__init__.py b/gns3server/old_modules/iou/nios/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/gns3server/old_modules/iou/nios/nio.py b/gns3server/old_modules/iou/nios/nio.py deleted file mode 100644 index 0c8e610e..00000000 --- a/gns3server/old_modules/iou/nios/nio.py +++ /dev/null @@ -1,80 +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 . - -""" -Base interface for NIOs. -""" - - -class NIO(object): - - """ - Network Input/Output. - """ - - def __init__(self): - - self._capturing = False - self._pcap_output_file = "" - self._pcap_data_link_type = "" - - def startPacketCapture(self, pcap_output_file, pcap_data_link_type="DLT_EN10MB"): - """ - - :param pcap_output_file: PCAP destination file for the capture - :param pcap_data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB - """ - - self._capturing = True - self._pcap_output_file = pcap_output_file - self._pcap_data_link_type = pcap_data_link_type - - def stopPacketCapture(self): - - self._capturing = False - self._pcap_output_file = "" - self._pcap_data_link_type = "" - - @property - def capturing(self): - """ - Returns either a capture is configured on this NIO. - - :returns: boolean - """ - - return self._capturing - - @property - def pcap_output_file(self): - """ - Returns the path to the PCAP output file. - - :returns: path to the PCAP output file - """ - - return self._pcap_output_file - - @property - def pcap_data_link_type(self): - """ - Returns the PCAP data link type - - :returns: PCAP data link type (DLT_* value) - """ - - return self._pcap_data_link_type diff --git a/gns3server/old_modules/iou/nios/nio_generic_ethernet.py b/gns3server/old_modules/iou/nios/nio_generic_ethernet.py deleted file mode 100644 index 709e6474..00000000 --- a/gns3server/old_modules/iou/nios/nio_generic_ethernet.py +++ /dev/null @@ -1,50 +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 . - -""" -Interface for generic Ethernet NIOs (PCAP library). -""" - -from .nio import NIO - - -class NIO_GenericEthernet(NIO): - - """ - Generic Ethernet NIO. - - :param ethernet_device: Ethernet device name (e.g. eth0) - """ - - def __init__(self, ethernet_device): - - NIO.__init__(self) - self._ethernet_device = ethernet_device - - @property - def ethernet_device(self): - """ - Returns the Ethernet device used by this NIO. - - :returns: the Ethernet device name - """ - - return self._ethernet_device - - def __str__(self): - - return "NIO Ethernet" diff --git a/gns3server/old_modules/iou/nios/nio_tap.py b/gns3server/old_modules/iou/nios/nio_tap.py deleted file mode 100644 index f6b1663f..00000000 --- a/gns3server/old_modules/iou/nios/nio_tap.py +++ /dev/null @@ -1,50 +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 . - -""" -Interface for TAP NIOs (UNIX based OSes only). -""" - -from .nio import NIO - - -class NIO_TAP(NIO): - - """ - TAP NIO. - - :param tap_device: TAP device name (e.g. tap0) - """ - - def __init__(self, tap_device): - - NIO.__init__(self) - self._tap_device = tap_device - - @property - def tap_device(self): - """ - Returns the TAP device used by this NIO. - - :returns: the TAP device name - """ - - return self._tap_device - - def __str__(self): - - return "NIO TAP" diff --git a/gns3server/old_modules/iou/nios/nio_udp.py b/gns3server/old_modules/iou/nios/nio_udp.py deleted file mode 100644 index 3b25f0c4..00000000 --- a/gns3server/old_modules/iou/nios/nio_udp.py +++ /dev/null @@ -1,76 +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 . - -""" -Interface for UDP NIOs. -""" - -from .nio import NIO - - -class NIO_UDP(NIO): - - """ - UDP NIO. - - :param lport: local port number - :param rhost: remote address/host - :param rport: remote port number - """ - - _instance_count = 0 - - def __init__(self, lport, rhost, rport): - - NIO.__init__(self) - self._lport = lport - self._rhost = rhost - self._rport = rport - - @property - def lport(self): - """ - Returns the local port - - :returns: local port number - """ - - return self._lport - - @property - def rhost(self): - """ - Returns the remote host - - :returns: remote address/host - """ - - return self._rhost - - @property - def rport(self): - """ - Returns the remote port - - :returns: remote port number - """ - - return self._rport - - def __str__(self): - - return "NIO UDP" diff --git a/gns3server/old_modules/iou/schemas.py b/gns3server/old_modules/iou/schemas.py deleted file mode 100644 index f1315ec3..00000000 --- a/gns3server/old_modules/iou/schemas.py +++ /dev/null @@ -1,472 +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 . - - -IOU_CREATE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to create a new IOU instance", - "type": "object", - "properties": { - "name": { - "description": "IOU device name", - "type": "string", - "minLength": 1, - }, - "iou_id": { - "description": "IOU device instance ID", - "type": "integer" - }, - "console": { - "description": "console TCP port", - "minimum": 1, - "maximum": 65535, - "type": "integer" - }, - "path": { - "description": "path to the IOU executable", - "type": "string", - "minLength": 1, - }, - "cloud_path": { - "description": "Path to the image in the cloud object store", - "type": "string", - } - }, - "additionalProperties": False, - "required": ["name", "path"], -} - -IOU_DELETE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to delete an IOU instance", - "type": "object", - "properties": { - "id": { - "description": "IOU device instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -IOU_UPDATE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to update an IOU instance", - "type": "object", - "properties": { - "id": { - "description": "IOU device instance ID", - "type": "integer" - }, - "name": { - "description": "IOU device name", - "type": "string", - "minLength": 1, - }, - "path": { - "description": "path to the IOU executable", - "type": "string", - "minLength": 1, - }, - "initial_config": { - "description": "path to the IOU initial configuration file", - "type": "string", - "minLength": 1, - }, - "ram": { - "description": "amount of RAM in MB", - "type": "integer" - }, - "nvram": { - "description": "amount of NVRAM in KB", - "type": "integer" - }, - "ethernet_adapters": { - "description": "number of Ethernet adapters", - "type": "integer", - "minimum": 0, - "maximum": 16, - }, - "serial_adapters": { - "description": "number of serial adapters", - "type": "integer", - "minimum": 0, - "maximum": 16, - }, - "console": { - "description": "console TCP port", - "minimum": 1, - "maximum": 65535, - "type": "integer" - }, - "use_default_iou_values": { - "description": "use the default IOU RAM & NVRAM values", - "type": "boolean" - }, - "l1_keepalives": { - "description": "enable or disable layer 1 keepalive messages", - "type": "boolean" - }, - "initial_config_base64": { - "description": "initial configuration base64 encoded", - "type": "string" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -IOU_START_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to start an IOU instance", - "type": "object", - "properties": { - "id": { - "description": "IOU device instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -IOU_STOP_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to stop an IOU instance", - "type": "object", - "properties": { - "id": { - "description": "IOU device instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -IOU_RELOAD_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to reload an IOU instance", - "type": "object", - "properties": { - "id": { - "description": "IOU device instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -IOU_ALLOCATE_UDP_PORT_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to allocate an UDP port for an IOU instance", - "type": "object", - "properties": { - "id": { - "description": "IOU device instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the IOU instance", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id", "port_id"] -} - -IOU_ADD_NIO_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to add a NIO for an IOU 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": "IOU device instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the IOU instance", - "type": "integer" - }, - "slot": { - "description": "Slot number", - "type": "integer", - "minimum": 0, - "maximum": 15 - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 0, - "maximum": 3 - }, - "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", "slot", "port", "nio"] -} - - -IOU_DELETE_NIO_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to delete a NIO for an IOU instance", - "type": "object", - "properties": { - "id": { - "description": "IOU device instance ID", - "type": "integer" - }, - "slot": { - "description": "Slot number", - "type": "integer", - "minimum": 0, - "maximum": 15 - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 0, - "maximum": 3 - }, - }, - "additionalProperties": False, - "required": ["id", "slot", "port"] -} - -IOU_START_CAPTURE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to start a packet capture on an IOU instance port", - "type": "object", - "properties": { - "id": { - "description": "IOU device instance ID", - "type": "integer" - }, - "slot": { - "description": "Slot number", - "type": "integer", - "minimum": 0, - "maximum": 15 - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 0, - "maximum": 3 - }, - "port_id": { - "description": "Unique port identifier for the IOU instance", - "type": "integer" - }, - "capture_file_name": { - "description": "Capture file name", - "type": "string", - "minLength": 1, - }, - "data_link_type": { - "description": "PCAP data link type", - "type": "string", - "minLength": 1, - }, - }, - "additionalProperties": False, - "required": ["id", "slot", "port", "port_id", "capture_file_name"] -} - -IOU_STOP_CAPTURE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to stop a packet capture on an IOU instance port", - "type": "object", - "properties": { - "id": { - "description": "IOU device instance ID", - "type": "integer" - }, - "slot": { - "description": "Slot number", - "type": "integer", - "minimum": 0, - "maximum": 15 - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 0, - "maximum": 3 - }, - "port_id": { - "description": "Unique port identifier for the IOU instance", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id", "slot", "port", "port_id"] -} - -IOU_EXPORT_CONFIG_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to export an initial-config from an IOU instance", - "type": "object", - "properties": { - "id": { - "description": "IOU device instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} diff --git a/gns3server/old_modules/qemu/__init__.py b/gns3server/old_modules/qemu/__init__.py deleted file mode 100644 index 01b3c72e..00000000 --- a/gns3server/old_modules/qemu/__init__.py +++ /dev/null @@ -1,687 +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 . - -""" -QEMU server module. -""" - -import sys -import os -import socket -import shutil -import subprocess -import re - -from gns3server.modules import IModule -from gns3server.config import Config -from .qemu_vm import QemuVM -from .qemu_error import QemuError -from .nios.nio_udp import NIO_UDP -from ..attic import find_unused_port - -from .schemas import QEMU_CREATE_SCHEMA -from .schemas import QEMU_DELETE_SCHEMA -from .schemas import QEMU_UPDATE_SCHEMA -from .schemas import QEMU_START_SCHEMA -from .schemas import QEMU_STOP_SCHEMA -from .schemas import QEMU_SUSPEND_SCHEMA -from .schemas import QEMU_RELOAD_SCHEMA -from .schemas import QEMU_ALLOCATE_UDP_PORT_SCHEMA -from .schemas import QEMU_ADD_NIO_SCHEMA -from .schemas import QEMU_DELETE_NIO_SCHEMA - -import logging -log = logging.getLogger(__name__) - - -class Qemu(IModule): - - """ - QEMU module. - - :param name: module name - :param args: arguments for the module - :param kwargs: named arguments for the module - """ - - def __init__(self, name, *args, **kwargs): - - # a new process start when calling IModule - IModule.__init__(self, name, *args, **kwargs) - self._qemu_instances = {} - - config = Config.instance() - qemu_config = config.get_section_config(name.upper()) - self._console_start_port_range = qemu_config.get("console_start_port_range", 5001) - self._console_end_port_range = qemu_config.get("console_end_port_range", 5500) - self._monitor_start_port_range = qemu_config.get("monitor_start_port_range", 5501) - self._monitor_end_port_range = qemu_config.get("monitor_end_port_range", 6000) - self._allocated_udp_ports = [] - self._udp_start_port_range = qemu_config.get("udp_start_port_range", 40001) - self._udp_end_port_range = qemu_config.get("udp_end_port_range", 45500) - self._host = qemu_config.get("host", kwargs["host"]) - self._console_host = qemu_config.get("console_host", kwargs["console_host"]) - self._monitor_host = qemu_config.get("monitor_host", "127.0.0.1") - 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 QEMU instances - for qemu_id in self._qemu_instances: - qemu_instance = self._qemu_instances[qemu_id] - qemu_instance.delete() - - IModule.stop(self, signum) # this will stop the I/O loop - - def get_qemu_instance(self, qemu_id): - """ - Returns a QEMU VM instance. - - :param qemu_id: QEMU VM identifier - - :returns: QemuVM instance - """ - - if qemu_id not in self._qemu_instances: - log.debug("QEMU VM ID {} doesn't exist".format(qemu_id), exc_info=1) - self.send_custom_error("QEMU VM ID {} doesn't exist".format(qemu_id)) - return None - return self._qemu_instances[qemu_id] - - @IModule.route("qemu.reset") - def reset(self, request): - """ - Resets the module. - - :param request: JSON request - """ - - # delete all QEMU instances - for qemu_id in self._qemu_instances: - qemu_instance = self._qemu_instances[qemu_id] - qemu_instance.delete() - - # resets the instance IDs - QemuVM.reset() - - self._qemu_instances.clear() - self._allocated_udp_ports.clear() - - self._working_dir = self._projects_dir - log.info("QEMU module has been reset") - - @IModule.route("qemu.settings") - def settings(self, request): - """ - Set or update settings. - - Optional request parameters: - - working_dir (path to a working directory) - - project_name - - console_start_port_range - - console_end_port_range - - monitor_start_port_range - - monitor_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 "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 qemu_id in self._qemu_instances: - qemu_instance = self._qemu_instances[qemu_id] - qemu_instance.working_dir = os.path.join(self._working_dir, "qemu", "vm-{}".format(qemu_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 "monitor_start_port_range" in request and "monitor_end_port_range" in request: - self._monitor_start_port_range = request["monitor_start_port_range"] - self._monitor_end_port_range = request["monitor_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("qemu.create") - def qemu_create(self, request): - """ - Creates a new QEMU VM instance. - - Mandatory request parameters: - - name (QEMU VM name) - - qemu_path (path to the Qemu binary) - - Optional request parameters: - - console (QEMU VM console port) - - monitor (QEMU VM monitor port) - - Response parameters: - - id (QEMU VM instance identifier) - - name (QEMU VM name) - - default settings - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, QEMU_CREATE_SCHEMA): - return - - name = request["name"] - qemu_path = request["qemu_path"] - console = request.get("console") - monitor = request.get("monitor") - qemu_id = request.get("qemu_id") - - try: - qemu_instance = QemuVM(name, - qemu_path, - self._working_dir, - self._host, - qemu_id, - console, - self._console_host, - self._console_start_port_range, - self._console_end_port_range, - monitor, - self._monitor_host, - self._monitor_start_port_range, - self._monitor_end_port_range) - - except QemuError as e: - self.send_custom_error(str(e)) - return - - response = {"name": qemu_instance.name, - "id": qemu_instance.id} - - defaults = qemu_instance.defaults() - response.update(defaults) - self._qemu_instances[qemu_instance.id] = qemu_instance - self.send_response(response) - - @IModule.route("qemu.delete") - def qemu_delete(self, request): - """ - Deletes a QEMU VM instance. - - Mandatory request parameters: - - id (QEMU VM instance identifier) - - Response parameter: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, QEMU_DELETE_SCHEMA): - return - - # get the instance - qemu_instance = self.get_qemu_instance(request["id"]) - if not qemu_instance: - return - - try: - qemu_instance.clean_delete() - del self._qemu_instances[request["id"]] - except QemuError as e: - self.send_custom_error(str(e)) - return - - self.send_response(True) - - @IModule.route("qemu.update") - def qemu_update(self, request): - """ - Updates a QEMU VM instance - - Mandatory request parameters: - - id (QEMU VM instance identifier) - - Optional request parameters: - - any setting to update - - Response parameters: - - updated settings - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, QEMU_UPDATE_SCHEMA): - return - - # get the instance - qemu_instance = self.get_qemu_instance(request["id"]) - if not qemu_instance: - return - - # update the QEMU VM settings - response = {} - for name, value in request.items(): - if hasattr(qemu_instance, name) and getattr(qemu_instance, name) != value: - try: - setattr(qemu_instance, name, value) - response[name] = value - except QemuError as e: - self.send_custom_error(str(e)) - return - - self.send_response(response) - - @IModule.route("qemu.start") - def qemu_start(self, request): - """ - Starts a QEMU VM instance. - - Mandatory request parameters: - - id (QEMU VM instance identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, QEMU_START_SCHEMA): - return - - # get the instance - qemu_instance = self.get_qemu_instance(request["id"]) - if not qemu_instance: - return - - try: - qemu_instance.start() - except QemuError as e: - self.send_custom_error(str(e)) - return - self.send_response(True) - - @IModule.route("qemu.stop") - def qemu_stop(self, request): - """ - Stops a QEMU VM instance. - - Mandatory request parameters: - - id (QEMU VM instance identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, QEMU_STOP_SCHEMA): - return - - # get the instance - qemu_instance = self.get_qemu_instance(request["id"]) - if not qemu_instance: - return - - try: - qemu_instance.stop() - except QemuError as e: - self.send_custom_error(str(e)) - return - self.send_response(True) - - @IModule.route("qemu.reload") - def qemu_reload(self, request): - """ - Reloads a QEMU VM instance. - - Mandatory request parameters: - - id (QEMU VM identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, QEMU_RELOAD_SCHEMA): - return - - # get the instance - qemu_instance = self.get_qemu_instance(request["id"]) - if not qemu_instance: - return - - try: - qemu_instance.reload() - except QemuError as e: - self.send_custom_error(str(e)) - return - self.send_response(True) - - @IModule.route("qemu.stop") - def qemu_stop(self, request): - """ - Stops a QEMU VM instance. - - Mandatory request parameters: - - id (QEMU VM instance identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, QEMU_STOP_SCHEMA): - return - - # get the instance - qemu_instance = self.get_qemu_instance(request["id"]) - if not qemu_instance: - return - - try: - qemu_instance.stop() - except QemuError as e: - self.send_custom_error(str(e)) - return - self.send_response(True) - - @IModule.route("qemu.suspend") - def qemu_suspend(self, request): - """ - Suspends a QEMU VM instance. - - Mandatory request parameters: - - id (QEMU VM instance identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, QEMU_SUSPEND_SCHEMA): - return - - # get the instance - qemu_instance = self.get_qemu_instance(request["id"]) - if not qemu_instance: - return - - try: - qemu_instance.suspend() - except QemuError as e: - self.send_custom_error(str(e)) - return - self.send_response(True) - - @IModule.route("qemu.allocate_udp_port") - def allocate_udp_port(self, request): - """ - Allocates a UDP port in order to create an UDP NIO. - - Mandatory request parameters: - - id (QEMU VM 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, QEMU_ALLOCATE_UDP_PORT_SCHEMA): - return - - # get the instance - qemu_instance = self.get_qemu_instance(request["id"]) - if not qemu_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(qemu_instance.name, - qemu_instance.id, - port, - self._host)) - - response = {"lport": port, - "port_id": request["port_id"]} - self.send_response(response) - - @IModule.route("qemu.add_nio") - def add_nio(self, request): - """ - Adds an NIO (Network Input/Output) for a QEMU VM instance. - - Mandatory request parameters: - - id (QEMU VM 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) - - Response parameters: - - port_id (unique port identifier) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, QEMU_ADD_NIO_SCHEMA): - return - - # get the instance - qemu_instance = self.get_qemu_instance(request["id"]) - if not qemu_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 QemuError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e)) - nio = NIO_UDP(lport, rhost, rport) - if not nio: - raise QemuError("Requested NIO does not exist or is not supported: {}".format(request["nio"]["type"])) - except QemuError as e: - self.send_custom_error(str(e)) - return - - try: - qemu_instance.port_add_nio_binding(port, nio) - except QemuError as e: - self.send_custom_error(str(e)) - return - - self.send_response({"port_id": request["port_id"]}) - - @IModule.route("qemu.delete_nio") - def delete_nio(self, request): - """ - Deletes an NIO (Network Input/Output). - - Mandatory request parameters: - - id (QEMU VM instance identifier) - - port (port identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, QEMU_DELETE_NIO_SCHEMA): - return - - # get the instance - qemu_instance = self.get_qemu_instance(request["id"]) - if not qemu_instance: - return - - port = request["port"] - try: - nio = qemu_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 QemuError as e: - self.send_custom_error(str(e)) - return - - self.send_response(True) - - def _get_qemu_version(self, qemu_path): - """ - Gets the Qemu version. - - :param qemu_path: path to Qemu - """ - - if sys.platform.startswith("win"): - return "" - try: - output = subprocess.check_output([qemu_path, "-version"]) - match = re.search("version\s+([0-9a-z\-\.]+)", output.decode("utf-8")) - if match: - version = match.group(1) - return version - else: - raise QemuError("Could not determine the Qemu version for {}".format(qemu_path)) - except subprocess.SubprocessError as e: - raise QemuError("Error while looking for the Qemu version: {}".format(e)) - - @IModule.route("qemu.qemu_list") - def qemu_list(self, request): - """ - Gets QEMU binaries list. - - Response parameters: - - List of Qemu binaries - """ - - qemus = [] - paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep) - # look for Qemu binaries in the current working directory and $PATH - if sys.platform.startswith("win"): - # add specific Windows paths - if hasattr(sys, "frozen"): - # add any qemu dir in the same location as gns3server.exe to the list of paths - exec_dir = os.path.dirname(os.path.abspath(sys.executable)) - for f in os.listdir(exec_dir): - if f.lower().startswith("qemu"): - paths.append(os.path.join(exec_dir, f)) - - if "PROGRAMFILES(X86)" in os.environ and os.path.exists(os.environ["PROGRAMFILES(X86)"]): - paths.append(os.path.join(os.environ["PROGRAMFILES(X86)"], "qemu")) - if "PROGRAMFILES" in os.environ and os.path.exists(os.environ["PROGRAMFILES"]): - paths.append(os.path.join(os.environ["PROGRAMFILES"], "qemu")) - elif sys.platform.startswith("darwin"): - # add specific locations on Mac OS X regardless of what's in $PATH - paths.extend(["/usr/local/bin", "/opt/local/bin"]) - if hasattr(sys, "frozen"): - paths.append(os.path.abspath(os.path.join(os.getcwd(), "../../../qemu/bin/"))) - for path in paths: - try: - for f in os.listdir(path): - if (f.startswith("qemu-system") or f == "qemu" or f == "qemu.exe") and \ - os.access(os.path.join(path, f), os.X_OK) and \ - os.path.isfile(os.path.join(path, f)): - qemu_path = os.path.join(path, f) - version = self._get_qemu_version(qemu_path) - qemus.append({"path": qemu_path, "version": version}) - except OSError: - continue - - response = {"qemus": qemus} - self.send_response(response) - - @IModule.route("qemu.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/old_modules/qemu/adapters/__init__.py b/gns3server/old_modules/qemu/adapters/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/gns3server/old_modules/qemu/adapters/adapter.py b/gns3server/old_modules/qemu/adapters/adapter.py deleted file mode 100644 index ade660f9..00000000 --- a/gns3server/old_modules/qemu/adapters/adapter.py +++ /dev/null @@ -1,105 +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 . - - -class Adapter(object): - - """ - Base class for adapters. - - :param interfaces: number of interfaces supported by this adapter. - """ - - def __init__(self, interfaces=1): - - self._interfaces = interfaces - - self._ports = {} - for port_id in range(0, interfaces): - self._ports[port_id] = None - - def removable(self): - """ - Returns True if the adapter can be removed from a slot - and False if not. - - :returns: boolean - """ - - return True - - def port_exists(self, port_id): - """ - Checks if a port exists on this adapter. - - :returns: True is the port exists, - False otherwise. - """ - - if port_id in self._ports: - return True - return False - - def add_nio(self, port_id, nio): - """ - Adds a NIO to a port on this adapter. - - :param port_id: port ID (integer) - :param nio: NIO instance - """ - - self._ports[port_id] = nio - - def remove_nio(self, port_id): - """ - Removes a NIO from a port on this adapter. - - :param port_id: port ID (integer) - """ - - self._ports[port_id] = None - - def get_nio(self, port_id): - """ - Returns the NIO assigned to a port. - - :params port_id: port ID (integer) - - :returns: NIO instance - """ - - return self._ports[port_id] - - @property - def ports(self): - """ - Returns port to NIO mapping - - :returns: dictionary port -> NIO - """ - - return self._ports - - @property - def interfaces(self): - """ - Returns the number of interfaces supported by this adapter. - - :returns: number of interfaces - """ - - return self._interfaces diff --git a/gns3server/old_modules/qemu/adapters/ethernet_adapter.py b/gns3server/old_modules/qemu/adapters/ethernet_adapter.py deleted file mode 100644 index 2064bb68..00000000 --- a/gns3server/old_modules/qemu/adapters/ethernet_adapter.py +++ /dev/null @@ -1,32 +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 . - -from .adapter import Adapter - - -class EthernetAdapter(Adapter): - - """ - QEMU Ethernet adapter. - """ - - def __init__(self): - Adapter.__init__(self, interfaces=1) - - def __str__(self): - - return "QEMU Ethernet adapter" diff --git a/gns3server/old_modules/qemu/nios/__init__.py b/gns3server/old_modules/qemu/nios/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/gns3server/old_modules/qemu/nios/nio.py b/gns3server/old_modules/qemu/nios/nio.py deleted file mode 100644 index 3c8a6b9e..00000000 --- a/gns3server/old_modules/qemu/nios/nio.py +++ /dev/null @@ -1,66 +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 . - -""" -Base interface for NIOs. -""" - - -class NIO(object): - - """ - Network Input/Output. - """ - - def __init__(self): - - self._capturing = False - self._pcap_output_file = "" - - def startPacketCapture(self, pcap_output_file): - """ - - :param pcap_output_file: PCAP destination file for the capture - """ - - self._capturing = True - self._pcap_output_file = pcap_output_file - - def stopPacketCapture(self): - - self._capturing = False - self._pcap_output_file = "" - - @property - def capturing(self): - """ - Returns either a capture is configured on this NIO. - - :returns: boolean - """ - - return self._capturing - - @property - def pcap_output_file(self): - """ - Returns the path to the PCAP output file. - - :returns: path to the PCAP output file - """ - - return self._pcap_output_file diff --git a/gns3server/old_modules/qemu/nios/nio_udp.py b/gns3server/old_modules/qemu/nios/nio_udp.py deleted file mode 100644 index 3b25f0c4..00000000 --- a/gns3server/old_modules/qemu/nios/nio_udp.py +++ /dev/null @@ -1,76 +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 . - -""" -Interface for UDP NIOs. -""" - -from .nio import NIO - - -class NIO_UDP(NIO): - - """ - UDP NIO. - - :param lport: local port number - :param rhost: remote address/host - :param rport: remote port number - """ - - _instance_count = 0 - - def __init__(self, lport, rhost, rport): - - NIO.__init__(self) - self._lport = lport - self._rhost = rhost - self._rport = rport - - @property - def lport(self): - """ - Returns the local port - - :returns: local port number - """ - - return self._lport - - @property - def rhost(self): - """ - Returns the remote host - - :returns: remote address/host - """ - - return self._rhost - - @property - def rport(self): - """ - Returns the remote port - - :returns: remote port number - """ - - return self._rport - - def __str__(self): - - return "NIO UDP" diff --git a/gns3server/old_modules/qemu/qemu_error.py b/gns3server/old_modules/qemu/qemu_error.py deleted file mode 100644 index 55135a34..00000000 --- a/gns3server/old_modules/qemu/qemu_error.py +++ /dev/null @@ -1,39 +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 . - -""" -Custom exceptions for QEMU module. -""" - - -class QemuError(Exception): - - def __init__(self, message, original_exception=None): - - Exception.__init__(self, message) - if isinstance(message, Exception): - message = str(message) - self._message = message - self._original_exception = original_exception - - def __repr__(self): - - return self._message - - def __str__(self): - - return self._message diff --git a/gns3server/old_modules/qemu/qemu_vm.py b/gns3server/old_modules/qemu/qemu_vm.py deleted file mode 100644 index a5ae107d..00000000 --- a/gns3server/old_modules/qemu/qemu_vm.py +++ /dev/null @@ -1,1244 +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 . - -""" -QEMU VM instance. -""" - -import sys -import os -import shutil -import random -import subprocess -import shlex -import ntpath -import telnetlib -import time -import re - -from gns3server.config import Config -from gns3dms.cloud.rackspace_ctrl import get_provider - -from .qemu_error import QemuError -from .adapters.ethernet_adapter import EthernetAdapter -from .nios.nio_udp import NIO_UDP -from ..attic import find_unused_port - -import logging -log = logging.getLogger(__name__) - - -class QemuVM(object): - - """ - QEMU VM implementation. - - :param name: name of this QEMU VM - :param qemu_path: path to the QEMU binary - :param working_dir: path to a working directory - :param host: host/address to bind for console and UDP connections - :param qemu_id: QEMU VM instance ID - :param console: TCP console port - :param console_host: IP address to bind for console connections - :param console_start_port_range: TCP console port range start - :param console_end_port_range: TCP console port range end - :param monitor: TCP monitor port - :param monitor_host: IP address to bind for monitor connections - :param monitor_start_port_range: TCP monitor port range start - :param monitor_end_port_range: TCP monitor port range end - """ - - _instances = [] - _allocated_console_ports = [] - _allocated_monitor_ports = [] - - def __init__(self, - name, - qemu_path, - working_dir, - host="127.0.0.1", - qemu_id=None, - console=None, - console_host="0.0.0.0", - console_start_port_range=5001, - console_end_port_range=5500, - monitor=None, - monitor_host="0.0.0.0", - monitor_start_port_range=5501, - monitor_end_port_range=6000): - - if not qemu_id: - self._id = 0 - for identifier in range(1, 1024): - if identifier not in self._instances: - self._id = identifier - self._instances.append(self._id) - break - - if self._id == 0: - raise QemuError("Maximum number of QEMU VM instances reached") - else: - if qemu_id in self._instances: - raise QemuError("QEMU identifier {} is already used by another QEMU VM instance".format(qemu_id)) - self._id = qemu_id - self._instances.append(self._id) - - self._name = name - self._working_dir = None - self._host = host - self._command = [] - self._started = False - self._process = None - self._cpulimit_process = None - self._stdout_file = "" - self._console_host = console_host - self._console_start_port_range = console_start_port_range - self._console_end_port_range = console_end_port_range - self._monitor_host = monitor_host - self._monitor_start_port_range = monitor_start_port_range - self._monitor_end_port_range = monitor_end_port_range - self._cloud_path = None - - # QEMU settings - self._qemu_path = qemu_path - self._hda_disk_image = "" - self._hdb_disk_image = "" - self._options = "" - self._ram = 256 - self._console = console - self._monitor = monitor - self._ethernet_adapters = [] - self._adapter_type = "e1000" - self._initrd = "" - self._kernel_image = "" - self._kernel_command_line = "" - self._legacy_networking = False - self._cpu_throttling = 0 # means no CPU throttling - self._process_priority = "low" - - working_dir_path = os.path.join(working_dir, "qemu", "vm-{}".format(self._id)) - - if qemu_id and not os.path.isdir(working_dir_path): - raise QemuError("Working directory {} doesn't exist".format(working_dir_path)) - - # create the device own working directory - self.working_dir = working_dir_path - - if not self._console: - # allocate a console port - try: - self._console = find_unused_port(self._console_start_port_range, - self._console_end_port_range, - self._console_host, - ignore_ports=self._allocated_console_ports) - except Exception as e: - raise QemuError(e) - - if self._console in self._allocated_console_ports: - raise QemuError("Console port {} is already used by another QEMU VM".format(console)) - self._allocated_console_ports.append(self._console) - - if not self._monitor: - # allocate a monitor port - try: - self._monitor = find_unused_port(self._monitor_start_port_range, - self._monitor_end_port_range, - self._monitor_host, - ignore_ports=self._allocated_monitor_ports) - except Exception as e: - raise QemuError(e) - - if self._monitor in self._allocated_monitor_ports: - raise QemuError("Monitor port {} is already used by another QEMU VM".format(monitor)) - self._allocated_monitor_ports.append(self._monitor) - - self.adapters = 1 # creates 1 adapter by default - log.info("QEMU VM {name} [id={id}] has been created".format(name=self._name, - id=self._id)) - - def defaults(self): - """ - Returns all the default attribute values for this QEMU VM. - - :returns: default values (dictionary) - """ - - qemu_defaults = {"name": self._name, - "qemu_path": self._qemu_path, - "ram": self._ram, - "hda_disk_image": self._hda_disk_image, - "hdb_disk_image": self._hdb_disk_image, - "options": self._options, - "adapters": self.adapters, - "adapter_type": self._adapter_type, - "console": self._console, - "monitor": self._monitor, - "initrd": self._initrd, - "kernel_image": self._kernel_image, - "kernel_command_line": self._kernel_command_line, - "legacy_networking": self._legacy_networking, - "cpu_throttling": self._cpu_throttling, - "process_priority": self._process_priority - } - - return qemu_defaults - - @property - def id(self): - """ - Returns the unique ID for this QEMU VM. - - :returns: id (integer) - """ - - return self._id - - @classmethod - def reset(cls): - """ - Resets allocated instance list. - """ - - cls._instances.clear() - cls._allocated_console_ports.clear() - cls._allocated_monitor_ports.clear() - - @property - def name(self): - """ - Returns the name of this QEMU VM. - - :returns: name - """ - - return self._name - - @name.setter - def name(self, new_name): - """ - Sets the name of this QEMU VM. - - :param new_name: name - """ - - log.info("QEMU VM {name} [id={id}]: renamed to {new_name}".format(name=self._name, - id=self._id, - new_name=new_name)) - - self._name = new_name - - @property - def working_dir(self): - """ - Returns current working directory - - :returns: path to the working directory - """ - - return self._working_dir - - @working_dir.setter - def working_dir(self, working_dir): - """ - Sets the working directory this QEMU VM. - - :param working_dir: path to the working directory - """ - - try: - os.makedirs(working_dir) - except FileExistsError: - pass - except OSError as e: - raise QemuError("Could not create working directory {}: {}".format(working_dir, e)) - - self._working_dir = working_dir - log.info("QEMU VM {name} [id={id}]: working directory changed to {wd}".format(name=self._name, - id=self._id, - wd=self._working_dir)) - - @property - def console(self): - """ - Returns the TCP console port. - - :returns: console port (integer) - """ - - return self._console - - @console.setter - def console(self, console): - """ - Sets the TCP console port. - - :param console: console port (integer) - """ - - if console in self._allocated_console_ports: - raise QemuError("Console port {} is already used by another QEMU VM".format(console)) - - self._allocated_console_ports.remove(self._console) - self._console = console - self._allocated_console_ports.append(self._console) - - log.info("QEMU VM {name} [id={id}]: console port set to {port}".format(name=self._name, - id=self._id, - port=console)) - - @property - def monitor(self): - """ - Returns the TCP monitor port. - - :returns: monitor port (integer) - """ - - return self._monitor - - @monitor.setter - def monitor(self, monitor): - """ - Sets the TCP monitor port. - - :param monitor: monitor port (integer) - """ - - if monitor in self._allocated_monitor_ports: - raise QemuError("Monitor port {} is already used by another QEMU VM".format(monitor)) - - self._allocated_monitor_ports.remove(self._monitor) - self._monitor = monitor - self._allocated_monitor_ports.append(self._monitor) - - log.info("QEMU VM {name} [id={id}]: monitor port set to {port}".format(name=self._name, - id=self._id, - port=monitor)) - - def delete(self): - """ - Deletes this QEMU VM. - """ - - self.stop() - if self._id in self._instances: - self._instances.remove(self._id) - - if self._console and self._console in self._allocated_console_ports: - self._allocated_console_ports.remove(self._console) - - if self._monitor and self._monitor in self._allocated_monitor_ports: - self._allocated_monitor_ports.remove(self._monitor) - - log.info("QEMU VM {name} [id={id}] has been deleted".format(name=self._name, - id=self._id)) - - def clean_delete(self): - """ - Deletes this QEMU VM & all files. - """ - - self.stop() - if self._id in self._instances: - self._instances.remove(self._id) - - if self._console: - self._allocated_console_ports.remove(self._console) - - if self._monitor: - self._allocated_monitor_ports.remove(self._monitor) - - try: - shutil.rmtree(self._working_dir) - except OSError as e: - log.error("could not delete QEMU VM {name} [id={id}]: {error}".format(name=self._name, - id=self._id, - error=e)) - return - - log.info("QEMU VM {name} [id={id}] has been deleted (including associated files)".format(name=self._name, - id=self._id)) - - @property - def cloud_path(self): - """ - Returns the cloud path where images can be downloaded from. - - :returns: cloud path - """ - - return self._cloud_path - - @cloud_path.setter - def cloud_path(self, cloud_path): - """ - Sets the cloud path where images can be downloaded from. - - :param cloud_path: - :return: - """ - - self._cloud_path = cloud_path - - @property - def qemu_path(self): - """ - Returns the QEMU binary path for this QEMU VM. - - :returns: QEMU path - """ - - return self._qemu_path - - @qemu_path.setter - def qemu_path(self, qemu_path): - """ - Sets the QEMU binary path this QEMU VM. - - :param qemu_path: QEMU path - """ - - log.info("QEMU VM {name} [id={id}] has set the QEMU path to {qemu_path}".format(name=self._name, - id=self._id, - qemu_path=qemu_path)) - self._qemu_path = qemu_path - - @property - def hda_disk_image(self): - """ - Returns the hda disk image path for this QEMU VM. - - :returns: QEMU hda disk image path - """ - - return self._hda_disk_image - - @hda_disk_image.setter - def hda_disk_image(self, hda_disk_image): - """ - Sets the hda disk image for this QEMU VM. - - :param hda_disk_image: QEMU hda disk image path - """ - - log.info("QEMU VM {name} [id={id}] has set the QEMU hda disk image path to {disk_image}".format(name=self._name, - id=self._id, - disk_image=hda_disk_image)) - self._hda_disk_image = hda_disk_image - - @property - def hdb_disk_image(self): - """ - Returns the hdb disk image path for this QEMU VM. - - :returns: QEMU hdb disk image path - """ - - return self._hdb_disk_image - - @hdb_disk_image.setter - def hdb_disk_image(self, hdb_disk_image): - """ - Sets the hdb disk image for this QEMU VM. - - :param hdb_disk_image: QEMU hdb disk image path - """ - - log.info("QEMU VM {name} [id={id}] has set the QEMU hdb disk image path to {disk_image}".format(name=self._name, - id=self._id, - disk_image=hdb_disk_image)) - self._hdb_disk_image = hdb_disk_image - - @property - def adapters(self): - """ - Returns the number of Ethernet adapters for this QEMU VM instance. - - :returns: number of adapters - """ - - return len(self._ethernet_adapters) - - @adapters.setter - def adapters(self, adapters): - """ - Sets the number of Ethernet adapters for this QEMU VM instance. - - :param adapters: number of adapters - """ - - self._ethernet_adapters.clear() - for adapter_id in range(0, adapters): - self._ethernet_adapters.append(EthernetAdapter()) - - log.info("QEMU VM {name} [id={id}]: number of Ethernet adapters changed to {adapters}".format(name=self._name, - id=self._id, - adapters=adapters)) - - @property - def adapter_type(self): - """ - Returns the adapter type for this QEMU VM instance. - - :returns: adapter type (string) - """ - - return self._adapter_type - - @adapter_type.setter - def adapter_type(self, adapter_type): - """ - Sets the adapter type for this QEMU VM instance. - - :param adapter_type: adapter type (string) - """ - - self._adapter_type = adapter_type - - log.info("QEMU VM {name} [id={id}]: adapter type changed to {adapter_type}".format(name=self._name, - id=self._id, - adapter_type=adapter_type)) - - @property - def legacy_networking(self): - """ - Returns either QEMU legacy networking commands are used. - - :returns: boolean - """ - - return self._legacy_networking - - @legacy_networking.setter - def legacy_networking(self, legacy_networking): - """ - Sets either QEMU legacy networking commands are used. - - :param legacy_networking: boolean - """ - - if legacy_networking: - log.info("QEMU VM {name} [id={id}] has enabled legacy networking".format(name=self._name, id=self._id)) - else: - log.info("QEMU VM {name} [id={id}] has disabled legacy networking".format(name=self._name, id=self._id)) - self._legacy_networking = legacy_networking - - @property - def cpu_throttling(self): - """ - Returns the percentage of CPU allowed. - - :returns: integer - """ - - return self._cpu_throttling - - @cpu_throttling.setter - def cpu_throttling(self, cpu_throttling): - """ - Sets the percentage of CPU allowed. - - :param cpu_throttling: integer - """ - - log.info("QEMU VM {name} [id={id}] has set the percentage of CPU allowed to {cpu}".format(name=self._name, - id=self._id, - cpu=cpu_throttling)) - self._cpu_throttling = cpu_throttling - self._stop_cpulimit() - if cpu_throttling: - self._set_cpu_throttling() - - @property - def process_priority(self): - """ - Returns the process priority. - - :returns: string - """ - - return self._process_priority - - @process_priority.setter - def process_priority(self, process_priority): - """ - Sets the process priority. - - :param process_priority: string - """ - - log.info("QEMU VM {name} [id={id}] has set the process priority to {priority}".format(name=self._name, - id=self._id, - priority=process_priority)) - self._process_priority = process_priority - - @property - def ram(self): - """ - Returns the RAM amount for this QEMU VM. - - :returns: RAM amount in MB - """ - - return self._ram - - @ram.setter - def ram(self, ram): - """ - Sets the amount of RAM for this QEMU VM. - - :param ram: RAM amount in MB - """ - - log.info("QEMU VM {name} [id={id}] has set the RAM to {ram}".format(name=self._name, - id=self._id, - ram=ram)) - self._ram = ram - - @property - def options(self): - """ - Returns the options for this QEMU VM. - - :returns: QEMU options - """ - - return self._options - - @options.setter - def options(self, options): - """ - Sets the options for this QEMU VM. - - :param options: QEMU options - """ - - log.info("QEMU VM {name} [id={id}] has set the QEMU options to {options}".format(name=self._name, - id=self._id, - options=options)) - self._options = options - - @property - def initrd(self): - """ - Returns the initrd path for this QEMU VM. - - :returns: QEMU initrd path - """ - - return self._initrd - - @initrd.setter - def initrd(self, initrd): - """ - Sets the initrd path for this QEMU VM. - - :param initrd: QEMU initrd path - """ - - log.info("QEMU VM {name} [id={id}] has set the QEMU initrd path to {initrd}".format(name=self._name, - id=self._id, - initrd=initrd)) - self._initrd = initrd - - @property - def kernel_image(self): - """ - Returns the kernel image path for this QEMU VM. - - :returns: QEMU kernel image path - """ - - return self._kernel_image - - @kernel_image.setter - def kernel_image(self, kernel_image): - """ - Sets the kernel image path for this QEMU VM. - - :param kernel_image: QEMU kernel image path - """ - - log.info("QEMU VM {name} [id={id}] has set the QEMU kernel image path to {kernel_image}".format(name=self._name, - id=self._id, - kernel_image=kernel_image)) - self._kernel_image = kernel_image - - @property - def kernel_command_line(self): - """ - Returns the kernel command line for this QEMU VM. - - :returns: QEMU kernel command line - """ - - return self._kernel_command_line - - @kernel_command_line.setter - def kernel_command_line(self, kernel_command_line): - """ - Sets the kernel command line for this QEMU VM. - - :param kernel_command_line: QEMU kernel command line - """ - - log.info("QEMU VM {name} [id={id}] has set the QEMU kernel command line to {kernel_command_line}".format(name=self._name, - id=self._id, - kernel_command_line=kernel_command_line)) - self._kernel_command_line = kernel_command_line - - def _set_process_priority(self): - """ - Changes the process priority - """ - - if sys.platform.startswith("win"): - try: - import win32api - import win32con - import win32process - except ImportError: - log.error("pywin32 must be installed to change the priority class for QEMU VM {}".format(self._name)) - else: - log.info("setting QEMU VM {} priority class to BELOW_NORMAL".format(self._name)) - handle = win32api.OpenProcess(win32con.PROCESS_ALL_ACCESS, 0, self._process.pid) - if self._process_priority == "realtime": - priority = win32process.REALTIME_PRIORITY_CLASS - elif self._process_priority == "very high": - priority = win32process.HIGH_PRIORITY_CLASS - elif self._process_priority == "high": - priority = win32process.ABOVE_NORMAL_PRIORITY_CLASS - elif self._process_priority == "low": - priority = win32process.BELOW_NORMAL_PRIORITY_CLASS - elif self._process_priority == "very low": - priority = win32process.IDLE_PRIORITY_CLASS - else: - priority = win32process.NORMAL_PRIORITY_CLASS - win32process.SetPriorityClass(handle, priority) - else: - if self._process_priority == "realtime": - priority = -20 - elif self._process_priority == "very high": - priority = -15 - elif self._process_priority == "high": - priority = -5 - elif self._process_priority == "low": - priority = 5 - elif self._process_priority == "very low": - priority = 19 - else: - priority = 0 - try: - subprocess.call(['renice', '-n', str(priority), '-p', str(self._process.pid)]) - except (OSError, subprocess.SubprocessError) as e: - log.error("could not change process priority for QEMU VM {}: {}".format(self._name, e)) - - def _stop_cpulimit(self): - """ - Stops the cpulimit process. - """ - - if self._cpulimit_process and self._cpulimit_process.poll() is None: - self._cpulimit_process.kill() - try: - self._process.wait(3) - except subprocess.TimeoutExpired: - log.error("could not kill cpulimit process {}".format(self._cpulimit_process.pid)) - - def _set_cpu_throttling(self): - """ - Limits the CPU usage for current QEMU process. - """ - - if not self.is_running(): - return - - try: - if sys.platform.startswith("win") and hasattr(sys, "frozen"): - cpulimit_exec = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), "cpulimit", "cpulimit.exe") - else: - cpulimit_exec = "cpulimit" - subprocess.Popen([cpulimit_exec, "--lazy", "--pid={}".format(self._process.pid), "--limit={}".format(self._cpu_throttling)], cwd=self._working_dir) - log.info("CPU throttled to {}%".format(self._cpu_throttling)) - except FileNotFoundError: - raise QemuError("cpulimit could not be found, please install it or deactivate CPU throttling") - except (OSError, subprocess.SubprocessError) as e: - raise QemuError("Could not throttle CPU: {}".format(e)) - - def start(self): - """ - Starts this QEMU VM. - """ - - if self.is_running(): - - # resume the VM if it is paused - self.resume() - return - - else: - - if not os.path.isfile(self._qemu_path) or not os.path.exists(self._qemu_path): - found = False - paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep) - # look for the qemu binary in the current working directory and $PATH - for path in paths: - try: - if self._qemu_path in os.listdir(path) and os.access(os.path.join(path, self._qemu_path), os.X_OK): - self._qemu_path = os.path.join(path, self._qemu_path) - found = True - break - except OSError: - continue - - if not found: - raise QemuError("QEMU binary '{}' is not accessible".format(self._qemu_path)) - - if self.cloud_path is not None: - # Download from Cloud Files - if self.hda_disk_image != "": - _, filename = ntpath.split(self.hda_disk_image) - src = '{}/{}'.format(self.cloud_path, filename) - dst = os.path.join(self.working_dir, filename) - if not os.path.isfile(dst): - cloud_settings = Config.instance().cloud_settings() - provider = get_provider(cloud_settings) - log.debug("Downloading file from {} to {}...".format(src, dst)) - provider.download_file(src, dst) - log.debug("Download of {} complete.".format(src)) - self.hda_disk_image = dst - if self.hdb_disk_image != "": - _, filename = ntpath.split(self.hdb_disk_image) - src = '{}/{}'.format(self.cloud_path, filename) - dst = os.path.join(self.working_dir, filename) - if not os.path.isfile(dst): - cloud_settings = Config.instance().cloud_settings() - provider = get_provider(cloud_settings) - log.debug("Downloading file from {} to {}...".format(src, dst)) - provider.download_file(src, dst) - log.debug("Download of {} complete.".format(src)) - self.hdb_disk_image = dst - - if self.initrd != "": - _, filename = ntpath.split(self.initrd) - src = '{}/{}'.format(self.cloud_path, filename) - dst = os.path.join(self.working_dir, filename) - if not os.path.isfile(dst): - cloud_settings = Config.instance().cloud_settings() - provider = get_provider(cloud_settings) - log.debug("Downloading file from {} to {}...".format(src, dst)) - provider.download_file(src, dst) - log.debug("Download of {} complete.".format(src)) - self.initrd = dst - if self.kernel_image != "": - _, filename = ntpath.split(self.kernel_image) - src = '{}/{}'.format(self.cloud_path, filename) - dst = os.path.join(self.working_dir, filename) - if not os.path.isfile(dst): - cloud_settings = Config.instance().cloud_settings() - provider = get_provider(cloud_settings) - log.debug("Downloading file from {} to {}...".format(src, dst)) - provider.download_file(src, dst) - log.debug("Download of {} complete.".format(src)) - self.kernel_image = dst - - self._command = self._build_command() - try: - log.info("starting QEMU: {}".format(self._command)) - self._stdout_file = os.path.join(self._working_dir, "qemu.log") - log.info("logging to {}".format(self._stdout_file)) - with open(self._stdout_file, "w") as fd: - self._process = subprocess.Popen(self._command, - stdout=fd, - stderr=subprocess.STDOUT, - cwd=self._working_dir) - log.info("QEMU VM instance {} started PID={}".format(self._id, self._process.pid)) - self._started = True - except (OSError, subprocess.SubprocessError) as e: - stdout = self.read_stdout() - log.error("could not start QEMU {}: {}\n{}".format(self._qemu_path, e, stdout)) - raise QemuError("could not start QEMU {}: {}\n{}".format(self._qemu_path, e, stdout)) - - self._set_process_priority() - if self._cpu_throttling: - self._set_cpu_throttling() - - def stop(self): - """ - Stops this QEMU VM. - """ - - # stop the QEMU process - if self.is_running(): - log.info("stopping QEMU VM instance {} PID={}".format(self._id, self._process.pid)) - try: - self._process.terminate() - self._process.wait(1) - except subprocess.TimeoutExpired: - self._process.kill() - if self._process.poll() is None: - log.warn("QEMU VM instance {} PID={} is still running".format(self._id, - self._process.pid)) - self._process = None - self._started = False - self._stop_cpulimit() - - def _control_vm(self, command, expected=None, timeout=30): - """ - Executes a command with QEMU monitor when this VM is running. - - :param command: QEMU monitor command (e.g. info status, stop etc.) - :param timeout: how long to wait for QEMU monitor - - :returns: result of the command (Match object or None) - """ - - result = None - if self.is_running() and self._monitor: - log.debug("Execute QEMU monitor command: {}".format(command)) - try: - tn = telnetlib.Telnet(self._monitor_host, self._monitor, timeout=timeout) - except OSError as e: - log.warn("Could not connect to QEMU monitor: {}".format(e)) - return result - try: - tn.write(command.encode('ascii') + b"\n") - time.sleep(0.1) - except OSError as e: - log.warn("Could not write to QEMU monitor: {}".format(e)) - tn.close() - return result - if expected: - try: - ind, match, dat = tn.expect(list=expected, timeout=timeout) - if match: - result = match - except EOFError as e: - log.warn("Could not read from QEMU monitor: {}".format(e)) - tn.close() - return result - - def _get_vm_status(self): - """ - Returns this VM suspend status (running|paused) - - :returns: status (string) - """ - - result = None - - match = self._control_vm("info status", [b"running", b"paused"]) - if match: - result = match.group(0).decode('ascii') - return result - - def suspend(self): - """ - Suspends this QEMU VM. - """ - - vm_status = self._get_vm_status() - if vm_status == "running": - self._control_vm("stop") - log.debug("QEMU VM has been suspended") - else: - log.info("QEMU VM is not running to be suspended, current status is {}".format(vm_status)) - - def reload(self): - """ - Reloads this QEMU VM. - """ - - self._control_vm("system_reset") - log.debug("QEMU VM has been reset") - - def resume(self): - """ - Resumes this QEMU VM. - """ - - vm_status = self._get_vm_status() - if vm_status == "paused": - self._control_vm("cont") - log.debug("QEMU VM has been resumed") - else: - log.info("QEMU VM is not paused to be resumed, current status is {}".format(vm_status)) - - def port_add_nio_binding(self, adapter_id, nio): - """ - Adds a port NIO binding. - - :param adapter_id: adapter ID - :param nio: NIO instance to add to the slot/port - """ - - try: - adapter = self._ethernet_adapters[adapter_id] - except IndexError: - raise QemuError("Adapter {adapter_id} doesn't exist on QEMU VM {name}".format(name=self._name, - adapter_id=adapter_id)) - - if self.is_running(): - # dynamically configure an UDP tunnel on the QEMU VM adapter - if nio and isinstance(nio, NIO_UDP): - if self._legacy_networking: - self._control_vm("host_net_remove {} gns3-{}".format(adapter_id, adapter_id)) - self._control_vm("host_net_add udp vlan={},name=gns3-{},sport={},dport={},daddr={}".format(adapter_id, - adapter_id, - nio.lport, - nio.rport, - nio.rhost)) - else: - # FIXME: does it work? very undocumented feature... - self._control_vm("netdev_del gns3-{}".format(adapter_id)) - self._control_vm("netdev_add socket,id=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_id, - nio.rhost, - nio.rport, - self._host, - nio.lport)) - - adapter.add_nio(0, nio) - log.info("QEMU VM {name} [id={id}]: {nio} added to adapter {adapter_id}".format(name=self._name, - id=self._id, - nio=nio, - adapter_id=adapter_id)) - - def port_remove_nio_binding(self, adapter_id): - """ - Removes a port NIO binding. - - :param adapter_id: adapter ID - - :returns: NIO instance - """ - - try: - adapter = self._ethernet_adapters[adapter_id] - except IndexError: - raise QemuError("Adapter {adapter_id} doesn't exist on QEMU VM {name}".format(name=self._name, - adapter_id=adapter_id)) - - if self.is_running(): - # dynamically disable the QEMU VM adapter - if self._legacy_networking: - self._control_vm("host_net_remove {} gns3-{}".format(adapter_id, adapter_id)) - self._control_vm("host_net_add user vlan={},name=gns3-{}".format(adapter_id, adapter_id)) - else: - # FIXME: does it work? very undocumented feature... - self._control_vm("netdev_del gns3-{}".format(adapter_id)) - self._control_vm("netdev_add user,id=gns3-{}".format(adapter_id)) - - nio = adapter.get_nio(0) - adapter.remove_nio(0) - log.info("QEMU VM {name} [id={id}]: {nio} removed from adapter {adapter_id}".format(name=self._name, - id=self._id, - nio=nio, - adapter_id=adapter_id)) - return nio - - @property - def started(self): - """ - Returns either this QEMU VM has been started or not. - - :returns: boolean - """ - - return self._started - - def read_stdout(self): - """ - Reads the standard output of the QEMU process. - Only use when the process has been stopped or has crashed. - """ - - output = "" - if self._stdout_file: - try: - with open(self._stdout_file, errors="replace") as file: - output = file.read() - except OSError as e: - log.warn("could not read {}: {}".format(self._stdout_file, e)) - return output - - def is_running(self): - """ - Checks if the QEMU process is running - - :returns: True or False - """ - - if self._process and self._process.poll() is None: - return True - return False - - def command(self): - """ - Returns the QEMU command line. - - :returns: QEMU command line (string) - """ - - return " ".join(self._build_command()) - - def _serial_options(self): - - if self._console: - return ["-serial", "telnet:{}:{},server,nowait".format(self._console_host, self._console)] - else: - return [] - - def _monitor_options(self): - - if self._monitor: - return ["-monitor", "telnet:{}:{},server,nowait".format(self._monitor_host, self._monitor)] - else: - return [] - - def _disk_options(self): - - options = [] - qemu_img_path = "" - qemu_path_dir = os.path.dirname(self._qemu_path) - try: - for f in os.listdir(qemu_path_dir): - if f.startswith("qemu-img"): - qemu_img_path = os.path.join(qemu_path_dir, f) - except OSError as e: - raise QemuError("Error while looking for qemu-img in {}: {}".format(qemu_path_dir, e)) - - if not qemu_img_path: - raise QemuError("Could not find qemu-img in {}".format(qemu_path_dir)) - - try: - if self._hda_disk_image: - if not os.path.isfile(self._hda_disk_image) or not os.path.exists(self._hda_disk_image): - if os.path.islink(self._hda_disk_image): - raise QemuError("hda disk image '{}' linked to '{}' is not accessible".format(self._hda_disk_image, os.path.realpath(self._hda_disk_image))) - else: - raise QemuError("hda disk image '{}' is not accessible".format(self._hda_disk_image)) - hda_disk = os.path.join(self._working_dir, "hda_disk.qcow2") - if not os.path.exists(hda_disk): - retcode = subprocess.call([qemu_img_path, "create", "-o", - "backing_file={}".format(self._hda_disk_image), - "-f", "qcow2", hda_disk]) - log.info("{} returned with {}".format(qemu_img_path, retcode)) - else: - # create a "FLASH" with 256MB if no disk image has been specified - hda_disk = os.path.join(self._working_dir, "flash.qcow2") - if not os.path.exists(hda_disk): - retcode = subprocess.call([qemu_img_path, "create", "-f", "qcow2", hda_disk, "128M"]) - log.info("{} returned with {}".format(qemu_img_path, retcode)) - - except (OSError, subprocess.SubprocessError) as e: - raise QemuError("Could not create disk image {}".format(e)) - - options.extend(["-hda", hda_disk]) - if self._hdb_disk_image: - if not os.path.isfile(self._hdb_disk_image) or not os.path.exists(self._hdb_disk_image): - if os.path.islink(self._hdb_disk_image): - raise QemuError("hdb disk image '{}' linked to '{}' is not accessible".format(self._hdb_disk_image, os.path.realpath(self._hdb_disk_image))) - else: - raise QemuError("hdb disk image '{}' is not accessible".format(self._hdb_disk_image)) - hdb_disk = os.path.join(self._working_dir, "hdb_disk.qcow2") - if not os.path.exists(hdb_disk): - try: - retcode = subprocess.call([qemu_img_path, "create", "-o", - "backing_file={}".format(self._hdb_disk_image), - "-f", "qcow2", hdb_disk]) - log.info("{} returned with {}".format(qemu_img_path, retcode)) - except (OSError, subprocess.SubprocessError) as e: - raise QemuError("Could not create disk image {}".format(e)) - options.extend(["-hdb", hdb_disk]) - - return options - - def _linux_boot_options(self): - - options = [] - if self._initrd: - if not os.path.isfile(self._initrd) or not os.path.exists(self._initrd): - if os.path.islink(self._initrd): - raise QemuError("initrd file '{}' linked to '{}' is not accessible".format(self._initrd, os.path.realpath(self._initrd))) - else: - raise QemuError("initrd file '{}' is not accessible".format(self._initrd)) - options.extend(["-initrd", self._initrd]) - if self._kernel_image: - if not os.path.isfile(self._kernel_image) or not os.path.exists(self._kernel_image): - if os.path.islink(self._kernel_image): - raise QemuError("kernel image '{}' linked to '{}' is not accessible".format(self._kernel_image, os.path.realpath(self._kernel_image))) - else: - raise QemuError("kernel image '{}' is not accessible".format(self._kernel_image)) - options.extend(["-kernel", self._kernel_image]) - if self._kernel_command_line: - options.extend(["-append", self._kernel_command_line]) - - return options - - def _network_options(self): - - network_options = [] - adapter_id = 0 - for adapter in self._ethernet_adapters: - # TODO: let users specify a base mac address - mac = "00:00:ab:%02x:%02x:%02d" % (random.randint(0x00, 0xff), random.randint(0x00, 0xff), adapter_id) - if self._legacy_networking: - network_options.extend(["-net", "nic,vlan={},macaddr={},model={}".format(adapter_id, mac, self._adapter_type)]) - else: - network_options.extend(["-device", "{},mac={},netdev=gns3-{}".format(self._adapter_type, mac, adapter_id)]) - - nio = adapter.get_nio(0) - if nio and isinstance(nio, NIO_UDP): - if self._legacy_networking: - network_options.extend(["-net", "udp,vlan={},name=gns3-{},sport={},dport={},daddr={}".format(adapter_id, - adapter_id, - nio.lport, - nio.rport, - nio.rhost)]) - else: - network_options.extend(["-netdev", "socket,id=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_id, - nio.rhost, - nio.rport, - self._host, - nio.lport)]) - else: - if self._legacy_networking: - network_options.extend(["-net", "user,vlan={},name=gns3-{}".format(adapter_id, adapter_id)]) - else: - network_options.extend(["-netdev", "user,id=gns3-{}".format(adapter_id)]) - adapter_id += 1 - - return network_options - - def _build_command(self): - """ - Command to start the QEMU process. - (to be passed to subprocess.Popen()) - """ - - command = [self._qemu_path] - command.extend(["-name", self._name]) - command.extend(["-m", str(self._ram)]) - command.extend(self._disk_options()) - command.extend(self._linux_boot_options()) - command.extend(self._serial_options()) - command.extend(self._monitor_options()) - additional_options = self._options.strip() - if additional_options: - command.extend(shlex.split(additional_options)) - command.extend(self._network_options()) - return command diff --git a/gns3server/old_modules/qemu/schemas.py b/gns3server/old_modules/qemu/schemas.py deleted file mode 100644 index 32b09664..00000000 --- a/gns3server/old_modules/qemu/schemas.py +++ /dev/null @@ -1,423 +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 . - - -QEMU_CREATE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to create a new QEMU VM instance", - "type": "object", - "properties": { - "name": { - "description": "QEMU VM instance name", - "type": "string", - "minLength": 1, - }, - "qemu_path": { - "description": "Path to QEMU", - "type": "string", - "minLength": 1, - }, - "qemu_id": { - "description": "QEMU VM instance ID", - "type": "integer" - }, - "console": { - "description": "console TCP port", - "minimum": 1, - "maximum": 65535, - "type": "integer" - }, - "monitor": { - "description": "monitor TCP port", - "minimum": 1, - "maximum": 65535, - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["name", "qemu_path"], -} - -QEMU_DELETE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to delete a QEMU VM instance", - "type": "object", - "properties": { - "id": { - "description": "QEMU VM instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -QEMU_UPDATE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to update a QEMU VM instance", - "type": "object", - "properties": { - "id": { - "description": "QEMU VM instance ID", - "type": "integer" - }, - "name": { - "description": "QEMU VM instance name", - "type": "string", - "minLength": 1, - }, - "qemu_path": { - "description": "path to QEMU", - "type": "string", - "minLength": 1, - }, - "hda_disk_image": { - "description": "QEMU hda disk image path", - "type": "string", - }, - "hdb_disk_image": { - "description": "QEMU hdb disk image path", - "type": "string", - }, - "ram": { - "description": "amount of RAM in MB", - "type": "integer" - }, - "adapters": { - "description": "number of adapters", - "type": "integer", - "minimum": 0, - "maximum": 32, - }, - "adapter_type": { - "description": "QEMU adapter type", - "type": "string", - "minLength": 1, - }, - "console": { - "description": "console TCP port", - "minimum": 1, - "maximum": 65535, - "type": "integer" - }, - "monitor": { - "description": "monitor TCP port", - "minimum": 1, - "maximum": 65535, - "type": "integer" - }, - "initrd": { - "description": "QEMU initrd path", - "type": "string", - }, - "kernel_image": { - "description": "QEMU kernel image path", - "type": "string", - }, - "kernel_command_line": { - "description": "QEMU kernel command line", - "type": "string", - }, - "cloud_path": { - "description": "Path to the image in the cloud object store", - "type": "string", - }, - "legacy_networking": { - "description": "Use QEMU legagy networking commands (-net syntax)", - "type": "boolean", - }, - "cpu_throttling": { - "description": "Percentage of CPU allowed for QEMU", - "minimum": 0, - "maximum": 800, - "type": "integer", - }, - "process_priority": { - "description": "Process priority for QEMU", - "enum": ["realtime", - "very high", - "high", - "normal", - "low", - "very low"] - }, - "options": { - "description": "Additional QEMU options", - "type": "string", - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -QEMU_START_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to start a QEMU VM instance", - "type": "object", - "properties": { - "id": { - "description": "QEMU VM instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -QEMU_STOP_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to stop a QEMU VM instance", - "type": "object", - "properties": { - "id": { - "description": "QEMU VM instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -QEMU_SUSPEND_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to suspend a QEMU VM instance", - "type": "object", - "properties": { - "id": { - "description": "QEMU VM instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -QEMU_RELOAD_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to reload a QEMU VM instance", - "type": "object", - "properties": { - "id": { - "description": "QEMU VM instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -QEMU_ALLOCATE_UDP_PORT_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to allocate an UDP port for a QEMU VM instance", - "type": "object", - "properties": { - "id": { - "description": "QEMU VM instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the QEMU VM instance", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id", "port_id"] -} - -QEMU_ADD_NIO_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to add a NIO for a QEMU VM 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": "QEMU VM instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the QEMU VM instance", - "type": "integer" - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 0, - "maximum": 32 - }, - "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"] -} - - -QEMU_DELETE_NIO_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to delete a NIO for a QEMU VM instance", - "type": "object", - "properties": { - "id": { - "description": "QEMU VM instance ID", - "type": "integer" - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 0, - "maximum": 32 - }, - }, - "additionalProperties": False, - "required": ["id", "port"] -} From f99d8253465115735bbbb1c236324f30ac41bec9 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 12 Feb 2015 22:28:12 +0100 Subject: [PATCH 230/485] Support network for IOU --- gns3server/handlers/iou_handler.py | 46 +++++++++++++++++++ gns3server/modules/base_manager.py | 8 ++-- gns3server/modules/iou/iou_vm.py | 74 +++++++++++++++++++++++++++++- gns3server/modules/nios/nio_tap.py | 2 +- gns3server/modules/nios/nio_udp.py | 2 +- gns3server/schemas/iou.py | 72 +++++++++++++++++++++++++++++ gns3server/web/route.py | 6 +-- tests/api/test_iou.py | 30 ++++++++++++ 8 files changed, 230 insertions(+), 10 deletions(-) diff --git a/gns3server/handlers/iou_handler.py b/gns3server/handlers/iou_handler.py index d73a0fb1..eddb85bf 100644 --- a/gns3server/handlers/iou_handler.py +++ b/gns3server/handlers/iou_handler.py @@ -19,6 +19,7 @@ from ..web.route import Route from ..schemas.iou import IOU_CREATE_SCHEMA from ..schemas.iou import IOU_UPDATE_SCHEMA from ..schemas.iou import IOU_OBJECT_SCHEMA +from ..schemas.iou import IOU_NIO_SCHEMA from ..modules.iou import IOU @@ -187,3 +188,48 @@ class IOUHandler: vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) yield from vm.reload() response.set_status(204) + + @Route.post( + r"/projects/{project_id}/iou/vms/{vm_id}/ports/{port_number:\d+}/nio", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance", + "port_number": "Port where the nio should be added" + }, + status_codes={ + 201: "NIO created", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Add a NIO to a IOU instance", + input=IOU_NIO_SCHEMA, + output=IOU_NIO_SCHEMA) + def create_nio(request, response): + + iou_manager = IOU.instance() + vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + nio = iou_manager.create_nio(vm.iouyap_path, request.json) + vm.slot_add_nio_binding(0, int(request.match_info["port_number"]), nio) + response.set_status(201) + response.json(nio) + + @classmethod + @Route.delete( + r"/projects/{project_id}/iou/vms/{vm_id}/ports/{port_number:\d+}/nio", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance", + "port_number": "Port from where the nio should be removed" + }, + status_codes={ + 204: "NIO deleted", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Remove a NIO from a IOU instance") + def delete_nio(request, response): + + iou_manager = IOU.instance() + vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + vm.slot_remove_nio_binding(0, int(request.match_info["port_number"])) + response.set_status(204) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index ec51f658..f38feacd 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -32,8 +32,8 @@ from ..config import Config from ..utils.asyncio import wait_run_in_executor from .project_manager import ProjectManager -from .nios.nio_udp import NIOUDP -from .nios.nio_tap import NIOTAP +from .nios.nio_udp import NIO_UDP +from .nios.nio_tap import NIO_TAP class BaseManager: @@ -274,11 +274,11 @@ class BaseManager: sock.connect((rhost, rport)) except OSError as e: raise aiohttp.web.HTTPInternalServerError(text="Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e)) - nio = NIOUDP(lport, rhost, rport) + nio = NIO_UDP(lport, rhost, rport) elif nio_settings["type"] == "nio_tap": tap_device = nio_settings["tap_device"] if not self._has_privileged_access(executable): raise aiohttp.web.HTTPForbidden(text="{} has no privileged access to {}.".format(executable, tap_device)) - nio = NIOTAP(tap_device) + nio = NIO_TAP(tap_device) assert nio is not None return nio diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 6fb7db94..d2387c7d 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -35,6 +35,8 @@ from pkg_resources import parse_version from .iou_error import IOUError from ..adapters.ethernet_adapter import EthernetAdapter from ..adapters.serial_adapter import SerialAdapter +from ..nios.nio_udp import NIO_UDP +from ..nios.nio_tap import NIO_TAP from ..base_vm import BaseVM from .ioucon import start_ioucon @@ -86,7 +88,7 @@ class IOUVM(BaseVM): self._ethernet_adapters = [] self._serial_adapters = [] self.ethernet_adapters = 2 if ethernet_adapters is None else ethernet_adapters # one adapter = 4 interfaces - self.serial_adapters = 2 if serial_adapters is None else serial_adapters # one adapter = 4 interfaces + self.serial_adapters = 2 if serial_adapters is None else serial_adapters # one adapter = 4 interfaces self._use_default_iou_values = True # for RAM & NVRAM values self._nvram = 128 if nvram is None else nvram # Kilobytes self._initial_config = "" @@ -529,6 +531,17 @@ class IOUVM(BaseVM): return True return False + def is_iouyap_running(self): + """ + Checks if the IOUYAP process is running + + :returns: True or False + """ + + if self._iouyap_process: + return True + return False + def _create_netmap_config(self): """ Creates the NETMAP file. @@ -687,3 +700,62 @@ class IOUVM(BaseVM): adapters=len(self._serial_adapters))) self._slots = self._ethernet_adapters + self._serial_adapters + + def slot_add_nio_binding(self, slot_id, port_id, nio): + """ + Adds a slot NIO binding. + :param slot_id: slot ID + :param port_id: port ID + :param nio: NIO instance to add to the slot/port + """ + + try: + adapter = self._slots[slot_id] + except IndexError: + raise IOUError("Slot {slot_id} doesn't exist on IOU {name}".format(name=self._name, + slot_id=slot_id)) + + if not adapter.port_exists(port_id): + raise IOUError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, + port_id=port_id)) + + adapter.add_nio(port_id, nio) + log.info("IOU {name} [id={id}]: {nio} added to {slot_id}/{port_id}".format(name=self._name, + id=self._id, + nio=nio, + slot_id=slot_id, + port_id=port_id)) + if self.is_iouyap_running(): + self._update_iouyap_config() + os.kill(self._iouyap_process.pid, signal.SIGHUP) + + def slot_remove_nio_binding(self, slot_id, port_id): + """ + Removes a slot NIO binding. + :param slot_id: slot ID + :param port_id: port ID + :returns: NIO instance + """ + + try: + adapter = self._slots[slot_id] + except IndexError: + raise IOUError("Slot {slot_id} doesn't exist on IOU {name}".format(name=self._name, + slot_id=slot_id)) + + if not adapter.port_exists(port_id): + raise IOUError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, + port_id=port_id)) + + nio = adapter.get_nio(port_id) + adapter.remove_nio(port_id) + log.info("IOU {name} [id={id}]: {nio} removed from {slot_id}/{port_id}".format(name=self._name, + id=self._id, + nio=nio, + slot_id=slot_id, + port_id=port_id)) + if self.is_iouyap_running(): + self._update_iouyap_config() + os.kill(self._iouyap_process.pid, signal.SIGHUP) + + return nio diff --git a/gns3server/modules/nios/nio_tap.py b/gns3server/modules/nios/nio_tap.py index 9f51ce13..a63a72c3 100644 --- a/gns3server/modules/nios/nio_tap.py +++ b/gns3server/modules/nios/nio_tap.py @@ -22,7 +22,7 @@ Interface for TAP NIOs (UNIX based OSes only). from .nio import NIO -class NIOTAP(NIO): +class NIO_TAP(NIO): """ TAP NIO. diff --git a/gns3server/modules/nios/nio_udp.py b/gns3server/modules/nios/nio_udp.py index a87875fe..4af43cd6 100644 --- a/gns3server/modules/nios/nio_udp.py +++ b/gns3server/modules/nios/nio_udp.py @@ -22,7 +22,7 @@ Interface for UDP NIOs. from .nio import NIO -class NIOUDP(NIO): +class NIO_UDP(NIO): """ UDP NIO. diff --git a/gns3server/schemas/iou.py b/gns3server/schemas/iou.py index 387874cf..6d304a19 100644 --- a/gns3server/schemas/iou.py +++ b/gns3server/schemas/iou.py @@ -173,3 +173,75 @@ IOU_OBJECT_SCHEMA = { "additionalProperties": False, "required": ["name", "vm_id", "console", "project_id", "path", "serial_adapters", "ethernet_adapters", "ram", "nvram"] } + +IOU_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 + }, + "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 + }, + }, + "oneOf": [ + {"$ref": "#/definitions/UDP"}, + {"$ref": "#/definitions/Ethernet"}, + {"$ref": "#/definitions/TAP"}, + ], + "additionalProperties": True, + "required": ["type"] +} diff --git a/gns3server/web/route.py b/gns3server/web/route.py index d63701fc..4de33da0 100644 --- a/gns3server/web/route.py +++ b/gns3server/web/route.py @@ -28,6 +28,7 @@ log = logging.getLogger(__name__) from ..modules.vm_error import VMError from .response import Response + @asyncio.coroutine def parse_request(request, input_schema): """Parse body of request and raise HTTP errors in case of problems""" @@ -42,9 +43,8 @@ def parse_request(request, input_schema): jsonschema.validate(request.json, input_schema) except jsonschema.ValidationError as e: log.error("Invalid input query. JSON schema error: {}".format(e.message)) - raise aiohttp.web.HTTPBadRequest(text="Request is not {} '{}' in schema: {}".format( - e.validator, - e.validator_value, + raise aiohttp.web.HTTPBadRequest(text="Invalid JSON: {} in schema: {}".format( + e.message, json.dumps(e.schema))) return request diff --git a/tests/api/test_iou.py b/tests/api/test_iou.py index 6ae3f715..61837160 100644 --- a/tests/api/test_iou.py +++ b/tests/api/test_iou.py @@ -133,3 +133,33 @@ def test_iou_update(server, vm, tmpdir, free_console_port): assert response.json["serial_adapters"] == 0 assert response.json["ram"] == 512 assert response.json["nvram"] == 2048 + + +def test_iou_nio_create_udp(server, vm): + response = server.post("/projects/{project_id}/iou/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}, + example=True) + assert response.status == 201 + assert response.route == "/projects/{project_id}/iou/vms/{vm_id}/ports/{port_number:\d+}/nio" + assert response.json["type"] == "nio_udp" + + +def test_iou_nio_create_tap(server, vm): + with patch("gns3server.modules.base_manager.BaseManager._has_privileged_access", return_value=True): + response = server.post("/projects/{project_id}/iou/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_tap", + "tap_device": "test"}) + assert response.status == 201 + assert response.route == "/projects/{project_id}/iou/vms/{vm_id}/ports/{port_number:\d+}/nio" + assert response.json["type"] == "nio_tap" + + +def test_iou_delete_nio(server, vm): + server.post("/projects/{project_id}/iou/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}) + response = server.delete("/projects/{project_id}/iou/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) + assert response.status == 204 + assert response.route == "/projects/{project_id}/iou/vms/{vm_id}/ports/{port_number:\d+}/nio" From 8f089c45f50945b7b73e834ec5d258dcbe79b245 Mon Sep 17 00:00:00 2001 From: grossmj Date: Thu, 12 Feb 2015 19:15:35 -0700 Subject: [PATCH 231/485] Fixes a few problems in Dynamips implementation. --- gns3server/handlers/dynamips_handler.py | 25 +++++++++++-------- gns3server/modules/dynamips/dynamips_error.py | 4 ++- .../modules/dynamips/dynamips_hypervisor.py | 3 ++- gns3server/modules/dynamips/hypervisor.py | 2 +- gns3server/modules/dynamips/nodes/router.py | 18 +++++++++---- 5 files changed, 34 insertions(+), 18 deletions(-) diff --git a/gns3server/handlers/dynamips_handler.py b/gns3server/handlers/dynamips_handler.py index 0b0be6a1..e3ed5d95 100644 --- a/gns3server/handlers/dynamips_handler.py +++ b/gns3server/handlers/dynamips_handler.py @@ -54,14 +54,15 @@ class DynamipsHandler: request.json.get("dynamips_id"), request.json.pop("platform")) - # set VM options + # set VM settings for name, value in request.json.items(): if hasattr(vm, name) and getattr(vm, name) != value: - setter = getattr(vm, "set_{}".format(name)) - if asyncio.iscoroutinefunction(vm.close): - yield from setter(value) - else: - setter(value) + if hasattr(vm, "set_{}".format(name)): + setter = getattr(vm, "set_{}".format(name)) + if asyncio.iscoroutinefunction(vm.close): + yield from setter(value) + else: + setter(value) response.set_status(201) response.json(vm) @@ -107,10 +108,14 @@ class DynamipsHandler: dynamips_manager = Dynamips.instance() vm = dynamips_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) - # FIXME: set options - #for name, value in request.json.items(): - # if hasattr(vm, name) and getattr(vm, name) != value: - # setattr(vm, name, value) + # set VM settings + for name, value in request.json.items(): + if hasattr(vm, name) and getattr(vm, name) != value: + setter = getattr(vm, "set_{}".format(name)) + if asyncio.iscoroutinefunction(vm.close): + yield from setter(value) + else: + setter(value) response.json(vm) @classmethod diff --git a/gns3server/modules/dynamips/dynamips_error.py b/gns3server/modules/dynamips/dynamips_error.py index 58c306ee..0f64dff6 100644 --- a/gns3server/modules/dynamips/dynamips_error.py +++ b/gns3server/modules/dynamips/dynamips_error.py @@ -19,8 +19,10 @@ Custom exceptions for Dynamips module. """ +from ..vm_error import VMError -class DynamipsError(Exception): + +class DynamipsError(VMError): def __init__(self, message, original_exception=None): diff --git a/gns3server/modules/dynamips/dynamips_hypervisor.py b/gns3server/modules/dynamips/dynamips_hypervisor.py index 9a1fb6f4..01a92248 100644 --- a/gns3server/modules/dynamips/dynamips_hypervisor.py +++ b/gns3server/modules/dynamips/dynamips_hypervisor.py @@ -128,8 +128,9 @@ class DynamipsHypervisor: """ yield from self.send("hypervisor stop") + yield from self._writer.drain() self._writer.close() - self._reader, self._writer = None + self._reader = self._writer = None self._nio_udp_auto_instances.clear() @asyncio.coroutine diff --git a/gns3server/modules/dynamips/hypervisor.py b/gns3server/modules/dynamips/hypervisor.py index 87e05f75..57afb37a 100644 --- a/gns3server/modules/dynamips/hypervisor.py +++ b/gns3server/modules/dynamips/hypervisor.py @@ -128,7 +128,7 @@ class Hypervisor(DynamipsHypervisor): if self.is_running(): log.info("Stopping Dynamips process PID={}".format(self._process.pid)) - DynamipsHypervisor.stop(self) + yield from DynamipsHypervisor.stop(self) # give some time for the hypervisor to properly stop. # time to delete UNIX NIOs for instance. yield from asyncio.sleep(0.01) diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index a2ca1f4b..e8c05618 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -27,7 +27,6 @@ import asyncio import time import sys import os -import base64 import logging log = logging.getLogger(__name__) @@ -232,7 +231,7 @@ class Router(BaseVM): if elf_header_start != b'\x7fELF\x01\x02\x01': raise DynamipsError('"{}" is not a valid IOS image'.format(self._image)) - yield from self._hypervisor.send('vm start "{}"'.format(self._name)) + yield from self._hypervisor.send('vm start "{name}"'.format(name=self._name)) log.info('router "{name}" [{id}] has been started'.format(name=self._name, id=self._id)) @asyncio.coroutine @@ -243,9 +242,18 @@ class Router(BaseVM): status = yield from self.get_status() if status != "inactive": - yield from self._hypervisor.send('vm stop "{name}"'.format(self._name)) + yield from self._hypervisor.send('vm stop "{name}"'.format(name=self._name)) log.info('Router "{name}" [{id}] has been stopped'.format(name=self._name, id=self._id)) + @asyncio.coroutine + def reload(self): + """ + Reload this router. + """ + + yield from self.stop() + yield from self.start() + @asyncio.coroutine def suspend(self): """ @@ -254,7 +262,7 @@ class Router(BaseVM): status = yield from self.get_status() if status == "running": - yield from self._hypervisor.send('vm suspend "{}"'.format(self._name)) + yield from self._hypervisor.send('vm suspend "{name}"'.format(name=self._name)) log.info('Router "{name}" [{id}] has been suspended'.format(name=self._name, id=self._id)) @asyncio.coroutine @@ -263,7 +271,7 @@ class Router(BaseVM): Resumes this suspended router """ - yield from self._hypervisor.send('vm resume "{}"'.format(self._name)) + yield from self._hypervisor.send('vm resume "{name}"'.format(name=self._name)) log.info('Router "{name}" [{id}] has been resumed'.format(name=self._name, id=self._id)) @asyncio.coroutine From b4190018133ac721502d22d8eba21f0b92a4a6f1 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 13 Feb 2015 11:02:06 +0100 Subject: [PATCH 232/485] Fix test on Travis with Python 3.3 --- tests/api/test_project.py | 8 +++----- tests/test_main.py | 4 ++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/api/test_project.py b/tests/api/test_project.py index df684018..b633a57c 100644 --- a/tests/api/test_project.py +++ b/tests/api/test_project.py @@ -55,15 +55,13 @@ def test_create_project_with_uuid(server): def test_show_project(server): - query = {"project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f", "path": "/tmp", "temporary": False} - with patch("gns3server.config.Config.get_section_config", return_value={"local": True}): - response = server.post("/projects", query) - assert response.status == 200 + query = {"project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f", "temporary": False} + response = server.post("/projects", query) + assert response.status == 200 response = server.get("/projects/00010203-0405-0607-0809-0a0b0c0d0e0f", example=True) assert len(response.json.keys()) == 4 assert len(response.json["location"]) > 0 assert response.json["project_id"] == "00010203-0405-0607-0809-0a0b0c0d0e0f" - assert response.json["path"] == "/tmp" assert response.json["temporary"] is False diff --git a/tests/test_main.py b/tests/test_main.py index cdf71631..cac75afc 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -50,12 +50,12 @@ def test_parse_arguments(capsys): with pytest.raises(SystemExit): main.parse_arguments(["-v"], server_config) out, err = capsys.readouterr() - assert __version__ in err + assert __version__ in "{}{}".format(out, err) # Depending of the Python version the location of the version change with pytest.raises(SystemExit): main.parse_arguments(["--version"], server_config) out, err = capsys.readouterr() - assert __version__ in err + assert __version__ in "{}{}".format(out, err) # Depending of the Python version the location of the version change with pytest.raises(SystemExit): main.parse_arguments(["-h"], server_config) From a4669689e7733cffa15501509d6e58b563ca18ae Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 13 Feb 2015 11:15:11 +0100 Subject: [PATCH 233/485] Fix tests due to test reading the local config file --- gns3server/config.py | 12 ++++++++++-- tests/test_main.py | 7 ++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/gns3server/config.py b/gns3server/config.py index 05c0cb96..5d4078c0 100644 --- a/gns3server/config.py +++ b/gns3server/config.py @@ -190,6 +190,14 @@ class Config(object): :returns: instance of Config """ - if not hasattr(Config, "_instance"): - Config._instance = Config() + if not hasattr(Config, "_instance") or Config._instance is None: + Config._instance = Config(files) return Config._instance + + @staticmethod + def reset(): + """ + Reset singleton + """ + + Config._instance = None diff --git a/tests/test_main.py b/tests/test_main.py index cac75afc..8762bcd8 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -35,9 +35,10 @@ def test_locale_check(): assert locale.getlocale() == ('fr_FR', 'UTF-8') -def test_parse_arguments(capsys): +def test_parse_arguments(capsys, tmpdir): - config = Config.instance() + Config.reset() + config = Config.instance(str(tmpdir / "test.cfg")) server_config = config.get_section_config("Server") with pytest.raises(SystemExit): @@ -70,7 +71,7 @@ def test_parse_arguments(capsys): assert "optional arguments" in out assert main.parse_arguments(["--host", "192.168.1.1"], server_config).host == "192.168.1.1" - assert main.parse_arguments([], server_config).host == "127.0.0.1" + assert main.parse_arguments([], server_config).host == "0.0.0.0" server_config["host"] = "192.168.1.2" assert main.parse_arguments(["--host", "192.168.1.1"], server_config).host == "192.168.1.1" assert main.parse_arguments([], server_config).host == "192.168.1.2" From 68427eaddf22f073021e80bef61d7dc6fc0b89c5 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 13 Feb 2015 14:43:28 +0100 Subject: [PATCH 234/485] Auto PEP8 cleanup --- gns3server/modules/dynamips/__init__.py | 2 +- .../modules/dynamips/adapters/adapter.py | 1 + .../modules/dynamips/adapters/c1700_mb_1fe.py | 1 + .../dynamips/adapters/c1700_mb_wic1.py | 1 + .../modules/dynamips/adapters/c2600_mb_1e.py | 1 + .../modules/dynamips/adapters/c2600_mb_1fe.py | 1 + .../modules/dynamips/adapters/c2600_mb_2e.py | 1 + .../modules/dynamips/adapters/c2600_mb_2fe.py | 1 + .../modules/dynamips/adapters/c7200_io_2fe.py | 1 + .../modules/dynamips/adapters/c7200_io_fe.py | 1 + .../dynamips/adapters/c7200_io_ge_e.py | 1 + .../modules/dynamips/adapters/leopard_2fe.py | 1 + .../modules/dynamips/adapters/nm_16esw.py | 1 + gns3server/modules/dynamips/adapters/nm_1e.py | 1 + .../modules/dynamips/adapters/nm_1fe_tx.py | 1 + gns3server/modules/dynamips/adapters/nm_4e.py | 1 + gns3server/modules/dynamips/adapters/nm_4t.py | 1 + .../modules/dynamips/adapters/pa_2fe_tx.py | 1 + gns3server/modules/dynamips/adapters/pa_4e.py | 1 + gns3server/modules/dynamips/adapters/pa_4t.py | 1 + gns3server/modules/dynamips/adapters/pa_8e.py | 1 + gns3server/modules/dynamips/adapters/pa_8t.py | 1 + gns3server/modules/dynamips/adapters/pa_a1.py | 1 + .../modules/dynamips/adapters/pa_fe_tx.py | 1 + gns3server/modules/dynamips/adapters/pa_ge.py | 1 + .../modules/dynamips/adapters/pa_pos_oc3.py | 1 + .../modules/dynamips/adapters/wic_1enet.py | 1 + .../modules/dynamips/adapters/wic_1t.py | 1 + .../modules/dynamips/adapters/wic_2t.py | 1 + .../modules/dynamips/dynamips_hypervisor.py | 1 + gns3server/modules/dynamips/dynamips_vm.py | 1 + gns3server/modules/dynamips/hypervisor.py | 1 + gns3server/modules/dynamips/nios/nio.py | 1 + gns3server/modules/dynamips/nios/nio_fifo.py | 1 + .../dynamips/nios/nio_generic_ethernet.py | 3 +-- .../dynamips/nios/nio_linux_ethernet.py | 1 + gns3server/modules/dynamips/nios/nio_mcast.py | 1 + gns3server/modules/dynamips/nios/nio_null.py | 1 + gns3server/modules/dynamips/nios/nio_tap.py | 1 + gns3server/modules/dynamips/nios/nio_udp.py | 1 + .../modules/dynamips/nios/nio_udp_auto.py | 1 + gns3server/modules/dynamips/nios/nio_unix.py | 1 + gns3server/modules/dynamips/nios/nio_vde.py | 1 + gns3server/modules/dynamips/nodes/c1700.py | 1 + gns3server/modules/dynamips/nodes/c2600.py | 1 + gns3server/modules/dynamips/nodes/c2691.py | 1 + gns3server/modules/dynamips/nodes/c3600.py | 1 + gns3server/modules/dynamips/nodes/c3725.py | 1 + gns3server/modules/dynamips/nodes/c3745.py | 1 + gns3server/modules/dynamips/nodes/c7200.py | 1 + gns3server/modules/dynamips/nodes/router.py | 27 ++++++++++--------- .../modules/dynamips/test_dynamips_router.py | 2 +- tox.ini | 2 +- 53 files changed, 66 insertions(+), 18 deletions(-) diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index 1edc2a39..47dd96a5 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -215,7 +215,7 @@ class Dynamips(BaseManager): rhost = nio_settings["rhost"] rport = nio_settings["rport"] try: - #TODO: handle IPv6 + # TODO: handle IPv6 with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: sock.connect((rhost, rport)) except OSError as e: diff --git a/gns3server/modules/dynamips/adapters/adapter.py b/gns3server/modules/dynamips/adapters/adapter.py index d963933e..40d82c7e 100644 --- a/gns3server/modules/dynamips/adapters/adapter.py +++ b/gns3server/modules/dynamips/adapters/adapter.py @@ -17,6 +17,7 @@ class Adapter(object): + """ Base class for adapters. diff --git a/gns3server/modules/dynamips/adapters/c1700_mb_1fe.py b/gns3server/modules/dynamips/adapters/c1700_mb_1fe.py index 3c67f3df..c94f551d 100644 --- a/gns3server/modules/dynamips/adapters/c1700_mb_1fe.py +++ b/gns3server/modules/dynamips/adapters/c1700_mb_1fe.py @@ -19,6 +19,7 @@ from .adapter import Adapter class C1700_MB_1FE(Adapter): + """ Integrated 1 port FastEthernet adapter for c1700 platform. """ diff --git a/gns3server/modules/dynamips/adapters/c1700_mb_wic1.py b/gns3server/modules/dynamips/adapters/c1700_mb_wic1.py index eca72358..9c6d2190 100644 --- a/gns3server/modules/dynamips/adapters/c1700_mb_wic1.py +++ b/gns3server/modules/dynamips/adapters/c1700_mb_wic1.py @@ -19,6 +19,7 @@ from .adapter import Adapter class C1700_MB_WIC1(Adapter): + """ Fake module to provide a placeholder for slot 1 interfaces when WICs are inserted into WIC slot 1. diff --git a/gns3server/modules/dynamips/adapters/c2600_mb_1e.py b/gns3server/modules/dynamips/adapters/c2600_mb_1e.py index 26fe5497..bebe7fa9 100644 --- a/gns3server/modules/dynamips/adapters/c2600_mb_1e.py +++ b/gns3server/modules/dynamips/adapters/c2600_mb_1e.py @@ -19,6 +19,7 @@ from .adapter import Adapter class C2600_MB_1E(Adapter): + """ Integrated 1 port Ethernet adapter for the c2600 platform. """ diff --git a/gns3server/modules/dynamips/adapters/c2600_mb_1fe.py b/gns3server/modules/dynamips/adapters/c2600_mb_1fe.py index 768d9c95..1ad294f2 100644 --- a/gns3server/modules/dynamips/adapters/c2600_mb_1fe.py +++ b/gns3server/modules/dynamips/adapters/c2600_mb_1fe.py @@ -19,6 +19,7 @@ from .adapter import Adapter class C2600_MB_1FE(Adapter): + """ Integrated 1 port FastEthernet adapter for the c2600 platform. """ diff --git a/gns3server/modules/dynamips/adapters/c2600_mb_2e.py b/gns3server/modules/dynamips/adapters/c2600_mb_2e.py index c2ca7442..1e42d5dd 100644 --- a/gns3server/modules/dynamips/adapters/c2600_mb_2e.py +++ b/gns3server/modules/dynamips/adapters/c2600_mb_2e.py @@ -19,6 +19,7 @@ from .adapter import Adapter class C2600_MB_2E(Adapter): + """ Integrated 2 port Ethernet adapter for the c2600 platform. """ diff --git a/gns3server/modules/dynamips/adapters/c2600_mb_2fe.py b/gns3server/modules/dynamips/adapters/c2600_mb_2fe.py index a7e6df14..dcd96581 100644 --- a/gns3server/modules/dynamips/adapters/c2600_mb_2fe.py +++ b/gns3server/modules/dynamips/adapters/c2600_mb_2fe.py @@ -19,6 +19,7 @@ from .adapter import Adapter class C2600_MB_2FE(Adapter): + """ Integrated 2 port FastEthernet adapter for the c2600 platform. """ diff --git a/gns3server/modules/dynamips/adapters/c7200_io_2fe.py b/gns3server/modules/dynamips/adapters/c7200_io_2fe.py index 0b8ae8a4..8b545e99 100644 --- a/gns3server/modules/dynamips/adapters/c7200_io_2fe.py +++ b/gns3server/modules/dynamips/adapters/c7200_io_2fe.py @@ -19,6 +19,7 @@ from .adapter import Adapter class C7200_IO_2FE(Adapter): + """ C7200-IO-2FE FastEthernet Input/Ouput controller. """ diff --git a/gns3server/modules/dynamips/adapters/c7200_io_fe.py b/gns3server/modules/dynamips/adapters/c7200_io_fe.py index 56e86cf1..784b154d 100644 --- a/gns3server/modules/dynamips/adapters/c7200_io_fe.py +++ b/gns3server/modules/dynamips/adapters/c7200_io_fe.py @@ -19,6 +19,7 @@ from .adapter import Adapter class C7200_IO_FE(Adapter): + """ C7200-IO-FE FastEthernet Input/Ouput controller. """ diff --git a/gns3server/modules/dynamips/adapters/c7200_io_ge_e.py b/gns3server/modules/dynamips/adapters/c7200_io_ge_e.py index 12ebaed6..f233dffd 100644 --- a/gns3server/modules/dynamips/adapters/c7200_io_ge_e.py +++ b/gns3server/modules/dynamips/adapters/c7200_io_ge_e.py @@ -19,6 +19,7 @@ from .adapter import Adapter class C7200_IO_GE_E(Adapter): + """ C7200-IO-GE-E GigabitEthernet Input/Ouput controller. """ diff --git a/gns3server/modules/dynamips/adapters/leopard_2fe.py b/gns3server/modules/dynamips/adapters/leopard_2fe.py index 0afa95c0..db6ad9c2 100644 --- a/gns3server/modules/dynamips/adapters/leopard_2fe.py +++ b/gns3server/modules/dynamips/adapters/leopard_2fe.py @@ -19,6 +19,7 @@ from .adapter import Adapter class Leopard_2FE(Adapter): + """ Integrated 2 port FastEthernet adapter for c3660 router. """ diff --git a/gns3server/modules/dynamips/adapters/nm_16esw.py b/gns3server/modules/dynamips/adapters/nm_16esw.py index fc3755cd..31e74565 100644 --- a/gns3server/modules/dynamips/adapters/nm_16esw.py +++ b/gns3server/modules/dynamips/adapters/nm_16esw.py @@ -19,6 +19,7 @@ from .adapter import Adapter class NM_16ESW(Adapter): + """ NM-16ESW FastEthernet network module. """ diff --git a/gns3server/modules/dynamips/adapters/nm_1e.py b/gns3server/modules/dynamips/adapters/nm_1e.py index ac200247..59ac5569 100644 --- a/gns3server/modules/dynamips/adapters/nm_1e.py +++ b/gns3server/modules/dynamips/adapters/nm_1e.py @@ -19,6 +19,7 @@ from .adapter import Adapter class NM_1E(Adapter): + """ NM-1E Ethernet network module. """ diff --git a/gns3server/modules/dynamips/adapters/nm_1fe_tx.py b/gns3server/modules/dynamips/adapters/nm_1fe_tx.py index 9723f703..26568306 100644 --- a/gns3server/modules/dynamips/adapters/nm_1fe_tx.py +++ b/gns3server/modules/dynamips/adapters/nm_1fe_tx.py @@ -19,6 +19,7 @@ from .adapter import Adapter class NM_1FE_TX(Adapter): + """ NM-1FE-TX FastEthernet network module. """ diff --git a/gns3server/modules/dynamips/adapters/nm_4e.py b/gns3server/modules/dynamips/adapters/nm_4e.py index ae6a51ed..086b04ee 100644 --- a/gns3server/modules/dynamips/adapters/nm_4e.py +++ b/gns3server/modules/dynamips/adapters/nm_4e.py @@ -19,6 +19,7 @@ from .adapter import Adapter class NM_4E(Adapter): + """ NM-4E Ethernet network module. """ diff --git a/gns3server/modules/dynamips/adapters/nm_4t.py b/gns3server/modules/dynamips/adapters/nm_4t.py index df6db299..77c3ecc8 100644 --- a/gns3server/modules/dynamips/adapters/nm_4t.py +++ b/gns3server/modules/dynamips/adapters/nm_4t.py @@ -19,6 +19,7 @@ from .adapter import Adapter class NM_4T(Adapter): + """ NM-4T Serial network module. """ diff --git a/gns3server/modules/dynamips/adapters/pa_2fe_tx.py b/gns3server/modules/dynamips/adapters/pa_2fe_tx.py index 8589ff2e..09b677f3 100644 --- a/gns3server/modules/dynamips/adapters/pa_2fe_tx.py +++ b/gns3server/modules/dynamips/adapters/pa_2fe_tx.py @@ -19,6 +19,7 @@ from .adapter import Adapter class PA_2FE_TX(Adapter): + """ PA-2FE-TX FastEthernet port adapter. """ diff --git a/gns3server/modules/dynamips/adapters/pa_4e.py b/gns3server/modules/dynamips/adapters/pa_4e.py index 32564992..d5981860 100644 --- a/gns3server/modules/dynamips/adapters/pa_4e.py +++ b/gns3server/modules/dynamips/adapters/pa_4e.py @@ -19,6 +19,7 @@ from .adapter import Adapter class PA_4E(Adapter): + """ PA-4E Ethernet port adapter. """ diff --git a/gns3server/modules/dynamips/adapters/pa_4t.py b/gns3server/modules/dynamips/adapters/pa_4t.py index 6a098a24..5a1393bc 100644 --- a/gns3server/modules/dynamips/adapters/pa_4t.py +++ b/gns3server/modules/dynamips/adapters/pa_4t.py @@ -19,6 +19,7 @@ from .adapter import Adapter class PA_4T(Adapter): + """ PA-4T+ Serial port adapter. """ diff --git a/gns3server/modules/dynamips/adapters/pa_8e.py b/gns3server/modules/dynamips/adapters/pa_8e.py index a6b5075f..96684055 100644 --- a/gns3server/modules/dynamips/adapters/pa_8e.py +++ b/gns3server/modules/dynamips/adapters/pa_8e.py @@ -19,6 +19,7 @@ from .adapter import Adapter class PA_8E(Adapter): + """ PA-8E Ethernet port adapter. """ diff --git a/gns3server/modules/dynamips/adapters/pa_8t.py b/gns3server/modules/dynamips/adapters/pa_8t.py index 600a5c29..723e026f 100644 --- a/gns3server/modules/dynamips/adapters/pa_8t.py +++ b/gns3server/modules/dynamips/adapters/pa_8t.py @@ -19,6 +19,7 @@ from .adapter import Adapter class PA_8T(Adapter): + """ PA-8T Serial port adapter. """ diff --git a/gns3server/modules/dynamips/adapters/pa_a1.py b/gns3server/modules/dynamips/adapters/pa_a1.py index 21d51f15..469d9ce4 100644 --- a/gns3server/modules/dynamips/adapters/pa_a1.py +++ b/gns3server/modules/dynamips/adapters/pa_a1.py @@ -19,6 +19,7 @@ from .adapter import Adapter class PA_A1(Adapter): + """ PA-A1 ATM port adapter. """ diff --git a/gns3server/modules/dynamips/adapters/pa_fe_tx.py b/gns3server/modules/dynamips/adapters/pa_fe_tx.py index 70ce8489..6434d2b4 100644 --- a/gns3server/modules/dynamips/adapters/pa_fe_tx.py +++ b/gns3server/modules/dynamips/adapters/pa_fe_tx.py @@ -19,6 +19,7 @@ from .adapter import Adapter class PA_FE_TX(Adapter): + """ PA-FE-TX FastEthernet port adapter. """ diff --git a/gns3server/modules/dynamips/adapters/pa_ge.py b/gns3server/modules/dynamips/adapters/pa_ge.py index f0287408..e466d905 100644 --- a/gns3server/modules/dynamips/adapters/pa_ge.py +++ b/gns3server/modules/dynamips/adapters/pa_ge.py @@ -19,6 +19,7 @@ from .adapter import Adapter class PA_GE(Adapter): + """ PA-GE GigabitEthernet port adapter. """ diff --git a/gns3server/modules/dynamips/adapters/pa_pos_oc3.py b/gns3server/modules/dynamips/adapters/pa_pos_oc3.py index b120de97..de0bc5d1 100644 --- a/gns3server/modules/dynamips/adapters/pa_pos_oc3.py +++ b/gns3server/modules/dynamips/adapters/pa_pos_oc3.py @@ -19,6 +19,7 @@ from .adapter import Adapter class PA_POS_OC3(Adapter): + """ PA-POS-OC3 port adapter. """ diff --git a/gns3server/modules/dynamips/adapters/wic_1enet.py b/gns3server/modules/dynamips/adapters/wic_1enet.py index dac79b6b..2d5e62b7 100644 --- a/gns3server/modules/dynamips/adapters/wic_1enet.py +++ b/gns3server/modules/dynamips/adapters/wic_1enet.py @@ -17,6 +17,7 @@ class WIC_1ENET(object): + """ WIC-1ENET Ethernet """ diff --git a/gns3server/modules/dynamips/adapters/wic_1t.py b/gns3server/modules/dynamips/adapters/wic_1t.py index 0f7cb3ad..2067246d 100644 --- a/gns3server/modules/dynamips/adapters/wic_1t.py +++ b/gns3server/modules/dynamips/adapters/wic_1t.py @@ -17,6 +17,7 @@ class WIC_1T(object): + """ WIC-1T Serial """ diff --git a/gns3server/modules/dynamips/adapters/wic_2t.py b/gns3server/modules/dynamips/adapters/wic_2t.py index 2bf2d565..b5af954e 100644 --- a/gns3server/modules/dynamips/adapters/wic_2t.py +++ b/gns3server/modules/dynamips/adapters/wic_2t.py @@ -17,6 +17,7 @@ class WIC_2T(object): + """ WIC-2T Serial """ diff --git a/gns3server/modules/dynamips/dynamips_hypervisor.py b/gns3server/modules/dynamips/dynamips_hypervisor.py index 01a92248..071019f5 100644 --- a/gns3server/modules/dynamips/dynamips_hypervisor.py +++ b/gns3server/modules/dynamips/dynamips_hypervisor.py @@ -32,6 +32,7 @@ log = logging.getLogger(__name__) class DynamipsHypervisor: + """ Creates a new connection to a Dynamips server (also called hypervisor) diff --git a/gns3server/modules/dynamips/dynamips_vm.py b/gns3server/modules/dynamips/dynamips_vm.py index b73b1dbf..bcdd5636 100644 --- a/gns3server/modules/dynamips/dynamips_vm.py +++ b/gns3server/modules/dynamips/dynamips_vm.py @@ -38,6 +38,7 @@ PLATFORMS = {'c1700': C1700, class DynamipsVM: + """ Factory to create an Router object based on the correct platform. """ diff --git a/gns3server/modules/dynamips/hypervisor.py b/gns3server/modules/dynamips/hypervisor.py index 57afb37a..778c490b 100644 --- a/gns3server/modules/dynamips/hypervisor.py +++ b/gns3server/modules/dynamips/hypervisor.py @@ -32,6 +32,7 @@ log = logging.getLogger(__name__) class Hypervisor(DynamipsHypervisor): + """ Hypervisor. diff --git a/gns3server/modules/dynamips/nios/nio.py b/gns3server/modules/dynamips/nios/nio.py index 256ddc1b..f829e4ed 100644 --- a/gns3server/modules/dynamips/nios/nio.py +++ b/gns3server/modules/dynamips/nios/nio.py @@ -28,6 +28,7 @@ log = logging.getLogger(__name__) class NIO: + """ Base NIO class diff --git a/gns3server/modules/dynamips/nios/nio_fifo.py b/gns3server/modules/dynamips/nios/nio_fifo.py index 60c9aa3f..55b91b8d 100644 --- a/gns3server/modules/dynamips/nios/nio_fifo.py +++ b/gns3server/modules/dynamips/nios/nio_fifo.py @@ -27,6 +27,7 @@ log = logging.getLogger(__name__) class NIOFIFO(NIO): + """ Dynamips FIFO NIO. diff --git a/gns3server/modules/dynamips/nios/nio_generic_ethernet.py b/gns3server/modules/dynamips/nios/nio_generic_ethernet.py index fc0ab006..af631654 100644 --- a/gns3server/modules/dynamips/nios/nio_generic_ethernet.py +++ b/gns3server/modules/dynamips/nios/nio_generic_ethernet.py @@ -27,6 +27,7 @@ log = logging.getLogger(__name__) class NIOGenericEthernet(NIO): + """ Dynamips generic Ethernet NIO. @@ -46,8 +47,6 @@ class NIOGenericEthernet(NIO): self._name = 'nio_gen_eth' + str(self._id) self._ethernet_device = ethernet_device - - @classmethod def reset(cls): """ diff --git a/gns3server/modules/dynamips/nios/nio_linux_ethernet.py b/gns3server/modules/dynamips/nios/nio_linux_ethernet.py index 513bc12a..1d3b280f 100644 --- a/gns3server/modules/dynamips/nios/nio_linux_ethernet.py +++ b/gns3server/modules/dynamips/nios/nio_linux_ethernet.py @@ -27,6 +27,7 @@ log = logging.getLogger(__name__) class NIOLinuxEthernet(NIO): + """ Dynamips Linux Ethernet NIO. diff --git a/gns3server/modules/dynamips/nios/nio_mcast.py b/gns3server/modules/dynamips/nios/nio_mcast.py index cf03aaab..ed6ea896 100644 --- a/gns3server/modules/dynamips/nios/nio_mcast.py +++ b/gns3server/modules/dynamips/nios/nio_mcast.py @@ -27,6 +27,7 @@ log = logging.getLogger(__name__) class NIOMcast(NIO): + """ Dynamips Linux Ethernet NIO. diff --git a/gns3server/modules/dynamips/nios/nio_null.py b/gns3server/modules/dynamips/nios/nio_null.py index df666fb8..b2c0e65f 100644 --- a/gns3server/modules/dynamips/nios/nio_null.py +++ b/gns3server/modules/dynamips/nios/nio_null.py @@ -27,6 +27,7 @@ log = logging.getLogger(__name__) class NIONull(NIO): + """ Dynamips NULL NIO. diff --git a/gns3server/modules/dynamips/nios/nio_tap.py b/gns3server/modules/dynamips/nios/nio_tap.py index 926e9b0b..e077161e 100644 --- a/gns3server/modules/dynamips/nios/nio_tap.py +++ b/gns3server/modules/dynamips/nios/nio_tap.py @@ -27,6 +27,7 @@ log = logging.getLogger(__name__) class NIOTAP(NIO): + """ Dynamips TAP NIO. diff --git a/gns3server/modules/dynamips/nios/nio_udp.py b/gns3server/modules/dynamips/nios/nio_udp.py index 999fdf9a..f1b0ca18 100644 --- a/gns3server/modules/dynamips/nios/nio_udp.py +++ b/gns3server/modules/dynamips/nios/nio_udp.py @@ -27,6 +27,7 @@ log = logging.getLogger(__name__) class NIOUDP(NIO): + """ Dynamips UDP NIO. diff --git a/gns3server/modules/dynamips/nios/nio_udp_auto.py b/gns3server/modules/dynamips/nios/nio_udp_auto.py index 1caaaea0..40eb6768 100644 --- a/gns3server/modules/dynamips/nios/nio_udp_auto.py +++ b/gns3server/modules/dynamips/nios/nio_udp_auto.py @@ -27,6 +27,7 @@ log = logging.getLogger(__name__) class NIOUDPAuto(NIO): + """ Dynamips auto UDP NIO. diff --git a/gns3server/modules/dynamips/nios/nio_unix.py b/gns3server/modules/dynamips/nios/nio_unix.py index 234fd65b..d37c83ad 100644 --- a/gns3server/modules/dynamips/nios/nio_unix.py +++ b/gns3server/modules/dynamips/nios/nio_unix.py @@ -27,6 +27,7 @@ log = logging.getLogger(__name__) class NIOUNIX(NIO): + """ Dynamips UNIX NIO. diff --git a/gns3server/modules/dynamips/nios/nio_vde.py b/gns3server/modules/dynamips/nios/nio_vde.py index 6b00cf2f..04b9fc07 100644 --- a/gns3server/modules/dynamips/nios/nio_vde.py +++ b/gns3server/modules/dynamips/nios/nio_vde.py @@ -26,6 +26,7 @@ log = logging.getLogger(__name__) class NIOVDE(NIO): + """ Dynamips VDE NIO. diff --git a/gns3server/modules/dynamips/nodes/c1700.py b/gns3server/modules/dynamips/nodes/c1700.py index faee65cd..9ff6e51b 100644 --- a/gns3server/modules/dynamips/nodes/c1700.py +++ b/gns3server/modules/dynamips/nodes/c1700.py @@ -30,6 +30,7 @@ log = logging.getLogger(__name__) class C1700(Router): + """ Dynamips c1700 router. diff --git a/gns3server/modules/dynamips/nodes/c2600.py b/gns3server/modules/dynamips/nodes/c2600.py index e3972253..497a5d56 100644 --- a/gns3server/modules/dynamips/nodes/c2600.py +++ b/gns3server/modules/dynamips/nodes/c2600.py @@ -32,6 +32,7 @@ log = logging.getLogger(__name__) class C2600(Router): + """ Dynamips c2600 router. diff --git a/gns3server/modules/dynamips/nodes/c2691.py b/gns3server/modules/dynamips/nodes/c2691.py index 273b33de..d40efd2c 100644 --- a/gns3server/modules/dynamips/nodes/c2691.py +++ b/gns3server/modules/dynamips/nodes/c2691.py @@ -29,6 +29,7 @@ log = logging.getLogger(__name__) class C2691(Router): + """ Dynamips c2691 router. diff --git a/gns3server/modules/dynamips/nodes/c3600.py b/gns3server/modules/dynamips/nodes/c3600.py index fe11b48d..415d9a74 100644 --- a/gns3server/modules/dynamips/nodes/c3600.py +++ b/gns3server/modules/dynamips/nodes/c3600.py @@ -29,6 +29,7 @@ log = logging.getLogger(__name__) class C3600(Router): + """ Dynamips c3600 router. diff --git a/gns3server/modules/dynamips/nodes/c3725.py b/gns3server/modules/dynamips/nodes/c3725.py index 6cb4213c..0d0ce36d 100644 --- a/gns3server/modules/dynamips/nodes/c3725.py +++ b/gns3server/modules/dynamips/nodes/c3725.py @@ -29,6 +29,7 @@ log = logging.getLogger(__name__) class C3725(Router): + """ Dynamips c3725 router. diff --git a/gns3server/modules/dynamips/nodes/c3745.py b/gns3server/modules/dynamips/nodes/c3745.py index 28acfe99..d94b9883 100644 --- a/gns3server/modules/dynamips/nodes/c3745.py +++ b/gns3server/modules/dynamips/nodes/c3745.py @@ -29,6 +29,7 @@ log = logging.getLogger(__name__) class C3745(Router): + """ Dynamips c3745 router. diff --git a/gns3server/modules/dynamips/nodes/c7200.py b/gns3server/modules/dynamips/nodes/c7200.py index 4d69e825..ca70ecb7 100644 --- a/gns3server/modules/dynamips/nodes/c7200.py +++ b/gns3server/modules/dynamips/nodes/c7200.py @@ -32,6 +32,7 @@ log = logging.getLogger(__name__) class C7200(Router): + """ Dynamips c7200 router (model is 7206). diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index e8c05618..bfff9d27 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -145,14 +145,14 @@ class Router(BaseVM): "system_id": self._system_id} # FIXME: add default slots/wics - #slot_id = 0 - #for slot in self._slots: + # slot_id = 0 + # for slot in self._slots: # if slot: # slot = str(slot) # router_defaults["slot" + str(slot_id)] = slot # slot_id += 1 - #if self._slots[0] and self._slots[0].wics: + # if self._slots[0] and self._slots[0].wics: # for wic_slot_id in range(0, len(self._slots[0].wics)): # router_defaults["wic" + str(wic_slot_id)] = None @@ -1014,8 +1014,8 @@ class Router(BaseVM): # Only c7200, c3600 and c3745 (NM-4T only) support new adapter while running if is_running and not ((self._platform == 'c7200' and not str(adapter).startswith('C7200')) - and not (self._platform == 'c3600' and self.chassis == '3660') - and not (self._platform == 'c3745' and adapter == 'NM-4T')): + and not (self._platform == 'c3600' and self.chassis == '3660') + and not (self._platform == 'c3745' and adapter == 'NM-4T')): raise DynamipsError('Adapter {adapter} cannot be added while router "{name}" is running'.format(adapter=adapter, name=self._name)) @@ -1060,8 +1060,8 @@ class Router(BaseVM): # Only c7200, c3600 and c3745 (NM-4T only) support to remove adapter while running if is_running and not ((self._platform == 'c7200' and not str(adapter).startswith('C7200')) - and not (self._platform == 'c3600' and self.chassis == '3660') - and not (self._platform == 'c3745' and adapter == 'NM-4T')): + and not (self._platform == 'c3600' and self.chassis == '3660') + and not (self._platform == 'c3745' and adapter == 'NM-4T')): raise DynamipsError('Adapter {adapter} cannot be removed while router "{name}" is running'.format(adapter=adapter, name=self._name)) @@ -1181,7 +1181,7 @@ class Router(BaseVM): adapter = self._slots[slot_id] except IndexError: raise DynamipsError('Slot {slot_id} does not exist on router "{name}"'.format(name=self._name, - slot_id=slot_id)) + slot_id=slot_id)) if not adapter.port_exists(port_id): raise DynamipsError("Port {port_id} does not exist in adapter {adapter}".format(adapter=adapter, port_id=port_id)) @@ -1246,13 +1246,14 @@ class Router(BaseVM): is_running = yield from self.is_running() if is_running: # running router yield from self._hypervisor.send('vm slot_enable_nio "{name}" {slot_id} {port_id}'.format(name=self._name, - slot_id=slot_id, - port_id=port_id)) + slot_id=slot_id, + port_id=port_id)) log.info('Router "{name}" [{id}]: NIO enabled on port {slot_id}/{port_id}'.format(name=self._name, id=self._id, slot_id=slot_id, port_id=port_id)) + @asyncio.coroutine def slot_disable_nio(self, slot_id, port_id): """ @@ -1301,11 +1302,10 @@ class Router(BaseVM): raise DynamipsError("Port {port_id} has already a filter applied on {adapter}".format(adapter=adapter, port_id=port_id)) - # FIXME: capture - #try: + # try: # os.makedirs(os.path.dirname(output_file), exist_ok=True) - #except OSError as e: + # except OSError as e: # raise DynamipsError("Could not create captures directory {}".format(e)) yield from nio.bind_filter("both", "capture") @@ -1316,6 +1316,7 @@ class Router(BaseVM): nio_name=nio.name, slot_id=slot_id, port_id=port_id)) + @asyncio.coroutine def stop_capture(self, slot_id, port_id): """ diff --git a/tests/modules/dynamips/test_dynamips_router.py b/tests/modules/dynamips/test_dynamips_router.py index c7fbb236..174fe7aa 100644 --- a/tests/modules/dynamips/test_dynamips_router.py +++ b/tests/modules/dynamips/test_dynamips_router.py @@ -44,7 +44,7 @@ def test_router(project, manager): def test_router_invalid_dynamips_path(project, manager, loop): with patch("gns3server.config.Config.get_section_config", return_value={"dynamips_path": "/bin/test_fake"}): - with pytest.raises(DynamipsError): + with pytest.raises(DynamipsError): router = Router("test", "00010203-0405-0607-0809-0a0b0c0d0e0e", project, manager) loop.run_until_complete(asyncio.async(router.create())) assert router.name == "test" diff --git a/tox.ini b/tox.ini index 53eaab0b..155cefab 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,7 @@ commands = python setup.py test deps = -rdev-requirements.txt [pep8] -ignore = E501 +ignore = E501,E402 [pytest] norecursedirs = old_tests .tox From 3e1875b069f6f66138a7bd6f60a4f59f488581b4 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 13 Feb 2015 14:46:00 +0100 Subject: [PATCH 235/485] Set console host from port manager --- gns3server/handlers/iou_handler.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gns3server/handlers/iou_handler.py b/gns3server/handlers/iou_handler.py index eddb85bf..dc94e550 100644 --- a/gns3server/handlers/iou_handler.py +++ b/gns3server/handlers/iou_handler.py @@ -16,6 +16,7 @@ # along with this program. If not, see . from ..web.route import Route +from ..modules.port_manager import PortManager from ..schemas.iou import IOU_CREATE_SCHEMA from ..schemas.iou import IOU_UPDATE_SCHEMA from ..schemas.iou import IOU_OBJECT_SCHEMA @@ -50,6 +51,7 @@ class IOUHandler: request.match_info["project_id"], request.json.get("vm_id"), console=request.json.get("console"), + console_host=PortManager.instance().console_host, serial_adapters=request.json.get("serial_adapters"), ethernet_adapters=request.json.get("ethernet_adapters"), ram=request.json.get("ram"), From 1550ca01e6c7f0f74e448f37191248f164d5efad Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 13 Feb 2015 16:41:18 +0100 Subject: [PATCH 236/485] IOU support nio ethernet --- gns3server/modules/base_manager.py | 3 ++ .../modules/nios/nio_generic_ethernet.py | 54 +++++++++++++++++++ tests/api/test_iou.py | 11 ++++ 3 files changed, 68 insertions(+) create mode 100644 gns3server/modules/nios/nio_generic_ethernet.py diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index f38feacd..a93cf1fd 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -34,6 +34,7 @@ from .project_manager import ProjectManager from .nios.nio_udp import NIO_UDP from .nios.nio_tap import NIO_TAP +from .nios.nio_generic_ethernet import NIO_GenericEthernet class BaseManager: @@ -280,5 +281,7 @@ class BaseManager: if not self._has_privileged_access(executable): raise aiohttp.web.HTTPForbidden(text="{} has no privileged access to {}.".format(executable, tap_device)) nio = NIO_TAP(tap_device) + elif nio_settings["type"] == "nio_generic_ethernet": + nio = NIO_GenericEthernet(nio_settings["ethernet_device"]) assert nio is not None return nio diff --git a/gns3server/modules/nios/nio_generic_ethernet.py b/gns3server/modules/nios/nio_generic_ethernet.py new file mode 100644 index 00000000..16b46e05 --- /dev/null +++ b/gns3server/modules/nios/nio_generic_ethernet.py @@ -0,0 +1,54 @@ +# -*- 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 . + +""" +Interface for generic Ethernet NIOs (PCAP library). +""" + +from .nio import NIO + + +class NIO_GenericEthernet(NIO): + """ + Generic Ethernet NIO. + + :param ethernet_device: Ethernet device name (e.g. eth0) + """ + + def __init__(self, ethernet_device): + + NIO.__init__(self) + self._ethernet_device = ethernet_device + + @property + def ethernet_device(self): + """ + Returns the Ethernet device used by this NIO. + + :returns: the Ethernet device name + """ + + return self._ethernet_device + + def __str__(self): + + return "NIO Ethernet" + + def __json__(self): + + return {"type": "nio_generic_ethernet", + "ethernet_device": self._ethernet_device} diff --git a/tests/api/test_iou.py b/tests/api/test_iou.py index 61837160..3f5b40cb 100644 --- a/tests/api/test_iou.py +++ b/tests/api/test_iou.py @@ -146,6 +146,17 @@ def test_iou_nio_create_udp(server, vm): assert response.json["type"] == "nio_udp" +def test_iou_nio_create_ethernet(server, vm): + response = server.post("/projects/{project_id}/iou/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_generic_ethernet", + "ethernet_device": "eth0", + }, + example=True) + assert response.status == 201 + assert response.route == "/projects/{project_id}/iou/vms/{vm_id}/ports/{port_number:\d+}/nio" + assert response.json["type"] == "nio_generic_ethernet" + assert response.json["ethernet_device"] == "eth0" + + def test_iou_nio_create_tap(server, vm): with patch("gns3server.modules.base_manager.BaseManager._has_privileged_access", return_value=True): response = server.post("/projects/{project_id}/iou/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_tap", From ee019caa373091c2f5b5a75a149bc973b1742c50 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 13 Feb 2015 16:57:35 +0100 Subject: [PATCH 237/485] Support l1_keepalives in IOU --- gns3server/handlers/iou_handler.py | 4 +- gns3server/modules/iou/iou_vm.py | 48 +++++++++++++++++-- .../modules/nios/nio_generic_ethernet.py | 1 + gns3server/schemas/iou.py | 14 +++++- tests/api/test_iou.py | 8 +++- 5 files changed, 69 insertions(+), 6 deletions(-) diff --git a/gns3server/handlers/iou_handler.py b/gns3server/handlers/iou_handler.py index dc94e550..d54b762c 100644 --- a/gns3server/handlers/iou_handler.py +++ b/gns3server/handlers/iou_handler.py @@ -55,7 +55,8 @@ class IOUHandler: serial_adapters=request.json.get("serial_adapters"), ethernet_adapters=request.json.get("ethernet_adapters"), ram=request.json.get("ram"), - nvram=request.json.get("nvram") + nvram=request.json.get("nvram"), + l1_keepalives=request.json.get("l1_keepalives") ) vm.path = request.json.get("path", vm.path) vm.iourc_path = request.json.get("iourc_path", vm.iourc_path) @@ -110,6 +111,7 @@ class IOUHandler: vm.serial_adapters = request.json.get("serial_adapters", vm.serial_adapters) vm.ram = request.json.get("ram", vm.ram) vm.nvram = request.json.get("nvram", vm.nvram) + vm.l1_keepalives = request.json.get("l1_keepalives", vm.l1_keepalives) response.json(vm) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index d2387c7d..d0fb4f60 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -61,6 +61,7 @@ class IOUVM(BaseVM): :params serial_adapters: Number of serial adapters :params ram: Ram MB :params nvram: Nvram KB + :params l1_keepalives: Always up ethernet interface """ def __init__(self, name, vm_id, project, manager, @@ -69,7 +70,8 @@ class IOUVM(BaseVM): ram=None, nvram=None, ethernet_adapters=None, - serial_adapters=None): + serial_adapters=None, + l1_keepalives=None): super().__init__(name, vm_id, project, manager) @@ -93,7 +95,7 @@ class IOUVM(BaseVM): self._nvram = 128 if nvram is None else nvram # Kilobytes self._initial_config = "" self._ram = 256 if ram is None else ram # Megabytes - self._l1_keepalives = False # used to overcome the always-up Ethernet interfaces (not supported by all IOSes). + self._l1_keepalives = False if l1_keepalives is None else l1_keepalives # used to overcome the always-up Ethernet interfaces (not supported by all IOSes). if self._console is not None: self._console = self._manager.port_manager.reserve_console_port(self._console) @@ -208,7 +210,8 @@ class IOUVM(BaseVM): "ethernet_adapters": len(self._ethernet_adapters), "serial_adapters": len(self._serial_adapters), "ram": self._ram, - "nvram": self._nvram + "nvram": self._nvram, + "l1_keepalives": self._l1_keepalives } @property @@ -759,3 +762,42 @@ class IOUVM(BaseVM): os.kill(self._iouyap_process.pid, signal.SIGHUP) return nio + + @property + def l1_keepalives(self): + """ + Returns either layer 1 keepalive messages option is enabled or disabled. + :returns: boolean + """ + + return self._l1_keepalives + + @l1_keepalives.setter + def l1_keepalives(self, state): + """ + Enables or disables layer 1 keepalive messages. + :param state: boolean + """ + + self._l1_keepalives = state + if state: + log.info("IOU {name} [id={id}]: has activated layer 1 keepalive messages".format(name=self._name, id=self._id)) + else: + log.info("IOU {name} [id={id}]: has deactivated layer 1 keepalive messages".format(name=self._name, id=self._id)) + + def _enable_l1_keepalives(self, command): + """ + Enables L1 keepalive messages if supported. + :param command: command line + """ + + env = os.environ.copy() + env["IOURC"] = self._iourc + try: + output = subprocess.check_output([self._path, "-h"], stderr=subprocess.STDOUT, cwd=self._working_dir, env=env) + if re.search("-l\s+Enable Layer 1 keepalive messages", output.decode("utf-8")): + command.extend(["-l"]) + else: + raise IOUError("layer 1 keepalive messages are not supported by {}".format(os.path.basename(self._path))) + except (OSError, subprocess.SubprocessError) as e: + log.warn("could not determine if layer 1 keepalive messages are supported by {}: {}".format(os.path.basename(self._path), e)) diff --git a/gns3server/modules/nios/nio_generic_ethernet.py b/gns3server/modules/nios/nio_generic_ethernet.py index 16b46e05..98dc91ca 100644 --- a/gns3server/modules/nios/nio_generic_ethernet.py +++ b/gns3server/modules/nios/nio_generic_ethernet.py @@ -23,6 +23,7 @@ from .nio import NIO class NIO_GenericEthernet(NIO): + """ Generic Ethernet NIO. diff --git a/gns3server/schemas/iou.py b/gns3server/schemas/iou.py index 6d304a19..0f578cf0 100644 --- a/gns3server/schemas/iou.py +++ b/gns3server/schemas/iou.py @@ -65,6 +65,10 @@ IOU_CREATE_SCHEMA = { "nvram": { "description": "Allocated NVRAM KB", "type": ["integer", "null"] + }, + "l1_keepalives": { + "description": "Always up ethernet interface", + "type": ["boolean", "null"] } }, "additionalProperties": False, @@ -114,6 +118,10 @@ IOU_UPDATE_SCHEMA = { "nvram": { "description": "Allocated NVRAM KB", "type": ["integer", "null"] + }, + "l1_keepalives": { + "description": "Always up ethernet interface", + "type": ["boolean", "null"] } }, "additionalProperties": False, @@ -168,10 +176,14 @@ IOU_OBJECT_SCHEMA = { "nvram": { "description": "Allocated NVRAM KB", "type": "integer" + }, + "l1_keepalives": { + "description": "Always up ethernet interface", + "type": "boolean" } }, "additionalProperties": False, - "required": ["name", "vm_id", "console", "project_id", "path", "serial_adapters", "ethernet_adapters", "ram", "nvram"] + "required": ["name", "vm_id", "console", "project_id", "path", "serial_adapters", "ethernet_adapters", "ram", "nvram", "l1_keepalives"] } IOU_NIO_SCHEMA = { diff --git a/tests/api/test_iou.py b/tests/api/test_iou.py index 3f5b40cb..09a7155c 100644 --- a/tests/api/test_iou.py +++ b/tests/api/test_iou.py @@ -56,6 +56,7 @@ def test_iou_create(server, project, base_params): assert response.json["ethernet_adapters"] == 2 assert response.json["ram"] == 256 assert response.json["nvram"] == 128 + assert response.json["l1_keepalives"] == False def test_iou_create_with_params(server, project, base_params): @@ -64,6 +65,7 @@ def test_iou_create_with_params(server, project, base_params): params["nvram"] = 512 params["serial_adapters"] = 4 params["ethernet_adapters"] = 0 + params["l1_keepalives"] = True response = server.post("/projects/{project_id}/iou/vms".format(project_id=project.id), params, example=True) assert response.status == 201 @@ -74,6 +76,7 @@ def test_iou_create_with_params(server, project, base_params): assert response.json["ethernet_adapters"] == 0 assert response.json["ram"] == 1024 assert response.json["nvram"] == 512 + assert response.json["l1_keepalives"] == True def test_iou_get(server, project, vm): @@ -86,6 +89,7 @@ def test_iou_get(server, project, vm): assert response.json["ethernet_adapters"] == 2 assert response.json["ram"] == 256 assert response.json["nvram"] == 128 + assert response.json["l1_keepalives"] == False def test_iou_start(server, vm): @@ -123,7 +127,8 @@ def test_iou_update(server, vm, tmpdir, free_console_port): "ram": 512, "nvram": 2048, "ethernet_adapters": 4, - "serial_adapters": 0 + "serial_adapters": 0, + "l1_keepalives": True } response = server.put("/projects/{project_id}/iou/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), params) assert response.status == 200 @@ -133,6 +138,7 @@ def test_iou_update(server, vm, tmpdir, free_console_port): assert response.json["serial_adapters"] == 0 assert response.json["ram"] == 512 assert response.json["nvram"] == 2048 + assert response.json["l1_keepalives"] == True def test_iou_nio_create_udp(server, vm): From 821eb5e92b6704e8d9b23af8e1be3a1efa9c198f Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 13 Feb 2015 17:34:22 +0100 Subject: [PATCH 238/485] Repare config file loading --- gns3server/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/config.py b/gns3server/config.py index 5d4078c0..4b918205 100644 --- a/gns3server/config.py +++ b/gns3server/config.py @@ -182,7 +182,7 @@ class Config(object): self._override_config[section] = content @staticmethod - def instance(files=[]): + def instance(files=None): """ Singleton to return only on instance of Config. From 2cab5293c77bfce0bc75c03e5b492cccd5d89166 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 13 Feb 2015 18:09:50 +0100 Subject: [PATCH 239/485] Add the notion of adapters and slot in the api --- gns3server/handlers/iou_handler.py | 10 ++++++---- gns3server/handlers/virtualbox_handler.py | 10 ++++++---- gns3server/handlers/vpcs_handler.py | 6 ++++-- tests/api/base.py | 4 +++- tests/api/test_iou.py | 18 +++++++++--------- tests/api/test_virtualbox.py | 8 ++++---- tests/api/test_vpcs.py | 14 +++++++------- 7 files changed, 39 insertions(+), 31 deletions(-) diff --git a/gns3server/handlers/iou_handler.py b/gns3server/handlers/iou_handler.py index d54b762c..88e8aceb 100644 --- a/gns3server/handlers/iou_handler.py +++ b/gns3server/handlers/iou_handler.py @@ -194,10 +194,11 @@ class IOUHandler: response.set_status(204) @Route.post( - r"/projects/{project_id}/iou/vms/{vm_id}/ports/{port_number:\d+}/nio", + r"/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio", parameters={ "project_id": "UUID for the project", "vm_id": "UUID for the instance", + "adapter_number": "Network adapter where the nio is located", "port_number": "Port where the nio should be added" }, status_codes={ @@ -213,16 +214,17 @@ class IOUHandler: iou_manager = IOU.instance() vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) nio = iou_manager.create_nio(vm.iouyap_path, request.json) - vm.slot_add_nio_binding(0, int(request.match_info["port_number"]), nio) + vm.slot_add_nio_binding(int(request.match_info["adapter_number"]), int(request.match_info["port_number"]), nio) response.set_status(201) response.json(nio) @classmethod @Route.delete( - r"/projects/{project_id}/iou/vms/{vm_id}/ports/{port_number:\d+}/nio", + r"/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio", parameters={ "project_id": "UUID for the project", "vm_id": "UUID for the instance", + "adapter_number": "Network adapter where the nio is located", "port_number": "Port from where the nio should be removed" }, status_codes={ @@ -235,5 +237,5 @@ class IOUHandler: iou_manager = IOU.instance() vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) - vm.slot_remove_nio_binding(0, int(request.match_info["port_number"])) + vm.slot_remove_nio_binding(int(request.match_info["adapter_number"]), int(request.match_info["port_number"])) response.set_status(204) diff --git a/gns3server/handlers/virtualbox_handler.py b/gns3server/handlers/virtualbox_handler.py index 97997d3e..16361c1d 100644 --- a/gns3server/handlers/virtualbox_handler.py +++ b/gns3server/handlers/virtualbox_handler.py @@ -252,11 +252,12 @@ class VirtualBoxHandler: response.set_status(204) @Route.post( - r"/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/nio", + r"/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/ports/{port_id:\d+}/nio", parameters={ "project_id": "UUID for the project", "vm_id": "UUID for the instance", - "adapter_id": "Adapter where the nio should be added" + "adapter_id": "Adapter where the nio should be added", + "port_id": "Port in the adapter (always 0 for virtualbox)" }, status_codes={ 201: "NIO created", @@ -277,11 +278,12 @@ class VirtualBoxHandler: @classmethod @Route.delete( - r"/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/nio", + r"/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/ports/{port_id:\d+}/nio", parameters={ "project_id": "UUID for the project", "vm_id": "UUID for the instance", - "adapter_id": "Adapter from where the nio should be removed" + "adapter_id": "Adapter from where the nio should be removed", + "port_id": "Port in the adapter (always 0 for virtualbox)" }, status_codes={ 204: "NIO deleted", diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index 0ecb6c08..24458405 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -178,10 +178,11 @@ class VPCSHandler: response.set_status(204) @Route.post( - r"/projects/{project_id}/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio", + r"/projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio", parameters={ "project_id": "UUID for the project", "vm_id": "UUID for the instance", + "adapter_number": "Network adapter where the nio is located", "port_number": "Port where the nio should be added" }, status_codes={ @@ -203,10 +204,11 @@ class VPCSHandler: @classmethod @Route.delete( - r"/projects/{project_id}/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio", + r"/projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio", parameters={ "project_id": "UUID for the project", "vm_id": "UUID for the instance", + "adapter_number": "Network adapter where the nio is located", "port_number": "Port from where the nio should be removed" }, status_codes={ diff --git a/tests/api/base.py b/tests/api/base.py index 41a553d2..312c3175 100644 --- a/tests/api/base.py +++ b/tests/api/base.py @@ -74,7 +74,9 @@ class Query: asyncio.async(go(future, response)) self._loop.run_until_complete(future) response.body = future.result() - response.route = response.headers.get('X-Route', None).replace("/v1", "") + x_route = response.headers.get('X-Route', None) + if x_route is not None: + response.route = x_route.replace("/v1", "") if response.body is not None: try: diff --git a/tests/api/test_iou.py b/tests/api/test_iou.py index 09a7155c..3c8921b5 100644 --- a/tests/api/test_iou.py +++ b/tests/api/test_iou.py @@ -142,41 +142,41 @@ def test_iou_update(server, vm, tmpdir, free_console_port): def test_iou_nio_create_udp(server, vm): - response = server.post("/projects/{project_id}/iou/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", + response = server.post("/projects/{project_id}/iou/vms/{vm_id}/adapters/1/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"}, example=True) assert response.status == 201 - assert response.route == "/projects/{project_id}/iou/vms/{vm_id}/ports/{port_number:\d+}/nio" + assert response.route == "/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" assert response.json["type"] == "nio_udp" def test_iou_nio_create_ethernet(server, vm): - response = server.post("/projects/{project_id}/iou/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_generic_ethernet", + response = server.post("/projects/{project_id}/iou/vms/{vm_id}/adapters/1/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_generic_ethernet", "ethernet_device": "eth0", }, example=True) assert response.status == 201 - assert response.route == "/projects/{project_id}/iou/vms/{vm_id}/ports/{port_number:\d+}/nio" + assert response.route == "/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" assert response.json["type"] == "nio_generic_ethernet" assert response.json["ethernet_device"] == "eth0" def test_iou_nio_create_tap(server, vm): with patch("gns3server.modules.base_manager.BaseManager._has_privileged_access", return_value=True): - response = server.post("/projects/{project_id}/iou/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_tap", + response = server.post("/projects/{project_id}/iou/vms/{vm_id}/adapters/1/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_tap", "tap_device": "test"}) assert response.status == 201 - assert response.route == "/projects/{project_id}/iou/vms/{vm_id}/ports/{port_number:\d+}/nio" + assert response.route == "/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" assert response.json["type"] == "nio_tap" def test_iou_delete_nio(server, vm): - server.post("/projects/{project_id}/iou/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", + server.post("/projects/{project_id}/iou/vms/{vm_id}/adapters/1/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"}) - response = server.delete("/projects/{project_id}/iou/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) + response = server.delete("/projects/{project_id}/iou/vms/{vm_id}/adapters/1/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert response.status == 204 - assert response.route == "/projects/{project_id}/iou/vms/{vm_id}/ports/{port_number:\d+}/nio" + assert response.route == "/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" diff --git a/tests/api/test_virtualbox.py b/tests/api/test_virtualbox.py index c37a56f0..3c0e7ad1 100644 --- a/tests/api/test_virtualbox.py +++ b/tests/api/test_virtualbox.py @@ -93,7 +93,7 @@ def test_vbox_reload(server, vm): def test_vbox_nio_create_udp(server, vm): with asyncio_patch('gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.adapter_add_nio_binding') as mock: - response = server.post("/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/0/nio".format(project_id=vm["project_id"], + response = server.post("/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/0/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", "lport": 4242, "rport": 4343, @@ -105,21 +105,21 @@ def test_vbox_nio_create_udp(server, vm): assert args[0] == 0 assert response.status == 201 - assert response.route == "/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/nio" + assert response.route == "/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/ports/{port_id:\d+}/nio" assert response.json["type"] == "nio_udp" def test_vbox_delete_nio(server, vm): with asyncio_patch('gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.adapter_remove_nio_binding') as mock: - response = server.delete("/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) + response = server.delete("/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/0/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert mock.called args, kwgars = mock.call_args assert args[0] == 0 assert response.status == 204 - assert response.route == "/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/nio" + assert response.route == "/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/ports/{port_id:\d+}/nio" def test_vbox_update(server, vm, free_console_port): diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index ba42c45c..32bdb958 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -63,33 +63,33 @@ def test_vpcs_create_port(server, project, free_console_port): def test_vpcs_nio_create_udp(server, vm): - response = server.post("/projects/{project_id}/vpcs/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", + response = server.post("/projects/{project_id}/vpcs/vms/{vm_id}/adapters/0/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"}, example=True) assert response.status == 201 - assert response.route == "/projects/{project_id}/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio" + assert response.route == "/projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" assert response.json["type"] == "nio_udp" def test_vpcs_nio_create_tap(server, vm): with patch("gns3server.modules.base_manager.BaseManager._has_privileged_access", return_value=True): - response = server.post("/projects/{project_id}/vpcs/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_tap", + response = server.post("/projects/{project_id}/vpcs/vms/{vm_id}/adapters/0/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_tap", "tap_device": "test"}) assert response.status == 201 - assert response.route == "/projects/{project_id}/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio" + assert response.route == "/projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" assert response.json["type"] == "nio_tap" def test_vpcs_delete_nio(server, vm): - server.post("/projects/{project_id}/vpcs/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", + server.post("/projects/{project_id}/vpcs/vms/{vm_id}/adapters/0/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"}) - response = server.delete("/projects/{project_id}/vpcs/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) + response = server.delete("/projects/{project_id}/vpcs/vms/{vm_id}/adapters/0/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert response.status == 204 - assert response.route == "/projects/{project_id}/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio" + assert response.route == "/projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" def test_vpcs_start(server, vm): From 49f012cf4c0f6ab4f5b3211b7795728135f01233 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 13 Feb 2015 18:27:08 +0100 Subject: [PATCH 240/485] Turn off documentation sidebar because it's broken --- .../api/examples/delete_projectsprojectid.txt | 4 +- ...ptersadapternumberdportsportnumberdnio.txt | 13 ++ ...svmidadaptersadapteriddportsportiddnio.txt | 13 ++ ...ptersadapternumberdportsportnumberdnio.txt | 13 ++ docs/api/examples/get_interfaces.txt | 50 +++-- docs/api/examples/get_projectsprojectid.txt | 10 +- .../get_projectsprojectidiouvmsvmid.txt | 26 +++ ...get_projectsprojectidvirtualboxvmsvmid.txt | 26 +++ .../get_projectsprojectidvpcsvmsvmid.txt | 21 ++ docs/api/examples/get_version.txt | 4 +- docs/api/examples/post_portsudp.txt | 4 +- .../examples/post_projectsprojectidclose.txt | 4 +- .../examples/post_projectsprojectidcommit.txt | 4 +- .../examples/post_projectsprojectidiouvms.txt | 35 ++++ ...ptersadapternumberdportsportnumberdnio.txt | 21 ++ .../post_projectsprojectidvirtualboxvms.txt | 30 +++ ...svmidadaptersadapteriddportsportiddnio.txt | 25 +++ .../post_projectsprojectidvpcsvms.txt | 23 ++ ...ptersadapternumberdportsportnumberdnio.txt | 25 +++ docs/api/examples/post_version.txt | 4 +- docs/api/examples/put_projectsprojectid.txt | 12 +- docs/api/v1interfaces.rst | 4 +- docs/api/v1portsudp.rst | 4 +- docs/api/v1projects.rst | 6 +- docs/api/v1projectsprojectid.rst | 8 +- docs/api/v1projectsprojectidclose.rst | 4 +- docs/api/v1projectsprojectidcommit.rst | 4 +- docs/api/v1projectsprojectiddynamipsvms.rst | 116 +++++++++++ .../v1projectsprojectiddynamipsvmsvmid.rst | 197 ++++++++++++++++++ ...projectsprojectiddynamipsvmsvmidreload.rst | 20 ++ ...projectsprojectiddynamipsvmsvmidresume.rst | 20 ++ ...1projectsprojectiddynamipsvmsvmidstart.rst | 20 ++ ...v1projectsprojectiddynamipsvmsvmidstop.rst | 20 ++ ...rojectsprojectiddynamipsvmsvmidsuspend.rst | 20 ++ docs/api/v1projectsprojectidiouvms.rst | 55 +++++ docs/api/v1projectsprojectidiouvmsvmid.rst | 107 ++++++++++ ...ptersadapternumberdportsportnumberdnio.rst | 40 ++++ .../v1projectsprojectidiouvmsvmidreload.rst | 20 ++ .../v1projectsprojectidiouvmsvmidstart.rst | 20 ++ .../api/v1projectsprojectidiouvmsvmidstop.rst | 20 ++ ...t => v1projectsprojectidvirtualboxvms.rst} | 14 +- ... v1projectsprojectidvirtualboxvmsvmid.rst} | 22 +- ...vmidadaptersadapteriddportsportiddnio.rst} | 18 +- ...vmsvmidadaptersadapteriddstartcapture.rst} | 12 +- ...xvmsvmidadaptersadapteriddstopcapture.rst} | 10 +- ...jectsprojectidvirtualboxvmsvmidreload.rst} | 8 +- ...jectsprojectidvirtualboxvmsvmidresume.rst} | 8 +- ...ojectsprojectidvirtualboxvmsvmidstart.rst} | 8 +- ...rojectsprojectidvirtualboxvmsvmidstop.rst} | 8 +- ...ectsprojectidvirtualboxvmsvmidsuspend.rst} | 8 +- ...vms.rst => v1projectsprojectidvpcsvms.rst} | 11 +- ...rst => v1projectsprojectidvpcsvmsvmid.rst} | 18 +- ...tersadapternumberdportsportnumberdnio.rst} | 18 +- ... v1projectsprojectidvpcsvmsvmidreload.rst} | 8 +- ...> v1projectsprojectidvpcsvmsvmidstart.rst} | 8 +- ...=> v1projectsprojectidvpcsvmsvmidstop.rst} | 8 +- docs/api/v1version.rst | 6 +- docs/api/v1virtualboxvms.rst | 4 +- docs/conf.py | 4 + gns3server/web/documentation.py | 4 +- tests/api/test_iou.py | 18 +- tests/api/test_virtualbox.py | 8 +- tests/api/test_vpcs.py | 14 +- 63 files changed, 1146 insertions(+), 171 deletions(-) create mode 100644 docs/api/examples/delete_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt create mode 100644 docs/api/examples/delete_projectsprojectidvirtualboxvmsvmidadaptersadapteriddportsportiddnio.txt create mode 100644 docs/api/examples/delete_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt create mode 100644 docs/api/examples/get_projectsprojectidiouvmsvmid.txt create mode 100644 docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt create mode 100644 docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt create mode 100644 docs/api/examples/post_projectsprojectidiouvms.txt create mode 100644 docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt create mode 100644 docs/api/examples/post_projectsprojectidvirtualboxvms.txt create mode 100644 docs/api/examples/post_projectsprojectidvirtualboxvmsvmidadaptersadapteriddportsportiddnio.txt create mode 100644 docs/api/examples/post_projectsprojectidvpcsvms.txt create mode 100644 docs/api/examples/post_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt create mode 100644 docs/api/v1projectsprojectiddynamipsvms.rst create mode 100644 docs/api/v1projectsprojectiddynamipsvmsvmid.rst create mode 100644 docs/api/v1projectsprojectiddynamipsvmsvmidreload.rst create mode 100644 docs/api/v1projectsprojectiddynamipsvmsvmidresume.rst create mode 100644 docs/api/v1projectsprojectiddynamipsvmsvmidstart.rst create mode 100644 docs/api/v1projectsprojectiddynamipsvmsvmidstop.rst create mode 100644 docs/api/v1projectsprojectiddynamipsvmsvmidsuspend.rst create mode 100644 docs/api/v1projectsprojectidiouvms.rst create mode 100644 docs/api/v1projectsprojectidiouvmsvmid.rst create mode 100644 docs/api/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst create mode 100644 docs/api/v1projectsprojectidiouvmsvmidreload.rst create mode 100644 docs/api/v1projectsprojectidiouvmsvmidstart.rst create mode 100644 docs/api/v1projectsprojectidiouvmsvmidstop.rst rename docs/api/{v1projectidvirtualboxvms.rst => v1projectsprojectidvirtualboxvms.rst} (83%) rename docs/api/{v1projectidvirtualboxvmsvmid.rst => v1projectsprojectidvirtualboxvmsvmid.rst} (84%) rename docs/api/{v1projectidvirtualboxvmsvmidadaptersadapteriddnio.rst => v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddportsportiddnio.rst} (52%) rename docs/api/{v1projectidvirtualboxvmidcaptureadapteriddstart.rst => v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddstartcapture.rst} (53%) rename docs/api/{v1projectidvirtualboxvmidcaptureadapteriddstop.rst => v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddstopcapture.rst} (52%) rename docs/api/{v1projectidvirtualboxvmsvmidreload.rst => v1projectsprojectidvirtualboxvmsvmidreload.rst} (53%) rename docs/api/{v1projectidvirtualboxvmsvmidresume.rst => v1projectsprojectidvirtualboxvmsvmidresume.rst} (53%) rename docs/api/{v1projectidvirtualboxvmsvmidstart.rst => v1projectsprojectidvirtualboxvmsvmidstart.rst} (53%) rename docs/api/{v1projectidvirtualboxvmsvmidstop.rst => v1projectsprojectidvirtualboxvmsvmidstop.rst} (53%) rename docs/api/{v1projectidvirtualboxvmsvmidsuspend.rst => v1projectsprojectidvirtualboxvmsvmidsuspend.rst} (53%) rename docs/api/{v1projectidvpcsvms.rst => v1projectsprojectidvpcsvms.rst} (83%) rename docs/api/{v1projectidvpcsvmsvmid.rst => v1projectsprojectidvpcsvmsvmid.rst} (85%) rename docs/api/{v1projectidvpcsvmsvmidportsportnumberdnio.rst => v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst} (51%) rename docs/api/{v1projectidvpcsvmsvmidreload.rst => v1projectsprojectidvpcsvmsvmidreload.rst} (53%) rename docs/api/{v1projectidvpcsvmsvmidstart.rst => v1projectsprojectidvpcsvmsvmidstart.rst} (53%) rename docs/api/{v1projectidvpcsvmsvmidstop.rst => v1projectsprojectidvpcsvmsvmidstop.rst} (53%) diff --git a/docs/api/examples/delete_projectsprojectid.txt b/docs/api/examples/delete_projectsprojectid.txt index d48bdd35..45efff6c 100644 --- a/docs/api/examples/delete_projectsprojectid.txt +++ b/docs/api/examples/delete_projectsprojectid.txt @@ -5,9 +5,9 @@ DELETE /projects/{project_id} HTTP/1.1 HTTP/1.1 204 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /v1/projects/{project_id} diff --git a/docs/api/examples/delete_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt b/docs/api/examples/delete_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt new file mode 100644 index 00000000..f8aa407f --- /dev/null +++ b/docs/api/examples/delete_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -0,0 +1,13 @@ +curl -i -X DELETE 'http://localhost:8000/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio' + +DELETE /projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio HTTP/1.1 + + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio + diff --git a/docs/api/examples/delete_projectsprojectidvirtualboxvmsvmidadaptersadapteriddportsportiddnio.txt b/docs/api/examples/delete_projectsprojectidvirtualboxvmsvmidadaptersadapteriddportsportiddnio.txt new file mode 100644 index 00000000..0c732706 --- /dev/null +++ b/docs/api/examples/delete_projectsprojectidvirtualboxvmsvmidadaptersadapteriddportsportiddnio.txt @@ -0,0 +1,13 @@ +curl -i -X DELETE 'http://localhost:8000/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/ports/{port_id:\d+}/nio' + +DELETE /projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/ports/{port_id:\d+}/nio HTTP/1.1 + + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/ports/{port_id:\d+}/nio + diff --git a/docs/api/examples/delete_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt b/docs/api/examples/delete_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt new file mode 100644 index 00000000..6842905a --- /dev/null +++ b/docs/api/examples/delete_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -0,0 +1,13 @@ +curl -i -X DELETE 'http://localhost:8000/projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio' + +DELETE /projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio HTTP/1.1 + + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio + diff --git a/docs/api/examples/get_interfaces.txt b/docs/api/examples/get_interfaces.txt index f0566cea..7ac8c74d 100644 --- a/docs/api/examples/get_interfaces.txt +++ b/docs/api/examples/get_interfaces.txt @@ -5,32 +5,56 @@ GET /interfaces HTTP/1.1 HTTP/1.1 200 -CONNECTION: close -CONTENT-LENGTH: 298 +CONNECTION: keep-alive +CONTENT-LENGTH: 652 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /v1/interfaces [ { - "id": "lo", - "name": "lo" + "id": "lo0", + "name": "lo0" }, { - "id": "eth0", - "name": "eth0" + "id": "gif0", + "name": "gif0" }, { - "id": "wlan0", - "name": "wlan0" + "id": "stf0", + "name": "stf0" }, { - "id": "vmnet1", - "name": "vmnet1" + "id": "en0", + "name": "en0" }, { - "id": "vmnet8", - "name": "vmnet8" + "id": "en1", + "name": "en1" + }, + { + "id": "fw0", + "name": "fw0" + }, + { + "id": "en2", + "name": "en2" + }, + { + "id": "p2p0", + "name": "p2p0" + }, + { + "id": "bridge0", + "name": "bridge0" + }, + { + "id": "vboxnet0", + "name": "vboxnet0" + }, + { + "id": "vboxnet1", + "name": "vboxnet1" } ] diff --git a/docs/api/examples/get_projectsprojectid.txt b/docs/api/examples/get_projectsprojectid.txt index 035e8487..1c1716fd 100644 --- a/docs/api/examples/get_projectsprojectid.txt +++ b/docs/api/examples/get_projectsprojectid.txt @@ -5,16 +5,16 @@ GET /projects/{project_id} HTTP/1.1 HTTP/1.1 200 -CONNECTION: close -CONTENT-LENGTH: 165 +CONNECTION: keep-alive +CONTENT-LENGTH: 277 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /v1/projects/{project_id} { - "location": "/tmp", - "path": "/tmp/00010203-0405-0607-0809-0a0b0c0d0e0f", + "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmp95msfptc", + "path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmp95msfptc/00010203-0405-0607-0809-0a0b0c0d0e0f", "project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f", "temporary": false } diff --git a/docs/api/examples/get_projectsprojectidiouvmsvmid.txt b/docs/api/examples/get_projectsprojectidiouvmsvmid.txt new file mode 100644 index 00000000..0e1faa6c --- /dev/null +++ b/docs/api/examples/get_projectsprojectidiouvmsvmid.txt @@ -0,0 +1,26 @@ +curl -i -X GET 'http://localhost:8000/projects/{project_id}/iou/vms/{vm_id}' + +GET /projects/{project_id}/iou/vms/{vm_id} HTTP/1.1 + + + +HTTP/1.1 200 +CONNECTION: keep-alive +CONTENT-LENGTH: 381 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/iou/vms/{vm_id} + +{ + "console": 2000, + "ethernet_adapters": 2, + "l1_keepalives": false, + "name": "PC TEST 1", + "nvram": 128, + "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2249/test_iou_get0/iou.bin", + "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", + "ram": 256, + "serial_adapters": 2, + "vm_id": "3ab57efc-4016-4898-ac4c-6e804ed8d429" +} diff --git a/docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt b/docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt new file mode 100644 index 00000000..cbb4296b --- /dev/null +++ b/docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt @@ -0,0 +1,26 @@ +curl -i -X GET 'http://localhost:8000/projects/{project_id}/virtualbox/vms/{vm_id}' + +GET /projects/{project_id}/virtualbox/vms/{vm_id} HTTP/1.1 + + + +HTTP/1.1 200 +CONNECTION: keep-alive +CONTENT-LENGTH: 347 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/virtualbox/vms/{vm_id} + +{ + "adapter_type": "Intel PRO/1000 MT Desktop (82540EM)", + "adapters": 0, + "console": 2001, + "enable_remote_console": false, + "headless": false, + "name": "VMTEST", + "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", + "use_any_adapter": false, + "vm_id": "3e6ed300-eefc-4582-80f0-acbdba7e2b9b", + "vmname": "VMTEST" +} diff --git a/docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt b/docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt new file mode 100644 index 00000000..36365cd4 --- /dev/null +++ b/docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt @@ -0,0 +1,21 @@ +curl -i -X GET 'http://localhost:8000/projects/{project_id}/vpcs/vms/{vm_id}' + +GET /projects/{project_id}/vpcs/vms/{vm_id} HTTP/1.1 + + + +HTTP/1.1 200 +CONNECTION: keep-alive +CONTENT-LENGTH: 187 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/vpcs/vms/{vm_id} + +{ + "console": 2009, + "name": "PC TEST 1", + "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", + "startup_script": null, + "vm_id": "b4ff501b-c24d-4f0c-854b-8163e31b9a8e" +} diff --git a/docs/api/examples/get_version.txt b/docs/api/examples/get_version.txt index ddf810d1..88017034 100644 --- a/docs/api/examples/get_version.txt +++ b/docs/api/examples/get_version.txt @@ -5,11 +5,11 @@ GET /version HTTP/1.1 HTTP/1.1 200 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 29 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /v1/version { diff --git a/docs/api/examples/post_portsudp.txt b/docs/api/examples/post_portsudp.txt index 828df022..3be4b74c 100644 --- a/docs/api/examples/post_portsudp.txt +++ b/docs/api/examples/post_portsudp.txt @@ -5,11 +5,11 @@ POST /ports/udp HTTP/1.1 HTTP/1.1 201 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 25 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /v1/ports/udp { diff --git a/docs/api/examples/post_projectsprojectidclose.txt b/docs/api/examples/post_projectsprojectidclose.txt index 66366b9c..bcc429c9 100644 --- a/docs/api/examples/post_projectsprojectidclose.txt +++ b/docs/api/examples/post_projectsprojectidclose.txt @@ -5,9 +5,9 @@ POST /projects/{project_id}/close HTTP/1.1 HTTP/1.1 204 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /v1/projects/{project_id}/close diff --git a/docs/api/examples/post_projectsprojectidcommit.txt b/docs/api/examples/post_projectsprojectidcommit.txt index b17f5a85..0b36f05d 100644 --- a/docs/api/examples/post_projectsprojectidcommit.txt +++ b/docs/api/examples/post_projectsprojectidcommit.txt @@ -5,9 +5,9 @@ POST /projects/{project_id}/commit HTTP/1.1 HTTP/1.1 204 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /v1/projects/{project_id}/commit diff --git a/docs/api/examples/post_projectsprojectidiouvms.txt b/docs/api/examples/post_projectsprojectidiouvms.txt new file mode 100644 index 00000000..72501ea2 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidiouvms.txt @@ -0,0 +1,35 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/iou/vms' -d '{"ethernet_adapters": 0, "iourc_path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2249/test_iou_create_with_params0/iourc", "l1_keepalives": true, "name": "PC TEST 1", "nvram": 512, "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2249/test_iou_create_with_params0/iou.bin", "ram": 1024, "serial_adapters": 4}' + +POST /projects/{project_id}/iou/vms HTTP/1.1 +{ + "ethernet_adapters": 0, + "iourc_path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2249/test_iou_create_with_params0/iourc", + "l1_keepalives": true, + "name": "PC TEST 1", + "nvram": 512, + "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2249/test_iou_create_with_params0/iou.bin", + "ram": 1024, + "serial_adapters": 4 +} + + +HTTP/1.1 201 +CONNECTION: keep-alive +CONTENT-LENGTH: 396 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/iou/vms + +{ + "console": 2000, + "ethernet_adapters": 0, + "l1_keepalives": true, + "name": "PC TEST 1", + "nvram": 512, + "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2249/test_iou_create_with_params0/iou.bin", + "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", + "ram": 1024, + "serial_adapters": 4, + "vm_id": "b6a61d51-7225-4f4a-904a-54c8a8917b87" +} diff --git a/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt b/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt new file mode 100644 index 00000000..5940e201 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -0,0 +1,21 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio' -d '{"ethernet_device": "eth0", "type": "nio_generic_ethernet"}' + +POST /projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio HTTP/1.1 +{ + "ethernet_device": "eth0", + "type": "nio_generic_ethernet" +} + + +HTTP/1.1 201 +CONNECTION: keep-alive +CONTENT-LENGTH: 69 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio + +{ + "ethernet_device": "eth0", + "type": "nio_generic_ethernet" +} diff --git a/docs/api/examples/post_projectsprojectidvirtualboxvms.txt b/docs/api/examples/post_projectsprojectidvirtualboxvms.txt new file mode 100644 index 00000000..9509fef5 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidvirtualboxvms.txt @@ -0,0 +1,30 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/virtualbox/vms' -d '{"linked_clone": false, "name": "VM1", "vmname": "VM1"}' + +POST /projects/{project_id}/virtualbox/vms HTTP/1.1 +{ + "linked_clone": false, + "name": "VM1", + "vmname": "VM1" +} + + +HTTP/1.1 201 +CONNECTION: keep-alive +CONTENT-LENGTH: 341 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/virtualbox/vms + +{ + "adapter_type": "Intel PRO/1000 MT Desktop (82540EM)", + "adapters": 0, + "console": 2000, + "enable_remote_console": false, + "headless": false, + "name": "VM1", + "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", + "use_any_adapter": false, + "vm_id": "7abbfc73-3d92-4e31-a99c-3b9519b2e0e8", + "vmname": "VM1" +} diff --git a/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidadaptersadapteriddportsportiddnio.txt b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidadaptersadapteriddportsportiddnio.txt new file mode 100644 index 00000000..a2be8b22 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidadaptersadapteriddportsportiddnio.txt @@ -0,0 +1,25 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/ports/{port_id:\d+}/nio' -d '{"lport": 4242, "rhost": "127.0.0.1", "rport": 4343, "type": "nio_udp"}' + +POST /projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/ports/{port_id:\d+}/nio HTTP/1.1 +{ + "lport": 4242, + "rhost": "127.0.0.1", + "rport": 4343, + "type": "nio_udp" +} + + +HTTP/1.1 201 +CONNECTION: keep-alive +CONTENT-LENGTH: 89 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/ports/{port_id:\d+}/nio + +{ + "lport": 4242, + "rhost": "127.0.0.1", + "rport": 4343, + "type": "nio_udp" +} diff --git a/docs/api/examples/post_projectsprojectidvpcsvms.txt b/docs/api/examples/post_projectsprojectidvpcsvms.txt new file mode 100644 index 00000000..d3309113 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidvpcsvms.txt @@ -0,0 +1,23 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/vpcs/vms' -d '{"name": "PC TEST 1"}' + +POST /projects/{project_id}/vpcs/vms HTTP/1.1 +{ + "name": "PC TEST 1" +} + + +HTTP/1.1 201 +CONNECTION: keep-alive +CONTENT-LENGTH: 187 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/vpcs/vms + +{ + "console": 2009, + "name": "PC TEST 1", + "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", + "startup_script": null, + "vm_id": "b481d455-16d3-450f-bcec-afdf1ec0c682" +} diff --git a/docs/api/examples/post_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt b/docs/api/examples/post_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt new file mode 100644 index 00000000..e55c4ace --- /dev/null +++ b/docs/api/examples/post_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -0,0 +1,25 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio' -d '{"lport": 4242, "rhost": "127.0.0.1", "rport": 4343, "type": "nio_udp"}' + +POST /projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio HTTP/1.1 +{ + "lport": 4242, + "rhost": "127.0.0.1", + "rport": 4343, + "type": "nio_udp" +} + + +HTTP/1.1 201 +CONNECTION: keep-alive +CONTENT-LENGTH: 89 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio + +{ + "lport": 4242, + "rhost": "127.0.0.1", + "rport": 4343, + "type": "nio_udp" +} diff --git a/docs/api/examples/post_version.txt b/docs/api/examples/post_version.txt index b6f654df..2f6c1452 100644 --- a/docs/api/examples/post_version.txt +++ b/docs/api/examples/post_version.txt @@ -7,11 +7,11 @@ POST /version HTTP/1.1 HTTP/1.1 200 -CONNECTION: close +CONNECTION: keep-alive CONTENT-LENGTH: 29 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /v1/version { diff --git a/docs/api/examples/put_projectsprojectid.txt b/docs/api/examples/put_projectsprojectid.txt index 43cf8961..0e7f4503 100644 --- a/docs/api/examples/put_projectsprojectid.txt +++ b/docs/api/examples/put_projectsprojectid.txt @@ -1,20 +1,20 @@ -curl -i -X PUT 'http://localhost:8000/projects/{project_id}' -d '{"path": "/tmp/pytest-48/test_update_path_project_non_l0"}' +curl -i -X PUT 'http://localhost:8000/projects/{project_id}' -d '{"path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2249/test_update_path_project_non_l0"}' PUT /projects/{project_id} HTTP/1.1 { - "path": "/tmp/pytest-48/test_update_path_project_non_l0" + "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2249/test_update_path_project_non_l0" } HTTP/1.1 403 -CONNECTION: close -CONTENT-LENGTH: 101 +CONNECTION: keep-alive +CONTENT-LENGTH: 100 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 aiohttp/0.13.1 +SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /v1/projects/{project_id} { - "message": "You are not allowed to modifiy the project directory location", + "message": "You are not allowed to modify the project directory location", "status": 403 } diff --git a/docs/api/v1interfaces.rst b/docs/api/v1interfaces.rst index fc32b2ee..73042f7d 100644 --- a/docs/api/v1interfaces.rst +++ b/docs/api/v1interfaces.rst @@ -1,10 +1,10 @@ /v1/interfaces ------------------------------------------------------------ +----------------------------------------------------------------------------------------------------------------- .. contents:: GET /v1/interfaces -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ List all the network interfaces available on the server Response status codes diff --git a/docs/api/v1portsudp.rst b/docs/api/v1portsudp.rst index 3451314a..69d90882 100644 --- a/docs/api/v1portsudp.rst +++ b/docs/api/v1portsudp.rst @@ -1,10 +1,10 @@ /v1/ports/udp ------------------------------------------------------------ +----------------------------------------------------------------------------------------------------------------- .. contents:: POST /v1/ports/udp -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Allocate an UDP port on the server Response status codes diff --git a/docs/api/v1projects.rst b/docs/api/v1projects.rst index c1fa393c..fadee814 100644 --- a/docs/api/v1projects.rst +++ b/docs/api/v1projects.rst @@ -1,10 +1,10 @@ /v1/projects ------------------------------------------------------------ +----------------------------------------------------------------------------------------------------------------- .. contents:: POST /v1/projects -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Create a new project on the server Response status codes @@ -17,7 +17,7 @@ Input - +
Name Mandatory Type Description
location string Base directory where the project should be created on remote server
path ['string', 'null'] Project directory
project_id ['string', 'null'] Project UUID
temporary boolean If project is a temporary project
diff --git a/docs/api/v1projectsprojectid.rst b/docs/api/v1projectsprojectid.rst index 0ad7f43e..4881beae 100644 --- a/docs/api/v1projectsprojectid.rst +++ b/docs/api/v1projectsprojectid.rst @@ -1,10 +1,10 @@ /v1/projects/{project_id} ------------------------------------------------------------ +----------------------------------------------------------------------------------------------------------------- .. contents:: GET /v1/projects/**{project_id}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Get project information Parameters @@ -30,7 +30,7 @@ Output PUT /v1/projects/**{project_id}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Update a project Parameters @@ -67,7 +67,7 @@ Output DELETE /v1/projects/**{project_id}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Delete a project from disk Parameters diff --git a/docs/api/v1projectsprojectidclose.rst b/docs/api/v1projectsprojectidclose.rst index e9b4feb8..e3d9e87c 100644 --- a/docs/api/v1projectsprojectidclose.rst +++ b/docs/api/v1projectsprojectidclose.rst @@ -1,10 +1,10 @@ /v1/projects/{project_id}/close ------------------------------------------------------------ +----------------------------------------------------------------------------------------------------------------- .. contents:: POST /v1/projects/**{project_id}**/close -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Close a project Parameters diff --git a/docs/api/v1projectsprojectidcommit.rst b/docs/api/v1projectsprojectidcommit.rst index 4909e569..a3e0aac5 100644 --- a/docs/api/v1projectsprojectidcommit.rst +++ b/docs/api/v1projectsprojectidcommit.rst @@ -1,10 +1,10 @@ /v1/projects/{project_id}/commit ------------------------------------------------------------ +----------------------------------------------------------------------------------------------------------------- .. contents:: POST /v1/projects/**{project_id}**/commit -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Write changes on disk Parameters diff --git a/docs/api/v1projectsprojectiddynamipsvms.rst b/docs/api/v1projectsprojectiddynamipsvms.rst new file mode 100644 index 00000000..0479e826 --- /dev/null +++ b/docs/api/v1projectsprojectiddynamipsvms.rst @@ -0,0 +1,116 @@ +/v1/projects/{project_id}/dynamips/vms +----------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/dynamips/vms +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Create a new Dynamips VM instance + +Parameters +********** +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **201**: Instance created +- **409**: Conflict + +Input +******* +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name Mandatory Type Description
aux integer auxiliary console TCP port
clock_divisor integer clock divisor
confreg string configuration register
console integer console TCP port
disk0 integer disk0 size in MB
disk1 integer disk1 size in MB
dynamips_id integer ID to use with Dynamips
exec_area integer exec area value
idlemax integer idlemax value
idlepc string Idle-PC value
idlesleep integer idlesleep value
image string path to the IOS image
iomem integer I/O memory percentage
mac_addr string base MAC address
midplane enum Possible values: std, vxr
mmap boolean MMAP feature
name string Dynamips VM instance name
npe enum Possible values: npe-100, npe-150, npe-175, npe-200, npe-225, npe-300, npe-400, npe-g2
nvram integer amount of NVRAM in KB
platform string platform
power_supplies array Power supplies status
private_config string path to the IOS private configuration file
private_config_base64 string private configuration base64 encoded
ram integer amount of RAM in MB
sensors array Temperature sensors
slot0 Network module slot 0
slot1 Network module slot 1
slot2 Network module slot 2
slot3 Network module slot 3
slot4 Network module slot 4
slot5 Network module slot 5
slot6 Network module slot 6
sparsemem boolean sparse memory feature
startup_config string path to the IOS startup configuration file
startup_config_base64 string startup configuration base64 encoded
system_id string system ID
vm_id Dynamips VM instance identifier
wic0 Network module WIC slot 0
wic1 Network module WIC slot 0
wic2 Network module WIC slot 0
+ +Output +******* +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name Mandatory Type Description
aux integer auxiliary console TCP port
clock_divisor integer clock divisor
confreg string configuration register
console integer console TCP port
disk0 integer disk0 size in MB
disk1 integer disk1 size in MB
dynamips_id integer ID to use with Dynamips
exec_area integer exec area value
idlemax integer idlemax value
idlepc string Idle-PC value
idlesleep integer idlesleep value
image string path to the IOS image
iomem integer I/O memory percentage
mac_addr string base MAC address
midplane enum Possible values: std, vxr
mmap boolean MMAP feature
name string Dynamips VM instance name
npe enum Possible values: npe-100, npe-150, npe-175, npe-200, npe-225, npe-300, npe-400, npe-g2
nvram integer amount of NVRAM in KB
platform string platform
power_supplies array Power supplies status
private_config string path to the IOS private configuration file
private_config_base64 string private configuration base64 encoded
project_id string Project UUID
ram integer amount of RAM in MB
sensors array Temperature sensors
slot0 Network module slot 0
slot1 Network module slot 1
slot2 Network module slot 2
slot3 Network module slot 3
slot4 Network module slot 4
slot5 Network module slot 5
slot6 Network module slot 6
sparsemem boolean sparse memory feature
startup_config string path to the IOS startup configuration file
startup_config_base64 string startup configuration base64 encoded
system_id string system ID
vm_id string Dynamips router instance UUID
wic0 Network module WIC slot 0
wic1 Network module WIC slot 0
wic2 Network module WIC slot 0
+ diff --git a/docs/api/v1projectsprojectiddynamipsvmsvmid.rst b/docs/api/v1projectsprojectiddynamipsvmsvmid.rst new file mode 100644 index 00000000..07b207ac --- /dev/null +++ b/docs/api/v1projectsprojectiddynamipsvmsvmid.rst @@ -0,0 +1,197 @@ +/v1/projects/{project_id}/dynamips/vms/{vm_id} +----------------------------------------------------------------------------------------------------------------- + +.. contents:: + +GET /v1/projects/**{project_id}**/dynamips/vms/**{vm_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Get a Dynamips VM instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **200**: Success +- **400**: Invalid request +- **404**: Instance doesn't exist + +Output +******* +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name Mandatory Type Description
aux integer auxiliary console TCP port
clock_divisor integer clock divisor
confreg string configuration register
console integer console TCP port
disk0 integer disk0 size in MB
disk1 integer disk1 size in MB
dynamips_id integer ID to use with Dynamips
exec_area integer exec area value
idlemax integer idlemax value
idlepc string Idle-PC value
idlesleep integer idlesleep value
image string path to the IOS image
iomem integer I/O memory percentage
mac_addr string base MAC address
midplane enum Possible values: std, vxr
mmap boolean MMAP feature
name string Dynamips VM instance name
npe enum Possible values: npe-100, npe-150, npe-175, npe-200, npe-225, npe-300, npe-400, npe-g2
nvram integer amount of NVRAM in KB
platform string platform
power_supplies array Power supplies status
private_config string path to the IOS private configuration file
private_config_base64 string private configuration base64 encoded
project_id string Project UUID
ram integer amount of RAM in MB
sensors array Temperature sensors
slot0 Network module slot 0
slot1 Network module slot 1
slot2 Network module slot 2
slot3 Network module slot 3
slot4 Network module slot 4
slot5 Network module slot 5
slot6 Network module slot 6
sparsemem boolean sparse memory feature
startup_config string path to the IOS startup configuration file
startup_config_base64 string startup configuration base64 encoded
system_id string system ID
vm_id string Dynamips router instance UUID
wic0 Network module WIC slot 0
wic1 Network module WIC slot 0
wic2 Network module WIC slot 0
+ + +PUT /v1/projects/**{project_id}**/dynamips/vms/**{vm_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Update a Dynamips VM instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **200**: Instance updated +- **400**: Invalid request +- **404**: Instance doesn't exist +- **409**: Conflict + +Input +******* +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name Mandatory Type Description
aux integer auxiliary console TCP port
clock_divisor integer clock divisor
confreg string configuration register
console integer console TCP port
disk0 integer disk0 size in MB
disk1 integer disk1 size in MB
exec_area integer exec area value
idlemax integer idlemax value
idlepc string Idle-PC value
idlesleep integer idlesleep value
image string path to the IOS image
iomem integer I/O memory percentage
mac_addr string base MAC address
midplane enum Possible values: std, vxr
mmap boolean MMAP feature
name string Dynamips VM instance name
npe enum Possible values: npe-100, npe-150, npe-175, npe-200, npe-225, npe-300, npe-400, npe-g2
nvram integer amount of NVRAM in KB
platform string platform
power_supplies array Power supplies status
private_config string path to the IOS private configuration file
private_config_base64 string private configuration base64 encoded
ram integer amount of RAM in MB
sensors array Temperature sensors
slot0 Network module slot 0
slot1 Network module slot 1
slot2 Network module slot 2
slot3 Network module slot 3
slot4 Network module slot 4
slot5 Network module slot 5
slot6 Network module slot 6
sparsemem boolean sparse memory feature
startup_config string path to the IOS startup configuration file
startup_config_base64 string startup configuration base64 encoded
system_id string system ID
wic0 Network module WIC slot 0
wic1 Network module WIC slot 0
wic2 Network module WIC slot 0
+ +Output +******* +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name Mandatory Type Description
aux integer auxiliary console TCP port
clock_divisor integer clock divisor
confreg string configuration register
console integer console TCP port
disk0 integer disk0 size in MB
disk1 integer disk1 size in MB
dynamips_id integer ID to use with Dynamips
exec_area integer exec area value
idlemax integer idlemax value
idlepc string Idle-PC value
idlesleep integer idlesleep value
image string path to the IOS image
iomem integer I/O memory percentage
mac_addr string base MAC address
midplane enum Possible values: std, vxr
mmap boolean MMAP feature
name string Dynamips VM instance name
npe enum Possible values: npe-100, npe-150, npe-175, npe-200, npe-225, npe-300, npe-400, npe-g2
nvram integer amount of NVRAM in KB
platform string platform
power_supplies array Power supplies status
private_config string path to the IOS private configuration file
private_config_base64 string private configuration base64 encoded
project_id string Project UUID
ram integer amount of RAM in MB
sensors array Temperature sensors
slot0 Network module slot 0
slot1 Network module slot 1
slot2 Network module slot 2
slot3 Network module slot 3
slot4 Network module slot 4
slot5 Network module slot 5
slot6 Network module slot 6
sparsemem boolean sparse memory feature
startup_config string path to the IOS startup configuration file
startup_config_base64 string startup configuration base64 encoded
system_id string system ID
vm_id string Dynamips router instance UUID
wic0 Network module WIC slot 0
wic1 Network module WIC slot 0
wic2 Network module WIC slot 0
+ + +DELETE /v1/projects/**{project_id}**/dynamips/vms/**{vm_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Delete a Dynamips VM instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance deleted + diff --git a/docs/api/v1projectsprojectiddynamipsvmsvmidreload.rst b/docs/api/v1projectsprojectiddynamipsvmsvmidreload.rst new file mode 100644 index 00000000..a1544bf0 --- /dev/null +++ b/docs/api/v1projectsprojectiddynamipsvmsvmidreload.rst @@ -0,0 +1,20 @@ +/v1/projects/{project_id}/dynamips/vms/{vm_id}/reload +----------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/dynamips/vms/**{vm_id}**/reload +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Reload a Dynamips VM instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance reloaded + diff --git a/docs/api/v1projectsprojectiddynamipsvmsvmidresume.rst b/docs/api/v1projectsprojectiddynamipsvmsvmidresume.rst new file mode 100644 index 00000000..9e5ca67c --- /dev/null +++ b/docs/api/v1projectsprojectiddynamipsvmsvmidresume.rst @@ -0,0 +1,20 @@ +/v1/projects/{project_id}/dynamips/vms/{vm_id}/resume +----------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/dynamips/vms/**{vm_id}**/resume +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Resume a suspended Dynamips VM instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance resumed + diff --git a/docs/api/v1projectsprojectiddynamipsvmsvmidstart.rst b/docs/api/v1projectsprojectiddynamipsvmsvmidstart.rst new file mode 100644 index 00000000..446d6bca --- /dev/null +++ b/docs/api/v1projectsprojectiddynamipsvmsvmidstart.rst @@ -0,0 +1,20 @@ +/v1/projects/{project_id}/dynamips/vms/{vm_id}/start +----------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/dynamips/vms/**{vm_id}**/start +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Start a Dynamips VM instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance started + diff --git a/docs/api/v1projectsprojectiddynamipsvmsvmidstop.rst b/docs/api/v1projectsprojectiddynamipsvmsvmidstop.rst new file mode 100644 index 00000000..9663fc78 --- /dev/null +++ b/docs/api/v1projectsprojectiddynamipsvmsvmidstop.rst @@ -0,0 +1,20 @@ +/v1/projects/{project_id}/dynamips/vms/{vm_id}/stop +----------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/dynamips/vms/**{vm_id}**/stop +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Stop a Dynamips VM instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance stopped + diff --git a/docs/api/v1projectsprojectiddynamipsvmsvmidsuspend.rst b/docs/api/v1projectsprojectiddynamipsvmsvmidsuspend.rst new file mode 100644 index 00000000..d53b213f --- /dev/null +++ b/docs/api/v1projectsprojectiddynamipsvmsvmidsuspend.rst @@ -0,0 +1,20 @@ +/v1/projects/{project_id}/dynamips/vms/{vm_id}/suspend +----------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/dynamips/vms/**{vm_id}**/suspend +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Suspend a Dynamips VM instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance suspended + diff --git a/docs/api/v1projectsprojectidiouvms.rst b/docs/api/v1projectsprojectidiouvms.rst new file mode 100644 index 00000000..a462804a --- /dev/null +++ b/docs/api/v1projectsprojectidiouvms.rst @@ -0,0 +1,55 @@ +/v1/projects/{project_id}/iou/vms +----------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/iou/vms +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Create a new IOU instance + +Parameters +********** +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **201**: Instance created +- **409**: Conflict + +Input +******* +.. raw:: html + + + + + + + + + + + + + +
Name Mandatory Type Description
console ['integer', 'null'] console TCP port
ethernet_adapters integer How many ethernet adapters are connected to the IOU
iourc_path string Path of iourc
l1_keepalives ['boolean', 'null'] Always up ethernet interface
name string IOU VM name
nvram ['integer', 'null'] Allocated NVRAM KB
path string Path of iou binary
ram ['integer', 'null'] Allocated RAM MB
serial_adapters integer How many serial adapters are connected to the IOU
vm_id IOU VM identifier
+ +Output +******* +.. raw:: html + + + + + + + + + + + + + +
Name Mandatory Type Description
console integer console TCP port
ethernet_adapters integer How many ethernet adapters are connected to the IOU
l1_keepalives boolean Always up ethernet interface
name string IOU VM name
nvram integer Allocated NVRAM KB
path string Path of iou binary
project_id string Project UUID
ram integer Allocated RAM MB
serial_adapters integer How many serial adapters are connected to the IOU
vm_id string IOU VM UUID
+ diff --git a/docs/api/v1projectsprojectidiouvmsvmid.rst b/docs/api/v1projectsprojectidiouvmsvmid.rst new file mode 100644 index 00000000..30935bbd --- /dev/null +++ b/docs/api/v1projectsprojectidiouvmsvmid.rst @@ -0,0 +1,107 @@ +/v1/projects/{project_id}/iou/vms/{vm_id} +----------------------------------------------------------------------------------------------------------------- + +.. contents:: + +GET /v1/projects/**{project_id}**/iou/vms/**{vm_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Get a IOU instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **200**: Success +- **400**: Invalid request +- **404**: Instance doesn't exist + +Output +******* +.. raw:: html + + + + + + + + + + + + + +
Name Mandatory Type Description
console integer console TCP port
ethernet_adapters integer How many ethernet adapters are connected to the IOU
l1_keepalives boolean Always up ethernet interface
name string IOU VM name
nvram integer Allocated NVRAM KB
path string Path of iou binary
project_id string Project UUID
ram integer Allocated RAM MB
serial_adapters integer How many serial adapters are connected to the IOU
vm_id string IOU VM UUID
+ + +PUT /v1/projects/**{project_id}**/iou/vms/**{vm_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Update a IOU instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **200**: Instance updated +- **400**: Invalid request +- **404**: Instance doesn't exist +- **409**: Conflict + +Input +******* +.. raw:: html + + + + + + + + + + + + + +
Name Mandatory Type Description
console ['integer', 'null'] console TCP port
ethernet_adapters ['integer', 'null'] How many ethernet adapters are connected to the IOU
initial_config ['string', 'null'] Initial configuration path
iourc_path ['string', 'null'] Path of iourc
l1_keepalives ['boolean', 'null'] Always up ethernet interface
name ['string', 'null'] IOU VM name
nvram ['integer', 'null'] Allocated NVRAM KB
path ['string', 'null'] Path of iou binary
ram ['integer', 'null'] Allocated RAM MB
serial_adapters ['integer', 'null'] How many serial adapters are connected to the IOU
+ +Output +******* +.. raw:: html + + + + + + + + + + + + + +
Name Mandatory Type Description
console integer console TCP port
ethernet_adapters integer How many ethernet adapters are connected to the IOU
l1_keepalives boolean Always up ethernet interface
name string IOU VM name
nvram integer Allocated NVRAM KB
path string Path of iou binary
project_id string Project UUID
ram integer Allocated RAM MB
serial_adapters integer How many serial adapters are connected to the IOU
vm_id string IOU VM UUID
+ + +DELETE /v1/projects/**{project_id}**/iou/vms/**{vm_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Delete a IOU instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance deleted + diff --git a/docs/api/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst new file mode 100644 index 00000000..68065488 --- /dev/null +++ b/docs/api/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -0,0 +1,40 @@ +/v1/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio +----------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/iou/vms/**{vm_id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Add a NIO to a IOU instance + +Parameters +********** +- **port_number**: Port where the nio should be added +- **adapter_number**: Network adapter where the nio is located +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **201**: NIO created +- **404**: Instance doesn't exist + + +DELETE /v1/projects/**{project_id}**/iou/vms/**{vm_id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Remove a NIO from a IOU instance + +Parameters +********** +- **port_number**: Port from where the nio should be removed +- **adapter_number**: Network adapter where the nio is located +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: NIO deleted + diff --git a/docs/api/v1projectsprojectidiouvmsvmidreload.rst b/docs/api/v1projectsprojectidiouvmsvmidreload.rst new file mode 100644 index 00000000..33c1d767 --- /dev/null +++ b/docs/api/v1projectsprojectidiouvmsvmidreload.rst @@ -0,0 +1,20 @@ +/v1/projects/{project_id}/iou/vms/{vm_id}/reload +----------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/iou/vms/**{vm_id}**/reload +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Reload a IOU instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance reloaded + diff --git a/docs/api/v1projectsprojectidiouvmsvmidstart.rst b/docs/api/v1projectsprojectidiouvmsvmidstart.rst new file mode 100644 index 00000000..3956f666 --- /dev/null +++ b/docs/api/v1projectsprojectidiouvmsvmidstart.rst @@ -0,0 +1,20 @@ +/v1/projects/{project_id}/iou/vms/{vm_id}/start +----------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/iou/vms/**{vm_id}**/start +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Start a IOU instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance started + diff --git a/docs/api/v1projectsprojectidiouvmsvmidstop.rst b/docs/api/v1projectsprojectidiouvmsvmidstop.rst new file mode 100644 index 00000000..860b566d --- /dev/null +++ b/docs/api/v1projectsprojectidiouvmsvmidstop.rst @@ -0,0 +1,20 @@ +/v1/projects/{project_id}/iou/vms/{vm_id}/stop +----------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/iou/vms/**{vm_id}**/stop +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Stop a IOU instance + +Parameters +********** +- **vm_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance stopped + diff --git a/docs/api/v1projectidvirtualboxvms.rst b/docs/api/v1projectsprojectidvirtualboxvms.rst similarity index 83% rename from docs/api/v1projectidvirtualboxvms.rst rename to docs/api/v1projectsprojectidvirtualboxvms.rst index 4b47e6f0..2e0ce096 100644 --- a/docs/api/v1projectidvirtualboxvms.rst +++ b/docs/api/v1projectsprojectidvirtualboxvms.rst @@ -1,10 +1,10 @@ -/v1/{project_id}/virtualbox/vms ------------------------------------------------------------ +/v1/projects/{project_id}/virtualbox/vms +----------------------------------------------------------------------------------------------------------------- .. contents:: -POST /v1/**{project_id}**/virtualbox/vms -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +POST /v1/projects/**{project_id}**/virtualbox/vms +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Create a new VirtualBox VM instance Parameters @@ -23,7 +23,6 @@ Input - @@ -31,7 +30,8 @@ Input - + +
Name Mandatory Type Description
adapter_start_index integer adapter index from which to start using adapters
adapter_type string VirtualBox adapter type
adapters integer number of adapters
console integer console TCP port
headless boolean headless mode
linked_clone boolean either the VM is a linked clone or not
name string VirtualBox VM instance name
vm_id string VirtualBox VM instance identifier
use_any_adapter boolean allow GNS3 to use any VirtualBox adapter
vm_id VirtualBox VM instance identifier
vmname string VirtualBox VM name (in VirtualBox itself)
@@ -41,7 +41,6 @@ Output - @@ -49,6 +48,7 @@ Output +
Name Mandatory Type Description
adapter_start_index integer adapter index from which to start using adapters
adapter_type string VirtualBox adapter type
adapters integer number of adapters
console integer console TCP port
headless boolean headless mode
name string VirtualBox VM instance name
project_id string Project UUID
use_any_adapter boolean allow GNS3 to use any VirtualBox adapter
vm_id string VirtualBox VM instance UUID
vmname string VirtualBox VM name (in VirtualBox itself)
diff --git a/docs/api/v1projectidvirtualboxvmsvmid.rst b/docs/api/v1projectsprojectidvirtualboxvmsvmid.rst similarity index 84% rename from docs/api/v1projectidvirtualboxvmsvmid.rst rename to docs/api/v1projectsprojectidvirtualboxvmsvmid.rst index 18c50005..11274204 100644 --- a/docs/api/v1projectidvirtualboxvmsvmid.rst +++ b/docs/api/v1projectsprojectidvirtualboxvmsvmid.rst @@ -1,10 +1,10 @@ -/v1/{project_id}/virtualbox/vms/{vm_id} ------------------------------------------------------------ +/v1/projects/{project_id}/virtualbox/vms/{vm_id} +----------------------------------------------------------------------------------------------------------------- .. contents:: -GET /v1/**{project_id}**/virtualbox/vms/**{vm_id}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +GET /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Get a VirtualBox VM instance Parameters @@ -24,7 +24,6 @@ Output - @@ -32,13 +31,14 @@ Output +
Name Mandatory Type Description
adapter_start_index integer adapter index from which to start using adapters
adapter_type string VirtualBox adapter type
adapters integer number of adapters
console integer console TCP port
headless boolean headless mode
name string VirtualBox VM instance name
project_id string Project UUID
use_any_adapter boolean allow GNS3 to use any VirtualBox adapter
vm_id string VirtualBox VM instance UUID
vmname string VirtualBox VM name (in VirtualBox itself)
-PUT /v1/**{project_id}**/virtualbox/vms/**{vm_id}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +PUT /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Update a VirtualBox VM instance Parameters @@ -59,13 +59,13 @@ Input - +
Name Mandatory Type Description
adapter_start_index integer adapter index from which to start using adapters
adapter_type string VirtualBox adapter type
adapters integer number of adapters
console integer console TCP port
enable_remote_console boolean enable the remote console
headless boolean headless mode
name string VirtualBox VM instance name
use_any_adapter boolean allow GNS3 to use any VirtualBox adapter
vmname string VirtualBox VM name (in VirtualBox itself)
@@ -75,7 +75,6 @@ Output - @@ -83,13 +82,14 @@ Output +
Name Mandatory Type Description
adapter_start_index integer adapter index from which to start using adapters
adapter_type string VirtualBox adapter type
adapters integer number of adapters
console integer console TCP port
headless boolean headless mode
name string VirtualBox VM instance name
project_id string Project UUID
use_any_adapter boolean allow GNS3 to use any VirtualBox adapter
vm_id string VirtualBox VM instance UUID
vmname string VirtualBox VM name (in VirtualBox itself)
-DELETE /v1/**{project_id}**/virtualbox/vms/**{vm_id}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +DELETE /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Delete a VirtualBox VM instance Parameters diff --git a/docs/api/v1projectidvirtualboxvmsvmidadaptersadapteriddnio.rst b/docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddportsportiddnio.rst similarity index 52% rename from docs/api/v1projectidvirtualboxvmsvmidadaptersadapteriddnio.rst rename to docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddportsportiddnio.rst index 880222ad..5104eb2d 100644 --- a/docs/api/v1projectidvirtualboxvmsvmidadaptersadapteriddnio.rst +++ b/docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddportsportiddnio.rst @@ -1,17 +1,18 @@ -/v1/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/nio ------------------------------------------------------------ +/v1/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/ports/{port_id:\d+}/nio +----------------------------------------------------------------------------------------------------------------- .. contents:: -POST /v1/**{project_id}**/virtualbox/vms/**{vm_id}**/adapters/**{adapter_id:\d+}**/nio -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +POST /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}**/adapters/**{adapter_id:\d+}**/ports/**{port_id:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Add a NIO to a VirtualBox VM instance Parameters ********** +- **port_id**: Port in the adapter (always 0 for virtualbox) - **vm_id**: UUID for the instance -- **project_id**: UUID for the project - **adapter_id**: Adapter where the nio should be added +- **project_id**: UUID for the project Response status codes ********************** @@ -20,15 +21,16 @@ Response status codes - **404**: Instance doesn't exist -DELETE /v1/**{project_id}**/virtualbox/vms/**{vm_id}**/adapters/**{adapter_id:\d+}**/nio -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +DELETE /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}**/adapters/**{adapter_id:\d+}**/ports/**{port_id:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Remove a NIO from a VirtualBox VM instance Parameters ********** +- **port_id**: Port in the adapter (always 0 for virtualbox) - **vm_id**: UUID for the instance -- **project_id**: UUID for the project - **adapter_id**: Adapter from where the nio should be removed +- **project_id**: UUID for the project Response status codes ********************** diff --git a/docs/api/v1projectidvirtualboxvmidcaptureadapteriddstart.rst b/docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddstartcapture.rst similarity index 53% rename from docs/api/v1projectidvirtualboxvmidcaptureadapteriddstart.rst rename to docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddstartcapture.rst index 2faa7709..ceab89f1 100644 --- a/docs/api/v1projectidvirtualboxvmidcaptureadapteriddstart.rst +++ b/docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddstartcapture.rst @@ -1,17 +1,17 @@ -/v1/{project_id}/virtualbox/{vm_id}/capture/{adapter_id:\d+}/start ------------------------------------------------------------ +/v1/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/start_capture +----------------------------------------------------------------------------------------------------------------- .. contents:: -POST /v1/**{project_id}**/virtualbox/**{vm_id}**/capture/**{adapter_id:\d+}**/start -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +POST /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}**/adapters/**{adapter_id:\d+}**/start_capture +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Start a packet capture on a VirtualBox VM instance Parameters ********** - **vm_id**: UUID for the instance -- **project_id**: UUID for the project - **adapter_id**: Adapter to start a packet capture +- **project_id**: UUID for the project Response status codes ********************** @@ -25,6 +25,6 @@ Input - +
Name Mandatory Type Description
capture_filename string Capture file name
capture_file_name string Capture file name
diff --git a/docs/api/v1projectidvirtualboxvmidcaptureadapteriddstop.rst b/docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddstopcapture.rst similarity index 52% rename from docs/api/v1projectidvirtualboxvmidcaptureadapteriddstop.rst rename to docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddstopcapture.rst index ddacddfd..4c2a6072 100644 --- a/docs/api/v1projectidvirtualboxvmidcaptureadapteriddstop.rst +++ b/docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddstopcapture.rst @@ -1,17 +1,17 @@ -/v1/{project_id}/virtualbox/{vm_id}/capture/{adapter_id:\d+}/stop ------------------------------------------------------------ +/v1/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/stop_capture +----------------------------------------------------------------------------------------------------------------- .. contents:: -POST /v1/**{project_id}**/virtualbox/**{vm_id}**/capture/**{adapter_id:\d+}**/stop -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +POST /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}**/adapters/**{adapter_id:\d+}**/stop_capture +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Stop a packet capture on a VirtualBox VM instance Parameters ********** - **vm_id**: UUID for the instance -- **project_id**: UUID for the project - **adapter_id**: Adapter to stop a packet capture +- **project_id**: UUID for the project Response status codes ********************** diff --git a/docs/api/v1projectidvirtualboxvmsvmidreload.rst b/docs/api/v1projectsprojectidvirtualboxvmsvmidreload.rst similarity index 53% rename from docs/api/v1projectidvirtualboxvmsvmidreload.rst rename to docs/api/v1projectsprojectidvirtualboxvmsvmidreload.rst index 48387994..d32b9e02 100644 --- a/docs/api/v1projectidvirtualboxvmsvmidreload.rst +++ b/docs/api/v1projectsprojectidvirtualboxvmsvmidreload.rst @@ -1,10 +1,10 @@ -/v1/{project_id}/virtualbox/vms/{vm_id}/reload ------------------------------------------------------------ +/v1/projects/{project_id}/virtualbox/vms/{vm_id}/reload +----------------------------------------------------------------------------------------------------------------- .. contents:: -POST /v1/**{project_id}**/virtualbox/vms/**{vm_id}**/reload -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +POST /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}**/reload +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Reload a VirtualBox VM instance Parameters diff --git a/docs/api/v1projectidvirtualboxvmsvmidresume.rst b/docs/api/v1projectsprojectidvirtualboxvmsvmidresume.rst similarity index 53% rename from docs/api/v1projectidvirtualboxvmsvmidresume.rst rename to docs/api/v1projectsprojectidvirtualboxvmsvmidresume.rst index 87a3aa85..7d82060f 100644 --- a/docs/api/v1projectidvirtualboxvmsvmidresume.rst +++ b/docs/api/v1projectsprojectidvirtualboxvmsvmidresume.rst @@ -1,10 +1,10 @@ -/v1/{project_id}/virtualbox/vms/{vm_id}/resume ------------------------------------------------------------ +/v1/projects/{project_id}/virtualbox/vms/{vm_id}/resume +----------------------------------------------------------------------------------------------------------------- .. contents:: -POST /v1/**{project_id}**/virtualbox/vms/**{vm_id}**/resume -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +POST /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}**/resume +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Resume a suspended VirtualBox VM instance Parameters diff --git a/docs/api/v1projectidvirtualboxvmsvmidstart.rst b/docs/api/v1projectsprojectidvirtualboxvmsvmidstart.rst similarity index 53% rename from docs/api/v1projectidvirtualboxvmsvmidstart.rst rename to docs/api/v1projectsprojectidvirtualboxvmsvmidstart.rst index 9a4672a3..20a30a8a 100644 --- a/docs/api/v1projectidvirtualboxvmsvmidstart.rst +++ b/docs/api/v1projectsprojectidvirtualboxvmsvmidstart.rst @@ -1,10 +1,10 @@ -/v1/{project_id}/virtualbox/vms/{vm_id}/start ------------------------------------------------------------ +/v1/projects/{project_id}/virtualbox/vms/{vm_id}/start +----------------------------------------------------------------------------------------------------------------- .. contents:: -POST /v1/**{project_id}**/virtualbox/vms/**{vm_id}**/start -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +POST /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}**/start +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Start a VirtualBox VM instance Parameters diff --git a/docs/api/v1projectidvirtualboxvmsvmidstop.rst b/docs/api/v1projectsprojectidvirtualboxvmsvmidstop.rst similarity index 53% rename from docs/api/v1projectidvirtualboxvmsvmidstop.rst rename to docs/api/v1projectsprojectidvirtualboxvmsvmidstop.rst index 70acc079..cafcbc1c 100644 --- a/docs/api/v1projectidvirtualboxvmsvmidstop.rst +++ b/docs/api/v1projectsprojectidvirtualboxvmsvmidstop.rst @@ -1,10 +1,10 @@ -/v1/{project_id}/virtualbox/vms/{vm_id}/stop ------------------------------------------------------------ +/v1/projects/{project_id}/virtualbox/vms/{vm_id}/stop +----------------------------------------------------------------------------------------------------------------- .. contents:: -POST /v1/**{project_id}**/virtualbox/vms/**{vm_id}**/stop -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +POST /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}**/stop +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Stop a VirtualBox VM instance Parameters diff --git a/docs/api/v1projectidvirtualboxvmsvmidsuspend.rst b/docs/api/v1projectsprojectidvirtualboxvmsvmidsuspend.rst similarity index 53% rename from docs/api/v1projectidvirtualboxvmsvmidsuspend.rst rename to docs/api/v1projectsprojectidvirtualboxvmsvmidsuspend.rst index e9caa7d9..0652378e 100644 --- a/docs/api/v1projectidvirtualboxvmsvmidsuspend.rst +++ b/docs/api/v1projectsprojectidvirtualboxvmsvmidsuspend.rst @@ -1,10 +1,10 @@ -/v1/{project_id}/virtualbox/vms/{vm_id}/suspend ------------------------------------------------------------ +/v1/projects/{project_id}/virtualbox/vms/{vm_id}/suspend +----------------------------------------------------------------------------------------------------------------- .. contents:: -POST /v1/**{project_id}**/virtualbox/vms/**{vm_id}**/suspend -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +POST /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}**/suspend +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Suspend a VirtualBox VM instance Parameters diff --git a/docs/api/v1projectidvpcsvms.rst b/docs/api/v1projectsprojectidvpcsvms.rst similarity index 83% rename from docs/api/v1projectidvpcsvms.rst rename to docs/api/v1projectsprojectidvpcsvms.rst index c6a3a9e4..7dfb6bc9 100644 --- a/docs/api/v1projectidvpcsvms.rst +++ b/docs/api/v1projectsprojectidvpcsvms.rst @@ -1,10 +1,10 @@ -/v1/{project_id}/vpcs/vms ------------------------------------------------------------ +/v1/projects/{project_id}/vpcs/vms +----------------------------------------------------------------------------------------------------------------- .. contents:: -POST /v1/**{project_id}**/vpcs/vms -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +POST /v1/projects/**{project_id}**/vpcs/vms +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Create a new VPCS instance Parameters @@ -26,7 +26,7 @@ Input console ['integer', 'null'] console TCP port name ✔ string VPCS VM name startup_script ['string', 'null'] Content of the VPCS startup script - vm_id string VPCS VM identifier + vm_id VPCS VM identifier Output @@ -38,7 +38,6 @@ Output console ✔ integer console TCP port name ✔ string VPCS VM name project_id ✔ string Project UUID - script_file ['string', 'null'] VPCS startup script startup_script ['string', 'null'] Content of the VPCS startup script vm_id ✔ string VPCS VM UUID diff --git a/docs/api/v1projectidvpcsvmsvmid.rst b/docs/api/v1projectsprojectidvpcsvmsvmid.rst similarity index 85% rename from docs/api/v1projectidvpcsvmsvmid.rst rename to docs/api/v1projectsprojectidvpcsvmsvmid.rst index ca80efaa..50268c7b 100644 --- a/docs/api/v1projectidvpcsvmsvmid.rst +++ b/docs/api/v1projectsprojectidvpcsvmsvmid.rst @@ -1,10 +1,10 @@ -/v1/{project_id}/vpcs/vms/{vm_id} ------------------------------------------------------------ +/v1/projects/{project_id}/vpcs/vms/{vm_id} +----------------------------------------------------------------------------------------------------------------- .. contents:: -GET /v1/**{project_id}**/vpcs/vms/**{vm_id}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +GET /v1/projects/**{project_id}**/vpcs/vms/**{vm_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Get a VPCS instance Parameters @@ -27,14 +27,13 @@ Output console ✔ integer console TCP port name ✔ string VPCS VM name project_id ✔ string Project UUID - script_file ['string', 'null'] VPCS startup script startup_script ['string', 'null'] Content of the VPCS startup script vm_id ✔ string VPCS VM UUID -PUT /v1/**{project_id}**/vpcs/vms/**{vm_id}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +PUT /v1/projects/**{project_id}**/vpcs/vms/**{vm_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Update a VPCS instance Parameters @@ -69,14 +68,13 @@ Output console ✔ integer console TCP port name ✔ string VPCS VM name project_id ✔ string Project UUID - script_file ['string', 'null'] VPCS startup script startup_script ['string', 'null'] Content of the VPCS startup script vm_id ✔ string VPCS VM UUID -DELETE /v1/**{project_id}**/vpcs/vms/**{vm_id}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +DELETE /v1/projects/**{project_id}**/vpcs/vms/**{vm_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Delete a VPCS instance Parameters diff --git a/docs/api/v1projectidvpcsvmsvmidportsportnumberdnio.rst b/docs/api/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst similarity index 51% rename from docs/api/v1projectidvpcsvmsvmidportsportnumberdnio.rst rename to docs/api/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst index 0abccb0b..4e7cc653 100644 --- a/docs/api/v1projectidvpcsvmsvmidportsportnumberdnio.rst +++ b/docs/api/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -1,17 +1,18 @@ -/v1/{project_id}/vpcs/vms/{vm_id}/ports/{port_number:\d+}/nio ------------------------------------------------------------ +/v1/projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio +----------------------------------------------------------------------------------------------------------------- .. contents:: -POST /v1/**{project_id}**/vpcs/vms/**{vm_id}**/ports/**{port_number:\d+}**/nio -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +POST /v1/projects/**{project_id}**/vpcs/vms/**{vm_id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Add a NIO to a VPCS instance Parameters ********** +- **port_number**: Port where the nio should be added +- **adapter_number**: Network adapter where the nio is located - **vm_id**: UUID for the instance - **project_id**: UUID for the project -- **port_number**: Port where the nio should be added Response status codes ********************** @@ -20,15 +21,16 @@ Response status codes - **404**: Instance doesn't exist -DELETE /v1/**{project_id}**/vpcs/vms/**{vm_id}**/ports/**{port_number:\d+}**/nio -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +DELETE /v1/projects/**{project_id}**/vpcs/vms/**{vm_id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Remove a NIO from a VPCS instance Parameters ********** +- **port_number**: Port from where the nio should be removed +- **adapter_number**: Network adapter where the nio is located - **vm_id**: UUID for the instance - **project_id**: UUID for the project -- **port_number**: Port from where the nio should be removed Response status codes ********************** diff --git a/docs/api/v1projectidvpcsvmsvmidreload.rst b/docs/api/v1projectsprojectidvpcsvmsvmidreload.rst similarity index 53% rename from docs/api/v1projectidvpcsvmsvmidreload.rst rename to docs/api/v1projectsprojectidvpcsvmsvmidreload.rst index b5357561..33b4d868 100644 --- a/docs/api/v1projectidvpcsvmsvmidreload.rst +++ b/docs/api/v1projectsprojectidvpcsvmsvmidreload.rst @@ -1,10 +1,10 @@ -/v1/{project_id}/vpcs/vms/{vm_id}/reload ------------------------------------------------------------ +/v1/projects/{project_id}/vpcs/vms/{vm_id}/reload +----------------------------------------------------------------------------------------------------------------- .. contents:: -POST /v1/**{project_id}**/vpcs/vms/**{vm_id}**/reload -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +POST /v1/projects/**{project_id}**/vpcs/vms/**{vm_id}**/reload +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Reload a VPCS instance Parameters diff --git a/docs/api/v1projectidvpcsvmsvmidstart.rst b/docs/api/v1projectsprojectidvpcsvmsvmidstart.rst similarity index 53% rename from docs/api/v1projectidvpcsvmsvmidstart.rst rename to docs/api/v1projectsprojectidvpcsvmsvmidstart.rst index ae8a095f..46897b88 100644 --- a/docs/api/v1projectidvpcsvmsvmidstart.rst +++ b/docs/api/v1projectsprojectidvpcsvmsvmidstart.rst @@ -1,10 +1,10 @@ -/v1/{project_id}/vpcs/vms/{vm_id}/start ------------------------------------------------------------ +/v1/projects/{project_id}/vpcs/vms/{vm_id}/start +----------------------------------------------------------------------------------------------------------------- .. contents:: -POST /v1/**{project_id}**/vpcs/vms/**{vm_id}**/start -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +POST /v1/projects/**{project_id}**/vpcs/vms/**{vm_id}**/start +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Start a VPCS instance Parameters diff --git a/docs/api/v1projectidvpcsvmsvmidstop.rst b/docs/api/v1projectsprojectidvpcsvmsvmidstop.rst similarity index 53% rename from docs/api/v1projectidvpcsvmsvmidstop.rst rename to docs/api/v1projectsprojectidvpcsvmsvmidstop.rst index 16f3135b..1bc21787 100644 --- a/docs/api/v1projectidvpcsvmsvmidstop.rst +++ b/docs/api/v1projectsprojectidvpcsvmsvmidstop.rst @@ -1,10 +1,10 @@ -/v1/{project_id}/vpcs/vms/{vm_id}/stop ------------------------------------------------------------ +/v1/projects/{project_id}/vpcs/vms/{vm_id}/stop +----------------------------------------------------------------------------------------------------------------- .. contents:: -POST /v1/**{project_id}**/vpcs/vms/**{vm_id}**/stop -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +POST /v1/projects/**{project_id}**/vpcs/vms/**{vm_id}**/stop +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Stop a VPCS instance Parameters diff --git a/docs/api/v1version.rst b/docs/api/v1version.rst index e0946a33..ae53f9c2 100644 --- a/docs/api/v1version.rst +++ b/docs/api/v1version.rst @@ -1,10 +1,10 @@ /v1/version ------------------------------------------------------------ +----------------------------------------------------------------------------------------------------------------- .. contents:: GET /v1/version -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Retrieve the server version number Response status codes @@ -22,7 +22,7 @@ Output POST /v1/version -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Check if version is the same as the server Response status codes diff --git a/docs/api/v1virtualboxvms.rst b/docs/api/v1virtualboxvms.rst index 18a82b95..a174df6c 100644 --- a/docs/api/v1virtualboxvms.rst +++ b/docs/api/v1virtualboxvms.rst @@ -1,10 +1,10 @@ /v1/virtualbox/vms ------------------------------------------------------------ +----------------------------------------------------------------------------------------------------------------- .. contents:: GET /v1/virtualbox/vms -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Get all VirtualBox VMs available Response status codes diff --git a/docs/conf.py b/docs/conf.py index 7fe6d119..4cef3395 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -103,6 +103,10 @@ pygments_style = 'sphinx' # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' + +html_sidebars = { + '**': ['sourcelink.html', 'searchbox.html'], +} # html_theme = 'nature' # If uncommented it's turn off the default read the doc style diff --git a/gns3server/web/documentation.py b/gns3server/web/documentation.py index 6b15b7e0..caf73aa4 100644 --- a/gns3server/web/documentation.py +++ b/gns3server/web/documentation.py @@ -34,11 +34,11 @@ class Documentation(object): 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('{}\n-----------------------------------------------------------------------------------------------------------------\n\n'.format(path)) f.write('.. contents::\n') for method in handler_doc["methods"]: f.write('\n{} {}\n'.format(method["method"], path.replace("{", '**{').replace("}", "}**"))) - f.write('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n') + f.write('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n') f.write('{}\n\n'.format(method["description"])) if len(method["parameters"]) > 0: diff --git a/tests/api/test_iou.py b/tests/api/test_iou.py index 3c8921b5..0c5bfdd1 100644 --- a/tests/api/test_iou.py +++ b/tests/api/test_iou.py @@ -143,9 +143,9 @@ def test_iou_update(server, vm, tmpdir, free_console_port): def test_iou_nio_create_udp(server, vm): response = server.post("/projects/{project_id}/iou/vms/{vm_id}/adapters/1/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"}, + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}, example=True) assert response.status == 201 assert response.route == "/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" @@ -154,8 +154,8 @@ def test_iou_nio_create_udp(server, vm): def test_iou_nio_create_ethernet(server, vm): response = server.post("/projects/{project_id}/iou/vms/{vm_id}/adapters/1/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_generic_ethernet", - "ethernet_device": "eth0", - }, + "ethernet_device": "eth0", + }, example=True) assert response.status == 201 assert response.route == "/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" @@ -166,7 +166,7 @@ def test_iou_nio_create_ethernet(server, vm): def test_iou_nio_create_tap(server, vm): with patch("gns3server.modules.base_manager.BaseManager._has_privileged_access", return_value=True): response = server.post("/projects/{project_id}/iou/vms/{vm_id}/adapters/1/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_tap", - "tap_device": "test"}) + "tap_device": "test"}) assert response.status == 201 assert response.route == "/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" assert response.json["type"] == "nio_tap" @@ -174,9 +174,9 @@ def test_iou_nio_create_tap(server, vm): def test_iou_delete_nio(server, vm): server.post("/projects/{project_id}/iou/vms/{vm_id}/adapters/1/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"}) + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}) response = server.delete("/projects/{project_id}/iou/vms/{vm_id}/adapters/1/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert response.status == 204 assert response.route == "/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" diff --git a/tests/api/test_virtualbox.py b/tests/api/test_virtualbox.py index 3c0e7ad1..dcd2e60f 100644 --- a/tests/api/test_virtualbox.py +++ b/tests/api/test_virtualbox.py @@ -94,10 +94,10 @@ def test_vbox_nio_create_udp(server, vm): with asyncio_patch('gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.adapter_add_nio_binding') as mock: response = server.post("/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/0/ports/0/nio".format(project_id=vm["project_id"], - vm_id=vm["vm_id"]), {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"}, + vm_id=vm["vm_id"]), {"type": "nio_udp", + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}, example=True) assert mock.called diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index 32bdb958..009da325 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -64,9 +64,9 @@ def test_vpcs_create_port(server, project, free_console_port): def test_vpcs_nio_create_udp(server, vm): response = server.post("/projects/{project_id}/vpcs/vms/{vm_id}/adapters/0/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"}, + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}, example=True) assert response.status == 201 assert response.route == "/projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" @@ -76,7 +76,7 @@ def test_vpcs_nio_create_udp(server, vm): def test_vpcs_nio_create_tap(server, vm): with patch("gns3server.modules.base_manager.BaseManager._has_privileged_access", return_value=True): response = server.post("/projects/{project_id}/vpcs/vms/{vm_id}/adapters/0/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_tap", - "tap_device": "test"}) + "tap_device": "test"}) assert response.status == 201 assert response.route == "/projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" assert response.json["type"] == "nio_tap" @@ -84,9 +84,9 @@ def test_vpcs_nio_create_tap(server, vm): def test_vpcs_delete_nio(server, vm): server.post("/projects/{project_id}/vpcs/vms/{vm_id}/adapters/0/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"}) + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}) response = server.delete("/projects/{project_id}/vpcs/vms/{vm_id}/adapters/0/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert response.status == 204 assert response.route == "/projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" From d86e880ff779b58b74b0ccc1c7a5ea6badb75ec6 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 13 Feb 2015 18:40:40 +0100 Subject: [PATCH 241/485] Definitions of port and adapter --- docs/api/examples/get_projectsprojectid.txt | 4 ++-- .../get_projectsprojectidiouvmsvmid.txt | 4 ++-- ...get_projectsprojectidvirtualboxvmsvmid.txt | 2 +- .../get_projectsprojectidvpcsvmsvmid.txt | 2 +- .../examples/post_projectsprojectidiouvms.txt | 10 ++++----- .../post_projectsprojectidvirtualboxvms.txt | 2 +- .../post_projectsprojectidvpcsvms.txt | 2 +- docs/api/examples/put_projectsprojectid.txt | 4 ++-- ...ptersadapternumberdportsportnumberdnio.rst | 4 ++-- ...svmidadaptersadapteriddportsportiddnio.rst | 4 ++-- ...xvmsvmidadaptersadapteriddstartcapture.rst | 2 +- ...oxvmsvmidadaptersadapteriddstopcapture.rst | 2 +- ...ptersadapternumberdportsportnumberdnio.rst | 4 ++-- docs/conf.py | 2 +- docs/general.rst | 21 +++++++++++++++++++ 15 files changed, 45 insertions(+), 24 deletions(-) diff --git a/docs/api/examples/get_projectsprojectid.txt b/docs/api/examples/get_projectsprojectid.txt index 1c1716fd..2cfcf9d8 100644 --- a/docs/api/examples/get_projectsprojectid.txt +++ b/docs/api/examples/get_projectsprojectid.txt @@ -13,8 +13,8 @@ SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /v1/projects/{project_id} { - "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmp95msfptc", - "path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmp95msfptc/00010203-0405-0607-0809-0a0b0c0d0e0f", + "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmp5cndh7nh", + "path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmp5cndh7nh/00010203-0405-0607-0809-0a0b0c0d0e0f", "project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f", "temporary": false } diff --git a/docs/api/examples/get_projectsprojectidiouvmsvmid.txt b/docs/api/examples/get_projectsprojectidiouvmsvmid.txt index 0e1faa6c..3cd4491d 100644 --- a/docs/api/examples/get_projectsprojectidiouvmsvmid.txt +++ b/docs/api/examples/get_projectsprojectidiouvmsvmid.txt @@ -18,9 +18,9 @@ X-ROUTE: /v1/projects/{project_id}/iou/vms/{vm_id} "l1_keepalives": false, "name": "PC TEST 1", "nvram": 128, - "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2249/test_iou_get0/iou.bin", + "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2262/test_iou_get0/iou.bin", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "ram": 256, "serial_adapters": 2, - "vm_id": "3ab57efc-4016-4898-ac4c-6e804ed8d429" + "vm_id": "548ed6e5-2ab6-421d-8f85-9dee638617fb" } diff --git a/docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt b/docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt index cbb4296b..fc9f1ff6 100644 --- a/docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt +++ b/docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt @@ -21,6 +21,6 @@ X-ROUTE: /v1/projects/{project_id}/virtualbox/vms/{vm_id} "name": "VMTEST", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "use_any_adapter": false, - "vm_id": "3e6ed300-eefc-4582-80f0-acbdba7e2b9b", + "vm_id": "f70dc1dd-563e-46cc-9cf7-bb169730b8e2", "vmname": "VMTEST" } diff --git a/docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt b/docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt index 36365cd4..cde6eea4 100644 --- a/docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt +++ b/docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt @@ -17,5 +17,5 @@ X-ROUTE: /v1/projects/{project_id}/vpcs/vms/{vm_id} "name": "PC TEST 1", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "startup_script": null, - "vm_id": "b4ff501b-c24d-4f0c-854b-8163e31b9a8e" + "vm_id": "ed3e4572-df65-4581-9332-324e749d480a" } diff --git a/docs/api/examples/post_projectsprojectidiouvms.txt b/docs/api/examples/post_projectsprojectidiouvms.txt index 72501ea2..0e129869 100644 --- a/docs/api/examples/post_projectsprojectidiouvms.txt +++ b/docs/api/examples/post_projectsprojectidiouvms.txt @@ -1,13 +1,13 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/iou/vms' -d '{"ethernet_adapters": 0, "iourc_path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2249/test_iou_create_with_params0/iourc", "l1_keepalives": true, "name": "PC TEST 1", "nvram": 512, "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2249/test_iou_create_with_params0/iou.bin", "ram": 1024, "serial_adapters": 4}' +curl -i -X POST 'http://localhost:8000/projects/{project_id}/iou/vms' -d '{"ethernet_adapters": 0, "iourc_path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2262/test_iou_create_with_params0/iourc", "l1_keepalives": true, "name": "PC TEST 1", "nvram": 512, "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2262/test_iou_create_with_params0/iou.bin", "ram": 1024, "serial_adapters": 4}' POST /projects/{project_id}/iou/vms HTTP/1.1 { "ethernet_adapters": 0, - "iourc_path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2249/test_iou_create_with_params0/iourc", + "iourc_path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2262/test_iou_create_with_params0/iourc", "l1_keepalives": true, "name": "PC TEST 1", "nvram": 512, - "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2249/test_iou_create_with_params0/iou.bin", + "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2262/test_iou_create_with_params0/iou.bin", "ram": 1024, "serial_adapters": 4 } @@ -27,9 +27,9 @@ X-ROUTE: /v1/projects/{project_id}/iou/vms "l1_keepalives": true, "name": "PC TEST 1", "nvram": 512, - "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2249/test_iou_create_with_params0/iou.bin", + "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2262/test_iou_create_with_params0/iou.bin", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "ram": 1024, "serial_adapters": 4, - "vm_id": "b6a61d51-7225-4f4a-904a-54c8a8917b87" + "vm_id": "12acb1c1-304e-4c3e-81c4-23c52e6c01ba" } diff --git a/docs/api/examples/post_projectsprojectidvirtualboxvms.txt b/docs/api/examples/post_projectsprojectidvirtualboxvms.txt index 9509fef5..2b3c4fb3 100644 --- a/docs/api/examples/post_projectsprojectidvirtualboxvms.txt +++ b/docs/api/examples/post_projectsprojectidvirtualboxvms.txt @@ -25,6 +25,6 @@ X-ROUTE: /v1/projects/{project_id}/virtualbox/vms "name": "VM1", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "use_any_adapter": false, - "vm_id": "7abbfc73-3d92-4e31-a99c-3b9519b2e0e8", + "vm_id": "ad5dba5d-0219-41a0-9bd3-eaca69649f6c", "vmname": "VM1" } diff --git a/docs/api/examples/post_projectsprojectidvpcsvms.txt b/docs/api/examples/post_projectsprojectidvpcsvms.txt index d3309113..60df8000 100644 --- a/docs/api/examples/post_projectsprojectidvpcsvms.txt +++ b/docs/api/examples/post_projectsprojectidvpcsvms.txt @@ -19,5 +19,5 @@ X-ROUTE: /v1/projects/{project_id}/vpcs/vms "name": "PC TEST 1", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "startup_script": null, - "vm_id": "b481d455-16d3-450f-bcec-afdf1ec0c682" + "vm_id": "6c343265-dc55-4bfe-8d4e-f3f21eeeca8d" } diff --git a/docs/api/examples/put_projectsprojectid.txt b/docs/api/examples/put_projectsprojectid.txt index 0e7f4503..878274d0 100644 --- a/docs/api/examples/put_projectsprojectid.txt +++ b/docs/api/examples/put_projectsprojectid.txt @@ -1,8 +1,8 @@ -curl -i -X PUT 'http://localhost:8000/projects/{project_id}' -d '{"path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2249/test_update_path_project_non_l0"}' +curl -i -X PUT 'http://localhost:8000/projects/{project_id}' -d '{"path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2262/test_update_path_project_non_l0"}' PUT /projects/{project_id} HTTP/1.1 { - "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2249/test_update_path_project_non_l0" + "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2262/test_update_path_project_non_l0" } diff --git a/docs/api/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst index 68065488..cd0e5f27 100644 --- a/docs/api/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -9,8 +9,8 @@ Add a NIO to a IOU instance Parameters ********** -- **port_number**: Port where the nio should be added - **adapter_number**: Network adapter where the nio is located +- **port_number**: Port where the nio should be added - **vm_id**: UUID for the instance - **project_id**: UUID for the project @@ -27,8 +27,8 @@ Remove a NIO from a IOU instance Parameters ********** -- **port_number**: Port from where the nio should be removed - **adapter_number**: Network adapter where the nio is located +- **port_number**: Port from where the nio should be removed - **vm_id**: UUID for the instance - **project_id**: UUID for the project diff --git a/docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddportsportiddnio.rst b/docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddportsportiddnio.rst index 5104eb2d..7a9228c5 100644 --- a/docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddportsportiddnio.rst +++ b/docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddportsportiddnio.rst @@ -11,8 +11,8 @@ Parameters ********** - **port_id**: Port in the adapter (always 0 for virtualbox) - **vm_id**: UUID for the instance -- **adapter_id**: Adapter where the nio should be added - **project_id**: UUID for the project +- **adapter_id**: Adapter where the nio should be added Response status codes ********************** @@ -29,8 +29,8 @@ Parameters ********** - **port_id**: Port in the adapter (always 0 for virtualbox) - **vm_id**: UUID for the instance -- **adapter_id**: Adapter from where the nio should be removed - **project_id**: UUID for the project +- **adapter_id**: Adapter from where the nio should be removed Response status codes ********************** diff --git a/docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddstartcapture.rst b/docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddstartcapture.rst index ceab89f1..6a3320de 100644 --- a/docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddstartcapture.rst +++ b/docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddstartcapture.rst @@ -10,8 +10,8 @@ Start a packet capture on a VirtualBox VM instance Parameters ********** - **vm_id**: UUID for the instance -- **adapter_id**: Adapter to start a packet capture - **project_id**: UUID for the project +- **adapter_id**: Adapter to start a packet capture Response status codes ********************** diff --git a/docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddstopcapture.rst b/docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddstopcapture.rst index 4c2a6072..9ceeef26 100644 --- a/docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddstopcapture.rst +++ b/docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddstopcapture.rst @@ -10,8 +10,8 @@ Stop a packet capture on a VirtualBox VM instance Parameters ********** - **vm_id**: UUID for the instance -- **adapter_id**: Adapter to stop a packet capture - **project_id**: UUID for the project +- **adapter_id**: Adapter to stop a packet capture Response status codes ********************** diff --git a/docs/api/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst index 4e7cc653..1c756021 100644 --- a/docs/api/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -9,8 +9,8 @@ Add a NIO to a VPCS instance Parameters ********** -- **port_number**: Port where the nio should be added - **adapter_number**: Network adapter where the nio is located +- **port_number**: Port where the nio should be added - **vm_id**: UUID for the instance - **project_id**: UUID for the project @@ -27,8 +27,8 @@ Remove a NIO from a VPCS instance Parameters ********** -- **port_number**: Port from where the nio should be removed - **adapter_number**: Network adapter where the nio is located +- **port_number**: Port from where the nio should be removed - **vm_id**: UUID for the instance - **project_id**: UUID for the project diff --git a/docs/conf.py b/docs/conf.py index 4cef3395..1c05ec66 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -105,7 +105,7 @@ pygments_style = 'sphinx' html_theme = 'default' html_sidebars = { - '**': ['sourcelink.html', 'searchbox.html'], + '**': ['sourcelink.html', 'searchbox.html'], } # html_theme = 'nature' diff --git a/docs/general.rst b/docs/general.rst index efc3cb3c..5e01f896 100644 --- a/docs/general.rst +++ b/docs/general.rst @@ -10,3 +10,24 @@ JSON like that "status": 409, "message": "Conflict" } + +Glossary +======== + +VM +--- + +A Virtual Machine (Dynamips, IOU, Qemu, VPCS...) + +Adapter +------- + +The physical network interface. The adapter can contain multiple ports. + +Port +---- + +A port is an opening on network adapter that cable plug into. + +For example a VM can have a serial and an ethernet adapter plugged in. +The ethernet adapter can have 4 ports. From a9a3bb1c38c3bb32b93e7872642f68e3d3bf755d Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 13 Feb 2015 20:57:09 +0100 Subject: [PATCH 242/485] Pep8 --- gns3server/modules/adapters/ethernet_adapter.py | 4 ++-- gns3server/modules/adapters/serial_adapter.py | 4 ++-- tests/api/test_iou.py | 11 +++++++++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/gns3server/modules/adapters/ethernet_adapter.py b/gns3server/modules/adapters/ethernet_adapter.py index f1a06c63..6a4981a0 100644 --- a/gns3server/modules/adapters/ethernet_adapter.py +++ b/gns3server/modules/adapters/ethernet_adapter.py @@ -24,8 +24,8 @@ class EthernetAdapter(Adapter): VPCS Ethernet adapter. """ - def __init__(self): - Adapter.__init__(self, interfaces=1) + def __init__(self, interfaces=1): + Adapter.__init__(self, interfaces=interfaces) def __str__(self): diff --git a/gns3server/modules/adapters/serial_adapter.py b/gns3server/modules/adapters/serial_adapter.py index 5bb00dc1..8986f095 100644 --- a/gns3server/modules/adapters/serial_adapter.py +++ b/gns3server/modules/adapters/serial_adapter.py @@ -24,8 +24,8 @@ class SerialAdapter(Adapter): VPCS Ethernet adapter. """ - def __init__(self): - Adapter.__init__(self, interfaces=1) + def __init__(self, interfaces=1): + Adapter.__init__(self, interfaces=interfaces) def __str__(self): diff --git a/tests/api/test_iou.py b/tests/api/test_iou.py index 0c5bfdd1..f22090da 100644 --- a/tests/api/test_iou.py +++ b/tests/api/test_iou.py @@ -163,6 +163,17 @@ def test_iou_nio_create_ethernet(server, vm): assert response.json["ethernet_device"] == "eth0" +def test_iou_nio_create_ethernet_different_port(server, vm): + response = server.post("/projects/{project_id}/iou/vms/{vm_id}/adapters/0/ports/3/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_generic_ethernet", + "ethernet_device": "eth0", + }, + example=False) + assert response.status == 201 + assert response.route == "/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" + assert response.json["type"] == "nio_generic_ethernet" + assert response.json["ethernet_device"] == "eth0" + + def test_iou_nio_create_tap(server, vm): with patch("gns3server.modules.base_manager.BaseManager._has_privileged_access", return_value=True): response = server.post("/projects/{project_id}/iou/vms/{vm_id}/adapters/1/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_tap", From 83edc649d2be5c6c2374262a41ffbc0a582a6231 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 13 Feb 2015 20:57:20 +0100 Subject: [PATCH 243/485] Rename NVRAM to the correct application id before start the server --- gns3server/modules/iou/iou_vm.py | 16 ++++++++++++++-- tests/modules/iou/test_iou_vm.py | 12 ++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index d0fb4f60..6c6bc02f 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -30,6 +30,7 @@ import shutil import argparse import threading import configparser +import glob from pkg_resources import parse_version from .iou_error import IOUError @@ -333,6 +334,8 @@ class IOUVM(BaseVM): self._check_requirements() if not self.is_running(): + self._rename_nvram_file() + # TODO: ASYNC # self._library_check() @@ -373,6 +376,15 @@ class IOUVM(BaseVM): # connections support self._start_iouyap() + def _rename_nvram_file(self): + """ + Before start the VM rename the nvram file to the correct application id + """ + + destination = os.path.join(self.working_dir, "nvram_{:05d}".format(self.application_id)) + for file_path in glob.glob(os.path.join(self.working_dir, "nvram_*")): + shutil.move(file_path, destination) + def _start_iouyap(self): """ Starts iouyap (handles connections to and from this IOU device). @@ -670,7 +682,7 @@ class IOUVM(BaseVM): self._ethernet_adapters.clear() for _ in range(0, ethernet_adapters): - self._ethernet_adapters.append(EthernetAdapter()) + self._ethernet_adapters.append(EthernetAdapter(interfaces=4)) log.info("IOU {name} [id={id}]: number of Ethernet adapters changed to {adapters}".format(name=self._name, id=self._id, @@ -696,7 +708,7 @@ class IOUVM(BaseVM): self._serial_adapters.clear() for _ in range(0, serial_adapters): - self._serial_adapters.append(SerialAdapter()) + self._serial_adapters.append(SerialAdapter(interfaces=4)) log.info("IOU {name} [id={id}]: number of Serial adapters changed to {adapters}".format(name=self._name, id=self._id, diff --git a/tests/modules/iou/test_iou_vm.py b/tests/modules/iou/test_iou_vm.py index 8f8a0181..55ad02a3 100644 --- a/tests/modules/iou/test_iou_vm.py +++ b/tests/modules/iou/test_iou_vm.py @@ -85,6 +85,18 @@ def test_start(loop, vm, monkeypatch): assert vm.is_running() +def test_rename_nvram_file(loop, vm, monkeypatch): + """ + It should rename the nvram file to the correct name before launching the VM + """ + + with open(os.path.join(vm.working_dir, "nvram_0000{}".format(vm.application_id + 1)), 'w+') as f: + f.write("1") + + vm._rename_nvram_file() + assert os.path.exists(os.path.join(vm.working_dir, "nvram_0000{}".format(vm.application_id))) + + def test_stop(loop, vm): process = MagicMock() From e082cd8b1a438793b697c6fd41bd597d0459ce05 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 13 Feb 2015 22:16:43 +0100 Subject: [PATCH 244/485] Update the remote IOU initial config --- gns3server/handlers/iou_handler.py | 4 +- gns3server/modules/iou/iou_vm.py | 79 ++++++++++++++++++++++++++++-- gns3server/schemas/iou.py | 10 +++- tests/api/test_iou.py | 14 ++++-- tests/modules/iou/test_iou_vm.py | 52 +++++++++++++++++++- 5 files changed, 148 insertions(+), 11 deletions(-) diff --git a/gns3server/handlers/iou_handler.py b/gns3server/handlers/iou_handler.py index 88e8aceb..04177f73 100644 --- a/gns3server/handlers/iou_handler.py +++ b/gns3server/handlers/iou_handler.py @@ -56,7 +56,8 @@ class IOUHandler: ethernet_adapters=request.json.get("ethernet_adapters"), ram=request.json.get("ram"), nvram=request.json.get("nvram"), - l1_keepalives=request.json.get("l1_keepalives") + l1_keepalives=request.json.get("l1_keepalives"), + initial_config=request.json.get("initial_config") ) vm.path = request.json.get("path", vm.path) vm.iourc_path = request.json.get("iourc_path", vm.iourc_path) @@ -112,6 +113,7 @@ class IOUHandler: vm.ram = request.json.get("ram", vm.ram) vm.nvram = request.json.get("nvram", vm.nvram) vm.l1_keepalives = request.json.get("l1_keepalives", vm.l1_keepalives) + vm.initial_config = request.json.get("initial_config", vm.initial_config) response.json(vm) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 6c6bc02f..5f953638 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -62,7 +62,8 @@ class IOUVM(BaseVM): :params serial_adapters: Number of serial adapters :params ram: Ram MB :params nvram: Nvram KB - :params l1_keepalives: Always up ethernet interface + :params l1_keepalives: Always up ethernet interface: + :params initial_config: Content of the initial configuration file """ def __init__(self, name, vm_id, project, manager, @@ -72,7 +73,8 @@ class IOUVM(BaseVM): nvram=None, ethernet_adapters=None, serial_adapters=None, - l1_keepalives=None): + l1_keepalives=None, + initial_config=None): super().__init__(name, vm_id, project, manager) @@ -98,6 +100,9 @@ class IOUVM(BaseVM): self._ram = 256 if ram is None else ram # Megabytes self._l1_keepalives = False if l1_keepalives is None else l1_keepalives # used to overcome the always-up Ethernet interfaces (not supported by all IOSes). + if initial_config is not None: + self.initial_config = initial_config + if self._console is not None: self._console = self._manager.port_manager.reserve_console_port(self._console) else: @@ -212,7 +217,7 @@ class IOUVM(BaseVM): "serial_adapters": len(self._serial_adapters), "ram": self._ram, "nvram": self._nvram, - "l1_keepalives": self._l1_keepalives + "l1_keepalives": self._l1_keepalives, } @property @@ -303,6 +308,21 @@ class IOUVM(BaseVM): new_nvram=nvram)) self._nvram = nvram + @BaseVM.name.setter + def name(self, new_name): + """ + Sets the name of this IOU vm. + + :param new_name: name + """ + + if self.initial_config_file: + content = self.initial_config + content = content.replace(self._name, new_name) + self.initial_config = content + + super(IOUVM, IOUVM).name.__set__(self, new_name) + @property def application_id(self): return self._manager.get_application_id(self.id) @@ -614,8 +634,10 @@ class IOUVM(BaseVM): command.extend(["-n", str(self._nvram)]) command.extend(["-m", str(self._ram)]) command.extend(["-L"]) # disable local console, use remote console - if self._initial_config: - command.extend(["-c", self._initial_config]) + + initial_config_file = self.initial_config_file + if initial_config_file: + command.extend(["-c", initial_config_file]) if self._l1_keepalives: self._enable_l1_keepalives(command) command.extend([str(self.application_id)]) @@ -813,3 +835,50 @@ class IOUVM(BaseVM): raise IOUError("layer 1 keepalive messages are not supported by {}".format(os.path.basename(self._path))) except (OSError, subprocess.SubprocessError) as e: log.warn("could not determine if layer 1 keepalive messages are supported by {}: {}".format(os.path.basename(self._path), e)) + + @property + def initial_config(self): + """Return the content of the current initial-config file""" + + config_file = self.initial_config_file + if config_file is None: + return None + + try: + with open(config_file) as f: + return f.read() + except OSError as e: + raise VPCSError("Can't read configuration file '{}'".format(config_file)) + + @initial_config.setter + def initial_config(self, initial_config): + """ + Update the initial config + + :param initial_config: The content of the initial configuration file + """ + + try: + script_file = os.path.join(self.working_dir, "initial-config.cfg") + with open(script_file, 'w+') as f: + if initial_config is None: + f.write('') + else: + initial_config = initial_config.replace("%h", self._name) + f.write(initial_config) + except OSError as e: + raise VPCSError("Can't write initial configuration file '{}'".format(self.script_file)) + + @property + def initial_config_file(self): + """ + Returns the initial config file for this IOU instance. + + :returns: path to config file. None if the file doesn't exist + """ + + path = os.path.join(self.working_dir, 'initial-config.cfg') + if os.path.exists(path): + return path + else: + return None diff --git a/gns3server/schemas/iou.py b/gns3server/schemas/iou.py index 0f578cf0..857208c3 100644 --- a/gns3server/schemas/iou.py +++ b/gns3server/schemas/iou.py @@ -69,6 +69,10 @@ IOU_CREATE_SCHEMA = { "l1_keepalives": { "description": "Always up ethernet interface", "type": ["boolean", "null"] + }, + "initial_config": { + "description": "Initial configuration of the IOU", + "type": ["string", "null"] } }, "additionalProperties": False, @@ -122,6 +126,10 @@ IOU_UPDATE_SCHEMA = { "l1_keepalives": { "description": "Always up ethernet interface", "type": ["boolean", "null"] + }, + "initial_config": { + "description": "Initial configuration of the IOU", + "type": ["string", "null"] } }, "additionalProperties": False, @@ -180,7 +188,7 @@ IOU_OBJECT_SCHEMA = { "l1_keepalives": { "description": "Always up ethernet interface", "type": "boolean" - } + }, }, "additionalProperties": False, "required": ["name", "vm_id", "console", "project_id", "path", "serial_adapters", "ethernet_adapters", "ram", "nvram", "l1_keepalives"] diff --git a/tests/api/test_iou.py b/tests/api/test_iou.py index f22090da..eaffed13 100644 --- a/tests/api/test_iou.py +++ b/tests/api/test_iou.py @@ -46,6 +46,9 @@ def vm(server, project, base_params): return response.json +def initial_config_file(project, vm): + return os.path.join(project.path, "project-files", "iou", vm["vm_id"], "initial-config.cfg") + def test_iou_create(server, project, base_params): response = server.post("/projects/{project_id}/iou/vms".format(project_id=project.id), base_params) assert response.status == 201 @@ -66,6 +69,7 @@ def test_iou_create_with_params(server, project, base_params): params["serial_adapters"] = 4 params["ethernet_adapters"] = 0 params["l1_keepalives"] = True + params["initial_config"] = "hostname test" response = server.post("/projects/{project_id}/iou/vms".format(project_id=project.id), params, example=True) assert response.status == 201 @@ -77,6 +81,8 @@ def test_iou_create_with_params(server, project, base_params): assert response.json["ram"] == 1024 assert response.json["nvram"] == 512 assert response.json["l1_keepalives"] == True + with open(initial_config_file(project, response.json)) as f: + assert f.read() == params["initial_config"] def test_iou_get(server, project, vm): @@ -120,7 +126,7 @@ def test_iou_delete(server, vm): assert response.status == 204 -def test_iou_update(server, vm, tmpdir, free_console_port): +def test_iou_update(server, vm, tmpdir, free_console_port, project): params = { "name": "test", "console": free_console_port, @@ -128,7 +134,8 @@ def test_iou_update(server, vm, tmpdir, free_console_port): "nvram": 2048, "ethernet_adapters": 4, "serial_adapters": 0, - "l1_keepalives": True + "l1_keepalives": True, + "initial_config": "hostname test" } response = server.put("/projects/{project_id}/iou/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), params) assert response.status == 200 @@ -139,7 +146,8 @@ def test_iou_update(server, vm, tmpdir, free_console_port): assert response.json["ram"] == 512 assert response.json["nvram"] == 2048 assert response.json["l1_keepalives"] == True - + with open(initial_config_file(project, response.json)) as f: + assert f.read() == "hostname test" def test_iou_nio_create_udp(server, vm): response = server.post("/projects/{project_id}/iou/vms/{vm_id}/adapters/1/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", diff --git a/tests/modules/iou/test_iou_vm.py b/tests/modules/iou/test_iou_vm.py index 55ad02a3..55d80a00 100644 --- a/tests/modules/iou/test_iou_vm.py +++ b/tests/modules/iou/test_iou_vm.py @@ -69,6 +69,12 @@ def test_vm(project, manager): assert vm.id == "00010203-0405-0607-0809-0a0b0c0d0e0f" +def test_vm_initial_config(project, manager): + vm = IOUVM("test", "00010203-0405-0607-0808-0a0b0c0d0e0f", project, manager, initial_config="hostname %h") + assert vm.name == "test" + assert vm.initial_config == "hostname test" + assert vm.id == "00010203-0405-0607-0808-0a0b0c0d0e0f" + @patch("gns3server.config.Config.get_section_config", return_value={"iouyap_path": "/bin/test_fake"}) def test_vm_invalid_iouyap_path(project, manager, loop): with pytest.raises(IOUError): @@ -179,4 +185,48 @@ def test_create_netmap_config(vm): def test_build_command(vm): - assert vm._build_command() == [vm.path, '-L', str(vm.application_id)] + assert vm._build_command() == [vm.path, "-L", str(vm.application_id)] + + +def test_build_command_initial_config(vm): + + filepath = os.path.join(vm.working_dir, "initial-config.cfg") + with open(filepath, "w+") as f: + f.write("service timestamps debug datetime msec\nservice timestamps log datetime msec\nno service password-encryption") + + assert vm._build_command() == [vm.path, "-L", "-c", vm.initial_config_file, str(vm.application_id)] + + +def test_get_initial_config(vm): + + content = "service timestamps debug datetime msec\nservice timestamps log datetime msec\nno service password-encryption" + vm.initial_config = content + assert vm.initial_config == content + + +def test_update_initial_config(vm): + content = "service timestamps debug datetime msec\nservice timestamps log datetime msec\nno service password-encryption" + vm.initial_config = content + filepath = os.path.join(vm.working_dir, "initial-config.cfg") + assert os.path.exists(filepath) + with open(filepath) as f: + assert f.read() == content + + +def test_update_initial_config_h(vm): + content = "hostname %h\n" + vm.name = "pc1" + vm.initial_config = content + with open(vm.initial_config_file) as f: + assert f.read() == "hostname pc1\n" + + +def test_change_name(vm, tmpdir): + path = os.path.join(vm.working_dir, "initial-config.cfg") + vm.name = "world" + with open(path, 'w+') as f: + f.write("hostname world") + vm.name = "hello" + assert vm.name == "hello" + with open(path) as f: + assert f.read() == "hostname hello" From a5ac7c54815b0d8782213d0d06515e3f0fab2544 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 13 Feb 2015 15:11:14 -0700 Subject: [PATCH 245/485] Dynamips NIO connections. --- gns3server/handlers/dynamips_handler.py | 52 +++ gns3server/handlers/virtualbox_handler.py | 32 +- .../modules/adapters/ethernet_adapter.py | 2 +- gns3server/modules/adapters/serial_adapter.py | 2 +- gns3server/modules/dynamips/__init__.py | 44 +-- gns3server/modules/dynamips/nios/nio.py | 2 +- gns3server/modules/dynamips/nios/nio_fifo.py | 13 +- .../dynamips/nios/nio_generic_ethernet.py | 14 +- .../dynamips/nios/nio_linux_ethernet.py | 14 +- gns3server/modules/dynamips/nios/nio_mcast.py | 15 +- gns3server/modules/dynamips/nios/nio_null.py | 13 +- gns3server/modules/dynamips/nios/nio_tap.py | 14 +- gns3server/modules/dynamips/nios/nio_udp.py | 16 +- .../modules/dynamips/nios/nio_udp_auto.py | 17 +- gns3server/modules/dynamips/nios/nio_unix.py | 15 +- gns3server/modules/dynamips/nios/nio_vde.py | 35 +- gns3server/modules/dynamips/nodes/router.py | 338 +++++++++--------- gns3server/schemas/dynamips.py | 159 ++++++++ 18 files changed, 521 insertions(+), 276 deletions(-) diff --git a/gns3server/handlers/dynamips_handler.py b/gns3server/handlers/dynamips_handler.py index e3ed5d95..993bbd44 100644 --- a/gns3server/handlers/dynamips_handler.py +++ b/gns3server/handlers/dynamips_handler.py @@ -20,6 +20,7 @@ import asyncio from ..web.route import Route from ..schemas.dynamips import VM_CREATE_SCHEMA from ..schemas.dynamips import VM_UPDATE_SCHEMA +from ..schemas.dynamips import VM_NIO_SCHEMA from ..schemas.dynamips import VM_OBJECT_SCHEMA from ..modules.dynamips import Dynamips from ..modules.project_manager import ProjectManager @@ -238,3 +239,54 @@ class DynamipsHandler: vm = dynamips_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) yield from vm.reload() response.set_status(204) + + @Route.post( + r"/projects/{project_id}/dynamips/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance", + "adapter_number": "Adapter where the nio should be added", + "port_number": "Port on the adapter" + }, + status_codes={ + 201: "NIO created", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Add a NIO to a Dynamips VM instance", + input=VM_NIO_SCHEMA, + output=VM_NIO_SCHEMA) + def create_nio(request, response): + + dynamips_manager = Dynamips.instance() + vm = dynamips_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + nio = yield from dynamips_manager.create_nio(vm, request.json) + slot_number = int(request.match_info["adapter_number"]) + port_number = int(request.match_info["port_number"]) + yield from vm.slot_add_nio_binding(slot_number, port_number, nio) + response.set_status(201) + response.json(nio) + + @classmethod + @Route.delete( + r"/projects/{project_id}/dynamips/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance", + "adapter_number": "Adapter from where the nio should be removed", + "port_number": "Port on the adapter" + }, + status_codes={ + 204: "NIO deleted", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Remove a NIO from a Dynamips VM instance") + def delete_nio(request, response): + + dynamips_manager = Dynamips.instance() + vm = dynamips_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + slot_number = int(request.match_info["adapter_number"]) + port_number = int(request.match_info["port_number"]) + yield from vm.slot_remove_nio_binding(slot_number, port_number) + response.set_status(204) diff --git a/gns3server/handlers/virtualbox_handler.py b/gns3server/handlers/virtualbox_handler.py index 16361c1d..ae8d296e 100644 --- a/gns3server/handlers/virtualbox_handler.py +++ b/gns3server/handlers/virtualbox_handler.py @@ -252,12 +252,12 @@ class VirtualBoxHandler: response.set_status(204) @Route.post( - r"/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/ports/{port_id:\d+}/nio", + r"/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio", parameters={ "project_id": "UUID for the project", "vm_id": "UUID for the instance", - "adapter_id": "Adapter where the nio should be added", - "port_id": "Port in the adapter (always 0 for virtualbox)" + "adapter_number": "Adapter where the nio should be added", + "port_number": "Port on the adapter (always 0)" }, status_codes={ 201: "NIO created", @@ -272,18 +272,18 @@ class VirtualBoxHandler: vbox_manager = VirtualBox.instance() vm = vbox_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) nio = vbox_manager.create_nio(vbox_manager.vboxmanage_path, request.json) - yield from vm.adapter_add_nio_binding(int(request.match_info["adapter_id"]), nio) + yield from vm.adapter_add_nio_binding(int(request.match_info["adapter_number"]), nio) response.set_status(201) response.json(nio) @classmethod @Route.delete( - r"/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/ports/{port_id:\d+}/nio", + r"/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio", parameters={ "project_id": "UUID for the project", "vm_id": "UUID for the instance", - "adapter_id": "Adapter from where the nio should be removed", - "port_id": "Port in the adapter (always 0 for virtualbox)" + "adapter_number": "Adapter from where the nio should be removed", + "port_number": "Port on the adapter (always)" }, status_codes={ 204: "NIO deleted", @@ -295,15 +295,16 @@ class VirtualBoxHandler: vbox_manager = VirtualBox.instance() vm = vbox_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) - yield from vm.adapter_remove_nio_binding(int(request.match_info["adapter_id"])) + yield from vm.adapter_remove_nio_binding(int(request.match_info["adapter_number"])) response.set_status(204) @Route.post( - r"/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/start_capture", + r"/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/start_capture", parameters={ "project_id": "UUID for the project", "vm_id": "UUID for the instance", - "adapter_id": "Adapter to start a packet capture" + "adapter_number": "Adapter to start a packet capture", + "port_number": "Port on the adapter (always 0)" }, status_codes={ 200: "Capture started", @@ -316,17 +317,18 @@ class VirtualBoxHandler: vbox_manager = VirtualBox.instance() vm = vbox_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) - adapter_id = int(request.match_info["adapter_id"]) + adapter_number = int(request.match_info["adapter_number"]) pcap_file_path = os.path.join(vm.project.capture_working_directory(), request.json["capture_file_name"]) - vm.start_capture(adapter_id, pcap_file_path) + vm.start_capture(adapter_number, pcap_file_path) response.json({"pcap_file_path": pcap_file_path}) @Route.post( - r"/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/stop_capture", + r"/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/stop_capture", parameters={ "project_id": "UUID for the project", "vm_id": "UUID for the instance", - "adapter_id": "Adapter to stop a packet capture" + "adapter_number": "Adapter to stop a packet capture", + "port_number": "Port on the adapter (always 0)" }, status_codes={ 204: "Capture stopped", @@ -338,5 +340,5 @@ class VirtualBoxHandler: vbox_manager = VirtualBox.instance() vm = vbox_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) - vm.stop_capture(int(request.match_info["adapter_id"])) + vm.stop_capture(int(request.match_info["adapter_number"])) response.set_status(204) diff --git a/gns3server/modules/adapters/ethernet_adapter.py b/gns3server/modules/adapters/ethernet_adapter.py index 6a4981a0..d5b5373f 100644 --- a/gns3server/modules/adapters/ethernet_adapter.py +++ b/gns3server/modules/adapters/ethernet_adapter.py @@ -21,7 +21,7 @@ from .adapter import Adapter class EthernetAdapter(Adapter): """ - VPCS Ethernet adapter. + Ethernet adapter. """ def __init__(self, interfaces=1): diff --git a/gns3server/modules/adapters/serial_adapter.py b/gns3server/modules/adapters/serial_adapter.py index 8986f095..1ac39ce1 100644 --- a/gns3server/modules/adapters/serial_adapter.py +++ b/gns3server/modules/adapters/serial_adapter.py @@ -21,7 +21,7 @@ from .adapter import Adapter class SerialAdapter(Adapter): """ - VPCS Ethernet adapter. + Ethernet adapter. """ def __init__(self, interfaces=1): diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index 47dd96a5..07af76e4 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -170,35 +170,7 @@ class Dynamips(BaseManager): return hypervisor - def create_nio(self, executable, nio_settings): - """ - Creates a new NIO. - - :param nio_settings: information to create the NIO - - :returns: a NIO object - """ - - nio = None - if nio_settings["type"] == "nio_udp": - lport = nio_settings["lport"] - rhost = nio_settings["rhost"] - rport = nio_settings["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 aiohttp.web.HTTPInternalServerError(text="Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e)) - nio = NIOUDP(lport, rhost, rport) - elif nio_settings["type"] == "nio_tap": - tap_device = nio_settings["tap_device"] - if not self._has_privileged_access(executable): - raise aiohttp.web.HTTPForbidden(text="{} has no privileged access to {}.".format(executable, tap_device)) - nio = NIOTAP(tap_device) - assert nio is not None - return nio - + @asyncio.coroutine def create_nio(self, node, nio_settings): """ Creates a new NIO. @@ -221,12 +193,12 @@ class Dynamips(BaseManager): except OSError as e: raise DynamipsError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e)) # check if we have an allocated NIO UDP auto - nio = node.hypervisor.get_nio_udp_auto(lport) - if not nio: - # otherwise create an NIO UDP - nio = NIOUDP(node.hypervisor, lport, rhost, rport) - else: - nio.connect(rhost, rport) + #nio = node.hypervisor.get_nio_udp_auto(lport) + #if not nio: + # otherwise create an NIO UDP + nio = NIOUDP(node.hypervisor, lport, rhost, rport) + #else: + # nio.connect(rhost, rport) elif nio_settings["type"] == "nio_generic_ethernet": ethernet_device = nio_settings["ethernet_device"] if sys.platform.startswith("win"): @@ -259,6 +231,8 @@ class Dynamips(BaseManager): nio = NIOVDE(node.hypervisor, control_file, local_file) elif nio_settings["type"] == "nio_null": nio = NIONull(node.hypervisor) + + yield from nio.create() return nio # def set_ghost_ios(self, router): diff --git a/gns3server/modules/dynamips/nios/nio.py b/gns3server/modules/dynamips/nios/nio.py index f829e4ed..2f978f19 100644 --- a/gns3server/modules/dynamips/nios/nio.py +++ b/gns3server/modules/dynamips/nios/nio.py @@ -38,7 +38,7 @@ class NIO: def __init__(self, name, hypervisor): self._hypervisor = hypervisor - self._name = None + self._name = name self._bandwidth = None # no bandwidth constraint by default self._input_filter = None # no input filter applied by default self._output_filter = None # no output filter applied by default diff --git a/gns3server/modules/dynamips/nios/nio_fifo.py b/gns3server/modules/dynamips/nios/nio_fifo.py index 55b91b8d..fd10e40f 100644 --- a/gns3server/modules/dynamips/nios/nio_fifo.py +++ b/gns3server/modules/dynamips/nios/nio_fifo.py @@ -38,12 +38,11 @@ class NIOFIFO(NIO): def __init__(self, hypervisor): - NIO.__init__(self, hypervisor) - - # create an unique ID - self._id = NIOFIFO._instance_count + # create an unique ID and name + nio_id = NIOFIFO._instance_count NIOFIFO._instance_count += 1 - self._name = 'nio_fifo' + str(self._id) + name = 'nio_fifo' + str(nio_id) + NIO.__init__(name, self, hypervisor) @classmethod def reset(cls): @@ -71,3 +70,7 @@ class NIOFIFO(NIO): nio=nio)) log.info("NIO FIFO {name} crossconnected with {nio_name}.".format(name=self._name, nio_name=nio.name)) + + def __json__(self): + + return {"type": "nio_fifo"} diff --git a/gns3server/modules/dynamips/nios/nio_generic_ethernet.py b/gns3server/modules/dynamips/nios/nio_generic_ethernet.py index af631654..9690237a 100644 --- a/gns3server/modules/dynamips/nios/nio_generic_ethernet.py +++ b/gns3server/modules/dynamips/nios/nio_generic_ethernet.py @@ -39,13 +39,12 @@ class NIOGenericEthernet(NIO): def __init__(self, hypervisor, ethernet_device): - NIO.__init__(self, hypervisor) - - # create an unique ID - self._id = NIOGenericEthernet._instance_count + # create an unique ID and name + nio_id = NIOGenericEthernet._instance_count NIOGenericEthernet._instance_count += 1 - self._name = 'nio_gen_eth' + str(self._id) + name = 'nio_gen_eth' + str(nio_id) self._ethernet_device = ethernet_device + NIO.__init__(self, name, hypervisor) @classmethod def reset(cls): @@ -73,3 +72,8 @@ class NIOGenericEthernet(NIO): """ return self._ethernet_device + + def __json__(self): + + return {"type": "nio_generic_ethernet", + "ethernet_device": self._ethernet_device} diff --git a/gns3server/modules/dynamips/nios/nio_linux_ethernet.py b/gns3server/modules/dynamips/nios/nio_linux_ethernet.py index 1d3b280f..f121dbb6 100644 --- a/gns3server/modules/dynamips/nios/nio_linux_ethernet.py +++ b/gns3server/modules/dynamips/nios/nio_linux_ethernet.py @@ -39,13 +39,12 @@ class NIOLinuxEthernet(NIO): def __init__(self, hypervisor, ethernet_device): - NIO.__init__(self, hypervisor) - - # create an unique ID - self._id = NIOLinuxEthernet._instance_count + # create an unique ID and name + nio_id = NIOLinuxEthernet._instance_count NIOLinuxEthernet._instance_count += 1 - self._name = 'nio_linux_eth' + str(self._id) + name = 'nio_linux_eth' + str(nio_id) self._ethernet_device = ethernet_device + NIO.__init__(self, name, hypervisor) @classmethod def reset(cls): @@ -73,3 +72,8 @@ class NIOLinuxEthernet(NIO): """ return self._ethernet_device + + def __json__(self): + + return {"type": "nio_linux_ethernet", + "ethernet_device": self._ethernet_device} diff --git a/gns3server/modules/dynamips/nios/nio_mcast.py b/gns3server/modules/dynamips/nios/nio_mcast.py index ed6ea896..95a9f2ae 100644 --- a/gns3server/modules/dynamips/nios/nio_mcast.py +++ b/gns3server/modules/dynamips/nios/nio_mcast.py @@ -40,15 +40,14 @@ class NIOMcast(NIO): def __init__(self, hypervisor, group, port): - NIO.__init__(self, hypervisor) - - # create an unique ID - self._id = NIOMcast._instance_count + # create an unique ID and name + nio_id = NIOMcast._instance_count NIOMcast._instance_count += 1 - self._name = 'nio_mcast' + str(self._id) + name = 'nio_mcast' + str(nio_id) self._group = group self._port = port self._ttl = 1 # default TTL + NIO.__init__(self, name, hypervisor) @classmethod def reset(cls): @@ -109,3 +108,9 @@ class NIOMcast(NIO): yield from self._hypervisor.send("nio set_mcast_ttl {name} {ttl}".format(name=self._name, ttl=ttl)) self._ttl = ttl + + def __json__(self): + + return {"type": "nio_mcast", + "mgroup": self._mgroup, + "mport": self._mport} diff --git a/gns3server/modules/dynamips/nios/nio_null.py b/gns3server/modules/dynamips/nios/nio_null.py index b2c0e65f..e36fb0e5 100644 --- a/gns3server/modules/dynamips/nios/nio_null.py +++ b/gns3server/modules/dynamips/nios/nio_null.py @@ -38,12 +38,11 @@ class NIONull(NIO): def __init__(self, hypervisor): - NIO.__init__(self, hypervisor) - - # create an unique ID - self._id = NIONull._instance_count + # create an unique ID and name + nio_id = NIONull._instance_count NIONull._instance_count += 1 - self._name = 'nio_null' + str(self._id) + name = 'nio_null' + str(nio_id) + NIO.__init__(self, name, hypervisor) @classmethod def reset(cls): @@ -58,3 +57,7 @@ class NIONull(NIO): yield from self._hypervisor.send("nio create_null {}".format(self._name)) log.info("NIO NULL {name} created.".format(name=self._name)) + + def __json__(self): + + return {"type": "nio_null"} diff --git a/gns3server/modules/dynamips/nios/nio_tap.py b/gns3server/modules/dynamips/nios/nio_tap.py index e077161e..0e3b5683 100644 --- a/gns3server/modules/dynamips/nios/nio_tap.py +++ b/gns3server/modules/dynamips/nios/nio_tap.py @@ -39,13 +39,12 @@ class NIOTAP(NIO): def __init__(self, hypervisor, tap_device): - NIO.__init__(self, hypervisor) - - # create an unique ID - self._id = NIOTAP._instance_count + # create an unique ID and name + nio_id = NIOTAP._instance_count NIOTAP._instance_count += 1 - self._name = 'nio_tap' + str(self._id) + name = 'nio_tap' + str(nio_id) self._tap_device = tap_device + NIO.__init__(self, name, hypervisor) @classmethod def reset(cls): @@ -70,3 +69,8 @@ class NIOTAP(NIO): """ return self._tap_device + + def __json__(self): + + return {"type": "nio_tap", + "tap_device": self._tap_device} diff --git a/gns3server/modules/dynamips/nios/nio_udp.py b/gns3server/modules/dynamips/nios/nio_udp.py index f1b0ca18..c7016e6a 100644 --- a/gns3server/modules/dynamips/nios/nio_udp.py +++ b/gns3server/modules/dynamips/nios/nio_udp.py @@ -41,15 +41,14 @@ class NIOUDP(NIO): def __init__(self, hypervisor, lport, rhost, rport): - NIO.__init__(self, hypervisor) - - # create an unique ID - self._id = NIOUDP._instance_count + # create an unique ID and name + nio_id = NIOUDP._instance_count NIOUDP._instance_count += 1 - self._name = 'nio_udp' + str(self._id) + name = 'nio_udp' + str(nio_id) self._lport = lport self._rhost = rhost self._rport = rport + NIO.__init__(self, name, hypervisor) @classmethod def reset(cls): @@ -101,3 +100,10 @@ class NIOUDP(NIO): """ return self._rport + + def __json__(self): + + return {"type": "nio_udp", + "lport": self._lport, + "rport": self._rport, + "rhost": self._rhost} diff --git a/gns3server/modules/dynamips/nios/nio_udp_auto.py b/gns3server/modules/dynamips/nios/nio_udp_auto.py index 40eb6768..a7757199 100644 --- a/gns3server/modules/dynamips/nios/nio_udp_auto.py +++ b/gns3server/modules/dynamips/nios/nio_udp_auto.py @@ -41,17 +41,15 @@ class NIOUDPAuto(NIO): def __init__(self, hypervisor, laddr, lport_start, lport_end): - NIO.__init__(self, hypervisor) - - # create an unique ID - self._id = NIOUDPAuto._instance_count + # create an unique ID and name + nio_id = NIOUDPAuto._instance_count NIOUDPAuto._instance_count += 1 - self._name = 'nio_udp_auto' + str(self._id) - + name = 'nio_udp_auto' + str(nio_id) self._laddr = laddr self._lport = None self._raddr = None self._rport = None + NIO.__init__(self, name, hypervisor) @classmethod def reset(cls): @@ -133,3 +131,10 @@ class NIOUDPAuto(NIO): log.info("NIO UDP AUTO {name} connected to {raddr}:{rport}".format(name=self._name, raddr=raddr, rport=rport)) + + def __json__(self): + + return {"type": "nio_udp_auto", + "lport": self._lport, + "rport": self._rport, + "raddr": self._raddr} diff --git a/gns3server/modules/dynamips/nios/nio_unix.py b/gns3server/modules/dynamips/nios/nio_unix.py index d37c83ad..dddfaf82 100644 --- a/gns3server/modules/dynamips/nios/nio_unix.py +++ b/gns3server/modules/dynamips/nios/nio_unix.py @@ -40,14 +40,13 @@ class NIOUNIX(NIO): def __init__(self, hypervisor, local_file, remote_file): - NIO.__init__(self, hypervisor) - - # create an unique ID - self._id = NIOUNIX._instance_count + # create an unique ID and name + nio_id = NIOUNIX._instance_count NIOUNIX._instance_count += 1 - self._name = 'nio_unix' + str(self._id) + name = 'nio_unix' + str(nio_id) self._local_file = local_file self._remote_file = remote_file + NIO.__init__(self, name, hypervisor) @classmethod def reset(cls): @@ -87,3 +86,9 @@ class NIOUNIX(NIO): """ return self._remote_file + + def __json__(self): + + return {"type": "nio_unix", + "local_file": self._local_file, + "remote_file": self._remote_file} diff --git a/gns3server/modules/dynamips/nios/nio_vde.py b/gns3server/modules/dynamips/nios/nio_vde.py index 04b9fc07..c62ce2ca 100644 --- a/gns3server/modules/dynamips/nios/nio_vde.py +++ b/gns3server/modules/dynamips/nios/nio_vde.py @@ -19,6 +19,7 @@ Interface for VDE (Virtual Distributed Ethernet) NIOs (Unix based OSes only). """ +import asyncio from .nio import NIO import logging @@ -39,22 +40,13 @@ class NIOVDE(NIO): def __init__(self, hypervisor, control_file, local_file): - NIO.__init__(self, hypervisor) - - # create an unique ID - self._id = NIOVDE._instance_count + # create an unique ID and name + nio_id = NIOVDE._instance_count NIOVDE._instance_count += 1 - self._name = 'nio_vde' + str(self._id) + name = 'nio_vde' + str(nio_id) self._control_file = control_file self._local_file = local_file - - self._hypervisor.send("nio create_vde {name} {control} {local}".format(name=self._name, - control=control_file, - local=local_file)) - - log.info("NIO VDE {name} created with control={control}, local={local}".format(name=self._name, - control=control_file, - local=local_file)) + NIO.__init__(self, name, hypervisor) @classmethod def reset(cls): @@ -64,6 +56,17 @@ class NIOVDE(NIO): cls._instance_count = 0 + @asyncio.coroutine + def create(self): + + self._hypervisor.send("nio create_vde {name} {control} {local}".format(name=self._name, + control=self._control_file, + local=self._local_file)) + + log.info("NIO VDE {name} created with control={control}, local={local}".format(name=self._name, + control=self._control_file, + local=self._local_file)) + @property def control_file(self): """ @@ -83,3 +86,9 @@ class NIOVDE(NIO): """ return self._local_file + + def __json__(self): + + return {"type": "nio_vde", + "local_file": self._local_file, + "control_file": self._control_file} diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index bfff9d27..055cab13 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -145,16 +145,16 @@ class Router(BaseVM): "system_id": self._system_id} # FIXME: add default slots/wics - # slot_id = 0 + # slot_number = 0 # for slot in self._slots: # if slot: # slot = str(slot) - # router_defaults["slot" + str(slot_id)] = slot - # slot_id += 1 + # router_defaults["slot" + str(slot_number)] = slot + # slot_number += 1 # if self._slots[0] and self._slots[0].wics: - # for wic_slot_id in range(0, len(self._slots[0].wics)): - # router_defaults["wic" + str(wic_slot_id)] = None + # for wic_slot_number in range(0, len(self._slots[0].wics)): + # router_defaults["wic" + str(wic_slot_number)] = None return router_info @@ -991,23 +991,23 @@ class Router(BaseVM): return slot_bindings @asyncio.coroutine - def slot_add_binding(self, slot_id, adapter): + def slot_add_binding(self, slot_number, adapter): """ Adds a slot binding (a module into a slot). - :param slot_id: slot ID + :param slot_number: slot number :param adapter: device to add in the corresponding slot """ try: - slot = self._slots[slot_id] + slot = self._slots[slot_number] except IndexError: - raise DynamipsError('Slot {slot_id} does not exist on router "{name}"'.format(name=self._name, slot_id=slot_id)) + raise DynamipsError('Slot {slot_number} does not exist on router "{name}"'.format(name=self._name, slot_number=slot_number)) if slot is not None: current_adapter = slot - raise DynamipsError('Slot {slot_id} is already occupied by adapter {adapter} on router "{name}"'.format(name=self._name, - slot_id=slot_id, + raise DynamipsError('Slot {slot_number} is already occupied by adapter {adapter} on router "{name}"'.format(name=self._name, + slot_number=slot_number, adapter=current_adapter)) is_running = yield from self.is_running() @@ -1019,42 +1019,44 @@ class Router(BaseVM): raise DynamipsError('Adapter {adapter} cannot be added while router "{name}" is running'.format(adapter=adapter, name=self._name)) - yield from self._hypervisor.send('vm slot_add_binding "{name}" {slot_id} 0 {adapter}'.format(name=self._name, - slot_id=slot_id, - adapter=adapter)) + yield from self._hypervisor.send('vm slot_add_binding "{name}" {slot_number} 0 {adapter}'.format(name=self._name, + slot_number=slot_number, + adapter=adapter)) - log.info('Router "{name}" [{id}]: adapter {adapter} inserted into slot {slot_id}'.format(name=self._name, - id=self._id, - adapter=adapter, - slot_id=slot_id)) + log.info('Router "{name}" [{id}]: adapter {adapter} inserted into slot {slot_number}'.format(name=self._name, + id=self._id, + adapter=adapter, + slot_number=slot_number)) - self._slots[slot_id] = adapter + self._slots[slot_number] = adapter # Generate an OIR event if the router is running if is_running: - yield from self._hypervisor.send('vm slot_oir_start "{name}" {slot_id} 0'.format(name=self._name, slot_id=slot_id)) + yield from self._hypervisor.send('vm slot_oir_start "{name}" {slot_number} 0'.format(name=self._name, + slot_number=slot_number)) - log.info('Router "{name}" [{id}]: OIR start event sent to slot {slot_id}'.format(name=self._name, - id=self._id, - slot_id=slot_id)) + log.info('Router "{name}" [{id}]: OIR start event sent to slot {slot_number}'.format(name=self._name, + id=self._id, + slot_number=slot_number)) @asyncio.coroutine - def slot_remove_binding(self, slot_id): + def slot_remove_binding(self, slot_number): """ Removes a slot binding (a module from a slot). - :param slot_id: slot ID + :param slot_number: slot number """ try: - adapter = self._slots[slot_id] + adapter = self._slots[slot_number] except IndexError: - raise DynamipsError('Slot {slot_id} does not exist on router "{name}"'.format(name=self._name, slot_id=slot_id)) + raise DynamipsError('Slot {slot_number} does not exist on router "{name}"'.format(name=self._name, + slot_number=slot_number)) if adapter is None: - raise DynamipsError('No adapter in slot {slot_id} on router "{name}"'.format(name=self._name, - slot_id=slot_id)) + raise DynamipsError('No adapter in slot {slot_number} on router "{name}"'.format(name=self._name, + slot_number=slot_number)) is_running = yield from self.is_running() @@ -1068,239 +1070,245 @@ class Router(BaseVM): # Generate an OIR event if the router is running if is_running: - yield from self._hypervisor.send('vm slot_oir_stop "{name}" {slot_id} 0'.format(name=self._name, slot_id=slot_id)) + yield from self._hypervisor.send('vm slot_oir_stop "{name}" {slot_number} 0'.format(name=self._name, + slot_number=slot_number)) - log.info('Router "{name}" [{id}]: OIR stop event sent to slot {slot_id}'.format(name=self._name, + log.info('Router "{name}" [{id}]: OIR stop event sent to slot {slot_number}'.format(name=self._name, id=self._id, - slot_id=slot_id)) + slot_number=slot_number)) - yield from self._hypervisor.send('vm slot_remove_binding "{name}" {slot_id} 0'.format(name=self._name, slot_id=slot_id)) + yield from self._hypervisor.send('vm slot_remove_binding "{name}" {slot_number} 0'.format(name=self._name, + slot_number=slot_number)) - log.info('Router "{name}" [{id}]: adapter {adapter} removed from slot {slot_id}'.format(name=self._name, + log.info('Router "{name}" [{id}]: adapter {adapter} removed from slot {slot_number}'.format(name=self._name, id=self._id, adapter=adapter, - slot_id=slot_id)) - self._slots[slot_id] = None + slot_number=slot_number)) + self._slots[slot_number] = None @asyncio.coroutine - def install_wic(self, wic_slot_id, wic): + def install_wic(self, wic_slot_number, wic): """ Installs a WIC adapter into this router. - :param wic_slot_id: WIC slot ID + :param wic_slot_number: WIC slot number :param wic: WIC to be installed """ # WICs are always installed on adapters in slot 0 - slot_id = 0 + slot_number = 0 # Do not check if slot has an adapter because adapters with WICs interfaces # must be inserted by default in the router and cannot be removed. - adapter = self._slots[slot_id] + adapter = self._slots[slot_number] - if wic_slot_id > len(adapter.wics) - 1: - raise DynamipsError("WIC slot {wic_slot_id} doesn't exist".format(wic_slot_id=wic_slot_id)) + if wic_slot_number > len(adapter.wics) - 1: + raise DynamipsError("WIC slot {wic_slot_number} doesn't exist".format(wic_slot_number=wic_slot_number)) - if not adapter.wic_slot_available(wic_slot_id): - raise DynamipsError("WIC slot {wic_slot_id} is already occupied by another WIC".format(wic_slot_id=wic_slot_id)) + if not adapter.wic_slot_available(wic_slot_number): + raise DynamipsError("WIC slot {wic_slot_number} is already occupied by another WIC".format(wic_slot_number=wic_slot_number)) # Dynamips WICs slot IDs start on a multiple of 16 # WIC1 = 16, WIC2 = 32 and WIC3 = 48 - internal_wic_slot_id = 16 * (wic_slot_id + 1) - yield from self._hypervisor.send('vm slot_add_binding "{name}" {slot_id} {wic_slot_id} {wic}'.format(name=self._name, - slot_id=slot_id, - wic_slot_id=internal_wic_slot_id, + internal_wic_slot_number = 16 * (wic_slot_number + 1) + yield from self._hypervisor.send('vm slot_add_binding "{name}" {slot_number} {wic_slot_number} {wic}'.format(name=self._name, + slot_number=slot_number, + wic_slot_number=internal_wic_slot_number, wic=wic)) - log.info('Router "{name}" [{id}]: {wic} inserted into WIC slot {wic_slot_id}'.format(name=self._name, + log.info('Router "{name}" [{id}]: {wic} inserted into WIC slot {wic_slot_number}'.format(name=self._name, id=self._id, wic=wic, - wic_slot_id=wic_slot_id)) + wic_slot_number=wic_slot_number)) - adapter.install_wic(wic_slot_id, wic) + adapter.install_wic(wic_slot_number, wic) @asyncio.coroutine - def uninstall_wic(self, wic_slot_id): + def uninstall_wic(self, wic_slot_number): """ Uninstalls a WIC adapter from this router. - :param wic_slot_id: WIC slot ID + :param wic_slot_number: WIC slot number """ # WICs are always installed on adapters in slot 0 - slot_id = 0 + slot_number = 0 # Do not check if slot has an adapter because adapters with WICs interfaces # must be inserted by default in the router and cannot be removed. - adapter = self._slots[slot_id] + adapter = self._slots[slot_number] - if wic_slot_id > len(adapter.wics) - 1: - raise DynamipsError("WIC slot {wic_slot_id} doesn't exist".format(wic_slot_id=wic_slot_id)) + if wic_slot_number > len(adapter.wics) - 1: + raise DynamipsError("WIC slot {wic_slot_number} doesn't exist".format(wic_slot_number=wic_slot_number)) - if adapter.wic_slot_available(wic_slot_id): - raise DynamipsError("No WIC is installed in WIC slot {wic_slot_id}".format(wic_slot_id=wic_slot_id)) + if adapter.wic_slot_available(wic_slot_number): + raise DynamipsError("No WIC is installed in WIC slot {wic_slot_number}".format(wic_slot_number=wic_slot_number)) # Dynamips WICs slot IDs start on a multiple of 16 # WIC1 = 16, WIC2 = 32 and WIC3 = 48 - internal_wic_slot_id = 16 * (wic_slot_id + 1) - yield from self._hypervisor.send('vm slot_remove_binding "{name}" {slot_id} {wic_slot_id}'.format(name=self._name, - slot_id=slot_id, - wic_slot_id=internal_wic_slot_id)) + internal_wic_slot_number = 16 * (wic_slot_number + 1) + yield from self._hypervisor.send('vm slot_remove_binding "{name}" {slot_number} {wic_slot_number}'.format(name=self._name, + slot_number=slot_number, + wic_slot_number=internal_wic_slot_number)) - log.info('Router "{name}" [{id}]: {wic} removed from WIC slot {wic_slot_id}'.format(name=self._name, - id=self._id, - wic=adapter.wics[wic_slot_id], - wic_slot_id=wic_slot_id)) - adapter.uninstall_wic(wic_slot_id) + log.info('Router "{name}" [{id}]: {wic} removed from WIC slot {wic_slot_number}'.format(name=self._name, + id=self._id, + wic=adapter.wics[wic_slot_number], + wic_slot_number=wic_slot_number)) + adapter.uninstall_wic(wic_slot_number) @asyncio.coroutine - def get_slot_nio_bindings(self, slot_id): + def get_slot_nio_bindings(self, slot_number): """ Returns slot NIO bindings. - :param slot_id: slot ID + :param slot_number: slot number :returns: list of NIO bindings """ - nio_bindings = yield from self._hypervisor.send('vm slot_nio_bindings "{name}" {slot_id}'.format(name=self._name, - slot_id=slot_id)) + nio_bindings = yield from self._hypervisor.send('vm slot_nio_bindings "{name}" {slot_number}'.format(name=self._name, + slot_number=slot_number)) return nio_bindings @asyncio.coroutine - def slot_add_nio_binding(self, slot_id, port_id, nio): + def slot_add_nio_binding(self, slot_number, port_number, nio): """ Adds a slot NIO binding. - :param slot_id: slot ID - :param port_id: port ID + :param slot_number: slot number + :param port_number: port number :param nio: NIO instance to add to the slot/port """ try: - adapter = self._slots[slot_id] + adapter = self._slots[slot_number] except IndexError: - raise DynamipsError('Slot {slot_id} does not exist on router "{name}"'.format(name=self._name, - slot_id=slot_id)) - if not adapter.port_exists(port_id): - raise DynamipsError("Port {port_id} does not exist in adapter {adapter}".format(adapter=adapter, - port_id=port_id)) - - yield from self._hypervisor.send('vm slot_add_nio_binding "{name}" {slot_id} {port_id} {nio}'.format(name=self._name, - slot_id=slot_id, - port_id=port_id, - nio=nio)) - - log.info('Router "{name}" [{id}]: NIO {nio_name} bound to port {slot_id}/{port_id}'.format(name=self._name, - id=self._id, - nio_name=nio.name, - slot_id=slot_id, - port_id=port_id)) - - yield from self.slot_enable_nio(slot_id, port_id) - adapter.add_nio(port_id, nio) + raise DynamipsError('Slot {slot_number} does not exist on router "{name}"'.format(name=self._name, + slot_number=slot_number)) + if not adapter.port_exists(port_number): + raise DynamipsError("Port {port_number} does not exist in adapter {adapter}".format(adapter=adapter, + port_number=port_number)) + + yield from self._hypervisor.send('vm slot_add_nio_binding "{name}" {slot_number} {port_number} {nio}'.format(name=self._name, + slot_number=slot_number, + port_number=port_number, + nio=nio)) + + log.info('Router "{name}" [{id}]: NIO {nio_name} bound to port {slot_number}/{port_number}'.format(name=self._name, + id=self._id, + nio_name=nio.name, + slot_number=slot_number, + port_number=port_number)) + + yield from self.slot_enable_nio(slot_number, port_number) + adapter.add_nio(port_number, nio) @asyncio.coroutine - def slot_remove_nio_binding(self, slot_id, port_id): + def slot_remove_nio_binding(self, slot_number, port_number): """ Removes a slot NIO binding. - :param slot_id: slot ID - :param port_id: port ID + :param slot_number: slot number + :param port_number: port number :returns: removed NIO instance """ try: - adapter = self._slots[slot_id] + adapter = self._slots[slot_number] except IndexError: - raise DynamipsError('Slot {slot_id} does not exist on router "{name}"'.format(name=self._name, slot_id=slot_id)) - if not adapter.port_exists(port_id): - raise DynamipsError("Port {port_id} does not exist in adapter {adapter}".format(adapter=adapter, port_id=port_id)) - - yield from self.slot_disable_nio(slot_id, port_id) - yield from self._hypervisor.send('vm slot_remove_nio_binding "{name}" {slot_id} {port_id}'.format(name=self._name, - slot_id=slot_id, - port_id=port_id)) - - nio = adapter.get_nio(port_id) - adapter.remove_nio(port_id) - - log.info('Router "{name}" [{id}]: NIO {nio_name} removed from port {slot_id}/{port_id}'.format(name=self._name, - id=self._id, - nio_name=nio.name, - slot_id=slot_id, - port_id=port_id)) + raise DynamipsError('Slot {slot_number} does not exist on router "{name}"'.format(name=self._name, + slot_number=slot_number)) + if not adapter.port_exists(port_number): + raise DynamipsError("Port {port_number} does not exist in adapter {adapter}".format(adapter=adapter, + port_number=port_number)) + + yield from self.slot_disable_nio(slot_number, port_number) + yield from self._hypervisor.send('vm slot_remove_nio_binding "{name}" {slot_number} {port_number}'.format(name=self._name, + slot_number=slot_number, + port_number=port_number)) + + nio = adapter.get_nio(port_number) + adapter.remove_nio(port_number) + + log.info('Router "{name}" [{id}]: NIO {nio_name} removed from port {slot_number}/{port_number}'.format(name=self._name, + id=self._id, + nio_name=nio.name, + slot_number=slot_number, + port_number=port_number)) return nio @asyncio.coroutine - def slot_enable_nio(self, slot_id, port_id): + def slot_enable_nio(self, slot_number, port_number): """ Enables a slot NIO binding. - :param slot_id: slot ID - :param port_id: port ID + :param slot_number: slot number + :param port_number: port number """ is_running = yield from self.is_running() if is_running: # running router - yield from self._hypervisor.send('vm slot_enable_nio "{name}" {slot_id} {port_id}'.format(name=self._name, - slot_id=slot_id, - port_id=port_id)) + yield from self._hypervisor.send('vm slot_enable_nio "{name}" {slot_number} {port_number}'.format(name=self._name, + slot_number=slot_number, + port_number=port_number)) - log.info('Router "{name}" [{id}]: NIO enabled on port {slot_id}/{port_id}'.format(name=self._name, - id=self._id, - slot_id=slot_id, - port_id=port_id)) + log.info('Router "{name}" [{id}]: NIO enabled on port {slot_number}/{port_number}'.format(name=self._name, + id=self._id, + slot_number=slot_number, + port_number=port_number)) @asyncio.coroutine - def slot_disable_nio(self, slot_id, port_id): + def slot_disable_nio(self, slot_number, port_number): """ Disables a slot NIO binding. - :param slot_id: slot ID - :param port_id: port ID + :param slot_number: slot number + :param port_number: port number """ is_running = yield from self.is_running() if is_running: # running router - yield from self._hypervisor.send('vm slot_disable_nio "{name}" {slot_id} {port_id}'.format(name=self._name, - slot_id=slot_id, - port_id=port_id)) + yield from self._hypervisor.send('vm slot_disable_nio "{name}" {slot_number} {port_number}'.format(name=self._name, + slot_number=slot_number, + port_number=port_number)) - log.info('Router "{name}" [{id}]: NIO disabled on port {slot_id}/{port_id}'.format(name=self._name, - id=self._id, - slot_id=slot_id, - port_id=port_id)) + log.info('Router "{name}" [{id}]: NIO disabled on port {slot_number}/{port_number}'.format(name=self._name, + id=self._id, + slot_number=slot_number, + port_number=port_number)) @asyncio.coroutine - def start_capture(self, slot_id, port_id, output_file, data_link_type="DLT_EN10MB"): + def start_capture(self, slot_number, port_number, output_file, data_link_type="DLT_EN10MB"): """ Starts a packet capture. - :param slot_id: slot ID - :param port_id: port ID + :param slot_number: slot number + :param port_number: port number :param output_file: PCAP destination file for the capture :param data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB """ try: - adapter = self._slots[slot_id] + adapter = self._slots[slot_number] except IndexError: - raise DynamipsError('Slot {slot_id} does not exist on router "{name}"'.format(name=self._name, slot_id=slot_id)) - if not adapter.port_exists(port_id): - raise DynamipsError("Port {port_id} does not exist in adapter {adapter}".format(adapter=adapter, port_id=port_id)) + raise DynamipsError('Slot {slot_number} does not exist on router "{name}"'.format(name=self._name, + slot_number=slot_number)) + if not adapter.port_exists(port_number): + raise DynamipsError("Port {port_number} does not exist in adapter {adapter}".format(adapter=adapter, + port_number=port_number)) data_link_type = data_link_type.lower() if data_link_type.startswith("dlt_"): data_link_type = data_link_type[4:] - nio = adapter.get_nio(port_id) + nio = adapter.get_nio(port_number) if nio.input_filter[0] is not None and nio.output_filter[0] is not None: - raise DynamipsError("Port {port_id} has already a filter applied on {adapter}".format(adapter=adapter, - port_id=port_id)) + raise DynamipsError("Port {port_number} has already a filter applied on {adapter}".format(adapter=adapter, + port_number=port_number)) # FIXME: capture # try: @@ -1311,36 +1319,38 @@ class Router(BaseVM): yield from nio.bind_filter("both", "capture") yield from nio.setup_filter("both", '{} "{}"'.format(data_link_type, output_file)) - log.info('Router "{name}" [{id}]: starting packet capture on port {slot_id}/{port_id}'.format(name=self._name, - id=self._id, - nio_name=nio.name, - slot_id=slot_id, - port_id=port_id)) + log.info('Router "{name}" [{id}]: starting packet capture on port {slot_number}/{port_number}'.format(name=self._name, + id=self._id, + nio_name=nio.name, + slot_number=slot_number, + port_number=port_number)) @asyncio.coroutine - def stop_capture(self, slot_id, port_id): + def stop_capture(self, slot_number, port_number): """ Stops a packet capture. - :param slot_id: slot ID - :param port_id: port ID + :param slot_number: slot number + :param port_number: port number """ try: - adapter = self._slots[slot_id] + adapter = self._slots[slot_number] except IndexError: - raise DynamipsError('Slot {slot_id} does not exist on router "{name}"'.format(name=self._name, slot_id=slot_id)) - if not adapter.port_exists(port_id): - raise DynamipsError("Port {port_id} does not exist in adapter {adapter}".format(adapter=adapter, port_id=port_id)) + raise DynamipsError('Slot {slot_number} does not exist on router "{name}"'.format(name=self._name, + slot_number=slot_number)) + if not adapter.port_exists(port_number): + raise DynamipsError("Port {port_number} does not exist in adapter {adapter}".format(adapter=adapter, + port_number=port_number)) - nio = adapter.get_nio(port_id) + nio = adapter.get_nio(port_number) yield from nio.unbind_filter("both") - log.info('Router "{name}" [{id}]: stopping packet capture on port {slot_id}/{port_id}'.format(name=self._name, - id=self._id, - nio_name=nio.name, - slot_id=slot_id, - port_id=port_id)) + log.info('Router "{name}" [{id}]: stopping packet capture on port {slot_number}/{port_number}'.format(name=self._name, + id=self._id, + nio_name=nio.name, + slot_number=slot_number, + port_number=port_number)) def _create_slots(self, numslots): """ diff --git a/gns3server/schemas/dynamips.py b/gns3server/schemas/dynamips.py index d9fe845d..984b8ac6 100644 --- a/gns3server/schemas/dynamips.py +++ b/gns3server/schemas/dynamips.py @@ -46,6 +46,12 @@ VM_CREATE_SCHEMA = { "minLength": 1, "pattern": "^c[0-9]{4}$" }, + "chassis": { + "description": "router chassis model", + "type": "string", + "minLength": 1, + "pattern": "^[0-9]{4}(XM)?$" + }, "image": { "description": "path to the IOS image", "type": "string", @@ -265,6 +271,12 @@ VM_UPDATE_SCHEMA = { "minLength": 1, "pattern": "^c[0-9]{4}$" }, + "chassis": { + "description": "router chassis model", + "type": "string", + "minLength": 1, + "pattern": "^[0-9]{4}(XM)?$" + }, "image": { "description": "path to the IOS image", "type": "string", @@ -467,6 +479,147 @@ VM_UPDATE_SCHEMA = { "additionalProperties": False, } +VM_NIO_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to add a NIO for a Dynamips VM 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 + }, + }, + "oneOf": [ + {"$ref": "#/definitions/UDP"}, + {"$ref": "#/definitions/Ethernet"}, + {"$ref": "#/definitions/LinuxEthernet"}, + {"$ref": "#/definitions/TAP"}, + {"$ref": "#/definitions/UNIX"}, + {"$ref": "#/definitions/VDE"}, + {"$ref": "#/definitions/NULL"}, + ], + "additionalProperties": True, + "required": ["type"] +} + VM_OBJECT_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", "description": "Dynamips VM instance", @@ -501,6 +654,12 @@ VM_OBJECT_SCHEMA = { "minLength": 1, "pattern": "^c[0-9]{4}$" }, + "chassis": { + "description": "router chassis model", + "type": "string", + "minLength": 1, + "pattern": "^[0-9]{4}(XM)?$" + }, "image": { "description": "path to the IOS image", "type": "string", From 094339304c5868d567451de3e1e7a392f176d2d1 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 13 Feb 2015 15:41:56 -0700 Subject: [PATCH 246/485] Packet capture for Dynamips VMs. --- gns3server/handlers/dynamips_handler.py | 50 +++++++ .../modules/dynamips/adapters/adapter.py | 35 +++-- .../modules/virtualbox/virtualbox_vm.py | 136 +++++++++--------- gns3server/schemas/dynamips.py | 20 +++ 4 files changed, 155 insertions(+), 86 deletions(-) diff --git a/gns3server/handlers/dynamips_handler.py b/gns3server/handlers/dynamips_handler.py index 993bbd44..3ff92b43 100644 --- a/gns3server/handlers/dynamips_handler.py +++ b/gns3server/handlers/dynamips_handler.py @@ -16,11 +16,13 @@ # along with this program. If not, see . +import os import asyncio from ..web.route import Route from ..schemas.dynamips import VM_CREATE_SCHEMA from ..schemas.dynamips import VM_UPDATE_SCHEMA from ..schemas.dynamips import VM_NIO_SCHEMA +from ..schemas.dynamips import VM_CAPTURE_SCHEMA from ..schemas.dynamips import VM_OBJECT_SCHEMA from ..modules.dynamips import Dynamips from ..modules.project_manager import ProjectManager @@ -290,3 +292,51 @@ class DynamipsHandler: port_number = int(request.match_info["port_number"]) yield from vm.slot_remove_nio_binding(slot_number, port_number) response.set_status(204) + + @Route.post( + r"/projects/{project_id}/dynamips/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/start_capture", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance", + "adapter_number": "Adapter to start a packet capture", + "port_number": "Port on the adapter" + }, + status_codes={ + 200: "Capture started", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Start a packet capture on a Dynamips VM instance", + input=VM_CAPTURE_SCHEMA) + def start_capture(request, response): + + dynamips_manager = Dynamips.instance() + vm = dynamips_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + slot_number = int(request.match_info["adapter_number"]) + port_number = int(request.match_info["port_number"]) + pcap_file_path = os.path.join(vm.project.capture_working_directory(), request.json["capture_file_name"]) + yield from vm.start_capture(slot_number, port_number, pcap_file_path, request.json["data_link_type"]) + response.json({"pcap_file_path": pcap_file_path}) + + @Route.post( + r"/projects/{project_id}/dynamips/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/stop_capture", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance", + "adapter_number": "Adapter to stop a packet capture", + "port_number": "Port on the adapter (always 0)" + }, + status_codes={ + 204: "Capture stopped", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Stop a packet capture on a Dynamips VM instance") + def start_capture(request, response): + + dynamips_manager = Dynamips.instance() + vm = dynamips_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + slot_number = int(request.match_info["adapter_number"]) + port_number = int(request.match_info["port_number"]) + yield from vm.stop_capture(slot_number, port_number) + response.set_status(204) diff --git a/gns3server/modules/dynamips/adapters/adapter.py b/gns3server/modules/dynamips/adapters/adapter.py index 40d82c7e..9dd61619 100644 --- a/gns3server/modules/dynamips/adapters/adapter.py +++ b/gns3server/modules/dynamips/adapters/adapter.py @@ -28,10 +28,9 @@ class Adapter(object): def __init__(self, interfaces=0, wics=0): self._interfaces = interfaces - self._ports = {} - for port_id in range(0, interfaces): - self._ports[port_id] = None + for port_number in range(0, interfaces): + self._ports[port_number] = None self._wics = wics * [None] def removable(self): @@ -44,7 +43,7 @@ class Adapter(object): return True - def port_exists(self, port_id): + def port_exists(self, port_number): """ Checks if a port exists on this adapter. @@ -52,7 +51,7 @@ class Adapter(object): False otherwise. """ - if port_id in self._ports: + if port_number in self._ports: return True return False @@ -84,8 +83,8 @@ class Adapter(object): # WIC3 port 1 = 48, WIC3 port 2 = 49 base = 16 * (wic_slot_id + 1) for wic_port in range(0, wic.interfaces): - port_id = base + wic_port - self._ports[port_id] = None + port_number = base + wic_port + self._ports[port_number] = None def uninstall_wic(self, wic_slot_id): """ @@ -102,39 +101,39 @@ class Adapter(object): # WIC3 port 1 = 48, WIC3 port 2 = 49 base = 16 * (wic_slot_id + 1) for wic_port in range(0, wic.interfaces): - port_id = base + wic_port - del self._ports[port_id] + port_number = base + wic_port + del self._ports[port_number] self._wics[wic_slot_id] = None - def add_nio(self, port_id, nio): + def add_nio(self, port_number, nio): """ Adds a NIO to a port on this adapter. - :param port_id: port ID (integer) + :param port_number: port number (integer) :param nio: NIO instance """ - self._ports[port_id] = nio + self._ports[port_number] = nio - def remove_nio(self, port_id): + def remove_nio(self, port_number): """ Removes a NIO from a port on this adapter. - :param port_id: port ID (integer) + :param port_number: port number (integer) """ - self._ports[port_id] = None + self._ports[port_number] = None - def get_nio(self, port_id): + def get_nio(self, port_number): """ Returns the NIO assigned to a port. - :params port_id: port ID (integer) + :params port_number: port number (integer) :returns: NIO instance """ - return self._ports[port_id] + return self._ports[port_number] @property def ports(self): diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 202b59f2..318733a2 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -212,12 +212,12 @@ class VirtualBoxVM(BaseVM): except VirtualBoxError as e: log.warn("Could not deactivate the first serial port: {}".format(e)) - for adapter_id in range(0, len(self._ethernet_adapters)): - nio = self._ethernet_adapters[adapter_id].get_nio(0) + for adapter_number in range(0, len(self._ethernet_adapters)): + nio = self._ethernet_adapters[adapter_number].get_nio(0) if nio: - yield from self._modify_vm("--nictrace{} off".format(adapter_id + 1)) - yield from self._modify_vm("--cableconnected{} off".format(adapter_id + 1)) - yield from self._modify_vm("--nic{} null".format(adapter_id + 1)) + yield from self._modify_vm("--nictrace{} off".format(adapter_number + 1)) + yield from self._modify_vm("--cableconnected{} off".format(adapter_number + 1)) + yield from self._modify_vm("--nic{} null".format(adapter_number + 1)) @asyncio.coroutine def suspend(self): @@ -483,7 +483,7 @@ class VirtualBoxVM(BaseVM): raise VirtualBoxError("Number of adapters above the maximum supported of {}".format(self._maximum_adapters)) self._ethernet_adapters.clear() - for adapter_id in range(0, adapters): + for adapter_number in range(0, adapters): self._ethernet_adapters.append(EthernetAdapter()) self._adapters = len(self._ethernet_adapters) @@ -623,8 +623,8 @@ class VirtualBoxVM(BaseVM): nics = [] vm_info = yield from self._get_vm_info() - for adapter_id in range(0, maximum_adapters): - entry = "nic{}".format(adapter_id + 1) + for adapter_number in range(0, maximum_adapters): + entry = "nic{}".format(adapter_number + 1) if entry in vm_info: value = vm_info[entry] nics.append(value) @@ -639,15 +639,15 @@ class VirtualBoxVM(BaseVM): """ nic_attachments = yield from self._get_nic_attachements(self._maximum_adapters) - for adapter_id in range(0, len(self._ethernet_adapters)): - nio = self._ethernet_adapters[adapter_id].get_nio(0) + for adapter_number in range(0, len(self._ethernet_adapters)): + nio = self._ethernet_adapters[adapter_number].get_nio(0) if nio: - attachment = nic_attachments[adapter_id] + attachment = nic_attachments[adapter_number] if not self._use_any_adapter and attachment not in ("none", "null"): raise VirtualBoxError("Attachment ({}) already configured on adapter {}. " "Please set it to 'Not attached' to allow GNS3 to use it.".format(attachment, - adapter_id + 1)) - yield from self._modify_vm("--nictrace{} off".format(adapter_id + 1)) + adapter_number + 1)) + yield from self._modify_vm("--nictrace{} off".format(adapter_number + 1)) vbox_adapter_type = "82540EM" if self._adapter_type == "PCnet-PCI II (Am79C970A)": @@ -662,24 +662,24 @@ class VirtualBoxVM(BaseVM): vbox_adapter_type = "82545EM" if self._adapter_type == "Paravirtualized Network (virtio-net)": vbox_adapter_type = "virtio" - args = [self._vmname, "--nictype{}".format(adapter_id + 1), vbox_adapter_type] + args = [self._vmname, "--nictype{}".format(adapter_number + 1), vbox_adapter_type] yield from self.manager.execute("modifyvm", args) - log.debug("setting UDP params on adapter {}".format(adapter_id)) - yield from self._modify_vm("--nic{} generic".format(adapter_id + 1)) - yield from self._modify_vm("--nicgenericdrv{} UDPTunnel".format(adapter_id + 1)) - yield from self._modify_vm("--nicproperty{} sport={}".format(adapter_id + 1, nio.lport)) - yield from self._modify_vm("--nicproperty{} dest={}".format(adapter_id + 1, nio.rhost)) - yield from self._modify_vm("--nicproperty{} dport={}".format(adapter_id + 1, nio.rport)) - yield from self._modify_vm("--cableconnected{} on".format(adapter_id + 1)) + log.debug("setting UDP params on adapter {}".format(adapter_number)) + yield from self._modify_vm("--nic{} generic".format(adapter_number + 1)) + yield from self._modify_vm("--nicgenericdrv{} UDPTunnel".format(adapter_number + 1)) + yield from self._modify_vm("--nicproperty{} sport={}".format(adapter_number + 1, nio.lport)) + yield from self._modify_vm("--nicproperty{} dest={}".format(adapter_number + 1, nio.rhost)) + yield from self._modify_vm("--nicproperty{} dport={}".format(adapter_number + 1, nio.rport)) + yield from self._modify_vm("--cableconnected{} on".format(adapter_number + 1)) if nio.capturing: - yield from self._modify_vm("--nictrace{} on".format(adapter_id + 1)) - yield from self._modify_vm("--nictracefile{} {}".format(adapter_id + 1, nio.pcap_output_file)) + yield from self._modify_vm("--nictrace{} on".format(adapter_number + 1)) + yield from self._modify_vm("--nictracefile{} {}".format(adapter_number + 1, nio.pcap_output_file)) - for adapter_id in range(len(self._ethernet_adapters), self._maximum_adapters): - log.debug("disabling remaining adapter {}".format(adapter_id)) - yield from self._modify_vm("--nic{} none".format(adapter_id + 1)) + for adapter_number in range(len(self._ethernet_adapters), self._maximum_adapters): + log.debug("disabling remaining adapter {}".format(adapter_number)) + yield from self._modify_vm("--nic{} none".format(adapter_number + 1)) @asyncio.coroutine def _create_linked_clone(self): @@ -761,107 +761,107 @@ class VirtualBoxVM(BaseVM): self._serial_pipe = None @asyncio.coroutine - def adapter_add_nio_binding(self, adapter_id, nio): + def adapter_add_nio_binding(self, adapter_number, nio): """ Adds an adapter NIO binding. - :param adapter_id: adapter ID + :param adapter_number: adapter number :param nio: NIO instance to add to the slot/port """ try: - adapter = self._ethernet_adapters[adapter_id] + adapter = self._ethernet_adapters[adapter_number] except IndexError: - raise VirtualBoxError("Adapter {adapter_id} doesn't exist on VirtualBox VM '{name}'".format(name=self.name, - adapter_id=adapter_id)) + raise VirtualBoxError("Adapter {adapter_number} doesn't exist on VirtualBox VM '{name}'".format(name=self.name, + adapter_number=adapter_number)) vm_state = yield from self._get_vm_state() if vm_state == "running": # dynamically configure an UDP tunnel on the VirtualBox adapter - yield from self._control_vm("nic{} generic UDPTunnel".format(adapter_id + 1)) - yield from self._control_vm("nicproperty{} sport={}".format(adapter_id + 1, nio.lport)) - yield from self._control_vm("nicproperty{} dest={}".format(adapter_id + 1, nio.rhost)) - yield from self._control_vm("nicproperty{} dport={}".format(adapter_id + 1, nio.rport)) - yield from self._control_vm("setlinkstate{} on".format(adapter_id + 1)) + yield from self._control_vm("nic{} generic UDPTunnel".format(adapter_number + 1)) + yield from self._control_vm("nicproperty{} sport={}".format(adapter_number + 1, nio.lport)) + yield from self._control_vm("nicproperty{} dest={}".format(adapter_number + 1, nio.rhost)) + yield from self._control_vm("nicproperty{} dport={}".format(adapter_number + 1, nio.rport)) + yield from self._control_vm("setlinkstate{} on".format(adapter_number + 1)) adapter.add_nio(0, nio) - log.info("VirtualBox VM '{name}' [{id}]: {nio} added to adapter {adapter_id}".format(name=self.name, - id=self.id, - nio=nio, - adapter_id=adapter_id)) + log.info("VirtualBox VM '{name}' [{id}]: {nio} added to adapter {adapter_number}".format(name=self.name, + id=self.id, + nio=nio, + adapter_number=adapter_number)) @asyncio.coroutine - def adapter_remove_nio_binding(self, adapter_id): + def adapter_remove_nio_binding(self, adapter_number): """ Removes an adapter NIO binding. - :param adapter_id: adapter ID + :param adapter_number: adapter number :returns: NIO instance """ try: - adapter = self._ethernet_adapters[adapter_id] + adapter = self._ethernet_adapters[adapter_number] except IndexError: - raise VirtualBoxError("Adapter {adapter_id} doesn't exist on VirtualBox VM '{name}'".format(name=self.name, - adapter_id=adapter_id)) + raise VirtualBoxError("Adapter {adapter_number} doesn't exist on VirtualBox VM '{name}'".format(name=self.name, + adapter_number=adapter_number)) vm_state = yield from self._get_vm_state() if vm_state == "running": # dynamically disable the VirtualBox adapter - yield from self._control_vm("setlinkstate{} off".format(adapter_id + 1)) - yield from self._control_vm("nic{} null".format(adapter_id + 1)) + yield from self._control_vm("setlinkstate{} off".format(adapter_number + 1)) + yield from self._control_vm("nic{} null".format(adapter_number + 1)) nio = adapter.get_nio(0) if str(nio) == "NIO UDP": self.manager.port_manager.release_udp_port(nio.lport) adapter.remove_nio(0) - log.info("VirtualBox VM '{name}' [{id}]: {nio} removed from adapter {adapter_id}".format(name=self.name, - id=self.id, - nio=nio, - adapter_id=adapter_id)) + log.info("VirtualBox VM '{name}' [{id}]: {nio} removed from adapter {adapter_number}".format(name=self.name, + id=self.id, + nio=nio, + adapter_number=adapter_number)) return nio - def start_capture(self, adapter_id, output_file): + def start_capture(self, adapter_number, output_file): """ Starts a packet capture. - :param adapter_id: adapter ID + :param adapter_number: adapter number :param output_file: PCAP destination file for the capture """ try: - adapter = self._ethernet_adapters[adapter_id] + adapter = self._ethernet_adapters[adapter_number] except IndexError: - raise VirtualBoxError("Adapter {adapter_id} doesn't exist on VirtualBox VM '{name}'".format(name=self.name, - adapter_id=adapter_id)) + raise VirtualBoxError("Adapter {adapter_number} doesn't exist on VirtualBox VM '{name}'".format(name=self.name, + adapter_number=adapter_number)) nio = adapter.get_nio(0) if nio.capturing: - raise VirtualBoxError("Packet capture is already activated on adapter {adapter_id}".format(adapter_id=adapter_id)) + raise VirtualBoxError("Packet capture is already activated on adapter {adapter_number}".format(adapter_number=adapter_number)) nio.startPacketCapture(output_file) - log.info("VirtualBox VM '{name}' [{id}]: starting packet capture on adapter {adapter_id}".format(name=self.name, - id=self.id, - adapter_id=adapter_id)) + log.info("VirtualBox VM '{name}' [{id}]: starting packet capture on adapter {adapter_number}".format(name=self.name, + id=self.id, + adapter_number=adapter_number)) - def stop_capture(self, adapter_id): + def stop_capture(self, adapter_number): """ Stops a packet capture. - :param adapter_id: adapter ID + :param adapter_number: adapter number """ try: - adapter = self._ethernet_adapters[adapter_id] + adapter = self._ethernet_adapters[adapter_number] except IndexError: - raise VirtualBoxError("Adapter {adapter_id} doesn't exist on VirtualBox VM '{name}'".format(name=self.name, - adapter_id=adapter_id)) + raise VirtualBoxError("Adapter {adapter_number} doesn't exist on VirtualBox VM '{name}'".format(name=self.name, + adapter_number=adapter_number)) nio = adapter.get_nio(0) nio.stopPacketCapture() - log.info("VirtualBox VM '{name}' [{id}]: stopping packet capture on adapter {adapter_id}".format(name=self.name, - id=self.id, - adapter_id=adapter_id)) + log.info("VirtualBox VM '{name}' [{id}]: stopping packet capture on adapter {adapter_number}".format(name=self.name, + id=self.id, + adapter_number=adapter_number)) diff --git a/gns3server/schemas/dynamips.py b/gns3server/schemas/dynamips.py index 984b8ac6..c30d2920 100644 --- a/gns3server/schemas/dynamips.py +++ b/gns3server/schemas/dynamips.py @@ -620,6 +620,26 @@ VM_NIO_SCHEMA = { "required": ["type"] } +VM_CAPTURE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to start a packet capture on a Dynamips VM instance port", + "type": "object", + "properties": { + "capture_file_name": { + "description": "Capture file name", + "type": "string", + "minLength": 1, + }, + "data_link_type": { + "description": "PCAP data link type", + "type": "string", + "minLength": 1, + }, + }, + "additionalProperties": False, + "required": ["capture_file_name", "data_link_type"] +} + VM_OBJECT_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", "description": "Dynamips VM instance", From 4f38d96522a20d3abad956c91fae47c922e85ada Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 13 Feb 2015 20:01:18 -0700 Subject: [PATCH 247/485] Dynamips devices. --- .../modules/dynamips/dynamips_device.py | 45 +++ .../modules/dynamips/nodes/atm_switch.py | 348 ++++++++++++++++++ gns3server/modules/dynamips/nodes/bridge.py | 104 ++++++ gns3server/modules/dynamips/nodes/device.py | 54 +++ .../modules/dynamips/nodes/ethernet_hub.py | 161 ++++++++ .../modules/dynamips/nodes/ethernet_switch.py | 285 ++++++++++++++ .../dynamips/nodes/frame_relay_switch.py | 267 ++++++++++++++ gns3server/modules/dynamips/nodes/router.py | 7 +- gns3server/schemas/dynamips.py | 278 ++++++++++++++ tests/api/test_virtualbox.py | 4 +- 10 files changed, 1545 insertions(+), 8 deletions(-) create mode 100644 gns3server/modules/dynamips/dynamips_device.py create mode 100644 gns3server/modules/dynamips/nodes/atm_switch.py create mode 100644 gns3server/modules/dynamips/nodes/bridge.py create mode 100644 gns3server/modules/dynamips/nodes/device.py create mode 100644 gns3server/modules/dynamips/nodes/ethernet_hub.py create mode 100644 gns3server/modules/dynamips/nodes/ethernet_switch.py create mode 100644 gns3server/modules/dynamips/nodes/frame_relay_switch.py diff --git a/gns3server/modules/dynamips/dynamips_device.py b/gns3server/modules/dynamips/dynamips_device.py new file mode 100644 index 00000000..bf5012d1 --- /dev/null +++ b/gns3server/modules/dynamips/dynamips_device.py @@ -0,0 +1,45 @@ +# -*- 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 .dynamips_error import DynamipsError +from .nodes.atm_switch import ATMSwitch +from .nodes.ethernet_switch import EthernetSwitch +from .nodes.ethernet_hub import EthernetHub +from .nodes.frame_relay_switch import FrameRelaySwitch + +import logging +log = logging.getLogger(__name__) + +DEVICES = {'atmsw': ATMSwitch, + 'frsw': FrameRelaySwitch, + 'ethsw': EthernetSwitch, + 'ethhub': EthernetHub} + + +class DynamipsDevice: + + """ + Factory to create an Device object based on the type + """ + + def __new__(cls, name, vm_id, project, manager, device_type, **kwargs): + + if type not in DEVICES: + raise DynamipsError("Unknown device type: {}".format(device_type)) + + return DEVICES[device_type](name, vm_id, project, manager, **kwargs) diff --git a/gns3server/modules/dynamips/nodes/atm_switch.py b/gns3server/modules/dynamips/nodes/atm_switch.py new file mode 100644 index 00000000..b2186484 --- /dev/null +++ b/gns3server/modules/dynamips/nodes/atm_switch.py @@ -0,0 +1,348 @@ +# -*- 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 . + +""" +Interface for Dynamips virtual ATM switch module ("atmsw"). +http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L593 +""" + +import asyncio + +from .device import Device +from ..dynamips_error import DynamipsError + +import logging +log = logging.getLogger(__name__) + + +class ATMSwitch(Device): + """ + Dynamips ATM switch. + + :param name: name for this switch + :param node_id: Node instance identifier + :param project: Project instance + :param manager: Parent VM Manager + :param hypervisor: Dynamips hypervisor instance + """ + + def __init__(self, name, node_id, project, manager, hypervisor=None): + + super().__init__(name, node_id, project, manager) + self._nios = {} + self._mapping = {} + + @asyncio.coroutine + def create(self): + + if self._hypervisor is None: + self._hypervisor = yield from self.manager.start_new_hypervisor() + + yield from self._hypervisor.send('atmsw create "{}"'.format(self._name)) + log.info('ATM switch "{name}" [{id}] has been created'.format(name=self._name, id=self._id)) + self._hypervisor.devices.append(self) + + @asyncio.coroutine + def set_name(self, new_name): + """ + Renames this ATM switch. + + :param new_name: New name for this switch + """ + + yield from self._hypervisor.send('atm rename "{name}" "{new_name}"'.format(name=self._name, new_name=new_name)) + log.info('ATM switch "{name}" [{id}]: renamed to "{new_name}"'.format(name=self._name, + id=self._id, + new_name=new_name)) + self._name = new_name + + + @property + def nios(self): + """ + Returns all the NIOs member of this ATM switch. + + :returns: nio list + """ + + return self._nios + + @property + def mapping(self): + """ + Returns port mapping + + :returns: mapping list + """ + + return self._mapping + + @asyncio.coroutine + def delete(self): + """ + Deletes this ATM switch. + """ + + yield from self._hypervisor.send('atmsw delete "{}"'.format(self._name)) + log.info('ATM switch "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id)) + self._hypervisor.devices.remove(self) + self._instances.remove(self._id) + + def has_port(self, port): + """ + Checks if a port exists on this ATM switch. + + :returns: boolean + """ + + if port in self._nios: + return True + return False + + def add_nio(self, nio, port_number): + """ + Adds a NIO as new port on ATM switch. + + :param nio: NIO instance to add + :param port_number: port to allocate for the NIO + """ + + if port_number in self._nios: + raise DynamipsError("Port {} isn't free".format(port_number)) + + log.info('ATM switch "{name}" [id={id}]: NIO {nio} bound to port {port}'.format(name=self._name, + id=self._id, + nio=nio, + port=port_number)) + + self._nios[port_number] = nio + + def remove_nio(self, port_number): + """ + Removes the specified NIO as member of this ATM switch. + + :param port_number: allocated port number + """ + + if port_number not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port_number)) + + nio = self._nios[port_number] + log.info('ATM switch "{name}" [{id}]: NIO {nio} removed from port {port}'.format(name=self._name, + id=self._id, + nio=nio, + port=port_number)) + + del self._nios[port_number] + return nio + + @asyncio.coroutine + def map_vp(self, port1, vpi1, port2, vpi2): + """ + Creates a new Virtual Path connection. + + :param port1: input port + :param vpi1: input vpi + :param port2: output port + :param vpi2: output vpi + """ + + if port1 not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port1)) + + if port2 not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port2)) + + nio1 = self._nios[port1] + nio2 = self._nios[port2] + + yield from self._hypervisor.send('atmsw create_vpc "{name}" {input_nio} {input_vpi} {output_nio} {output_vpi}'.format(name=self._name, + input_nio=nio1, + input_vpi=vpi1, + output_nio=nio2, + output_vpi=vpi2)) + + log.info('ATM switch "{name}" [{id}]: VPC from port {port1} VPI {vpi1} to port {port2} VPI {vpi2} created'.format(name=self._name, + id=self._id, + port1=port1, + vpi1=vpi1, + port2=port2, + vpi2=vpi2)) + + self._mapping[(port1, vpi1)] = (port2, vpi2) + + @asyncio.coroutine + def unmap_vp(self, port1, vpi1, port2, vpi2): + """ + Deletes a new Virtual Path connection. + + :param port1: input port + :param vpi1: input vpi + :param port2: output port + :param vpi2: output vpi + """ + + if port1 not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port1)) + + if port2 not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port2)) + + nio1 = self._nios[port1] + nio2 = self._nios[port2] + + yield from self._hypervisor.send('atmsw delete_vpc "{name}" {input_nio} {input_vpi} {output_nio} {output_vpi}'.format(name=self._name, + input_nio=nio1, + input_vpi=vpi1, + output_nio=nio2, + output_vpi=vpi2)) + + log.info('ATM switch "{name}" [{id}]: VPC from port {port1} VPI {vpi1} to port {port2} VPI {vpi2} deleted'.format(name=self._name, + id=self._id, + port1=port1, + vpi1=vpi1, + port2=port2, + vpi2=vpi2)) + + del self._mapping[(port1, vpi1)] + + @asyncio.coroutine + def map_pvc(self, port1, vpi1, vci1, port2, vpi2, vci2): + """ + Creates a new Virtual Channel connection (unidirectional). + + :param port1: input port + :param vpi1: input vpi + :param vci1: input vci + :param port2: output port + :param vpi2: output vpi + :param vci2: output vci + """ + + if port1 not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port1)) + + if port2 not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port2)) + + nio1 = self._nios[port1] + nio2 = self._nios[port2] + + yield from self._hypervisor.send('atmsw create_vcc "{name}" {input_nio} {input_vpi} {input_vci} {output_nio} {output_vpi} {output_vci}'.format(name=self._name, + input_nio=nio1, + input_vpi=vpi1, + input_vci=vci1, + output_nio=nio2, + output_vpi=vpi2, + output_vci=vci2)) + + log.info('ATM switch "{name}" [{id}]: VCC from port {port1} VPI {vpi1} VCI {vci1} to port {port2} VPI {vpi2} VCI {vci2} created'.format(name=self._name, + id=self._id, + port1=port1, + vpi1=vpi1, + vci1=vci1, + port2=port2, + vpi2=vpi2, + vci2=vci2)) + + self._mapping[(port1, vpi1, vci1)] = (port2, vpi2, vci2) + + @asyncio.coroutine + def unmap_pvc(self, port1, vpi1, vci1, port2, vpi2, vci2): + """ + Deletes a new Virtual Channel connection (unidirectional). + + :param port1: input port + :param vpi1: input vpi + :param vci1: input vci + :param port2: output port + :param vpi2: output vpi + :param vci2: output vci + """ + + if port1 not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port1)) + + if port2 not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port2)) + + nio1 = self._nios[port1] + nio2 = self._nios[port2] + + yield from self._hypervisor.send('atmsw delete_vcc "{name}" {input_nio} {input_vpi} {input_vci} {output_nio} {output_vpi} {output_vci}'.format(name=self._name, + input_nio=nio1, + input_vpi=vpi1, + input_vci=vci1, + output_nio=nio2, + output_vpi=vpi2, + output_vci=vci2)) + + log.info('ATM switch "{name}" [{id}]: VCC from port {port1} VPI {vpi1} VCI {vci1} to port {port2} VPI {vpi2} VCI {vci2} deleted'.format(name=self._name, + id=self._id, + port1=port1, + vpi1=vpi1, + vci1=vci1, + port2=port2, + vpi2=vpi2, + vci2=vci2)) + del self._mapping[(port1, vpi1, vci1)] + + @asyncio.coroutine + def start_capture(self, port_number, output_file, data_link_type="DLT_ATM_RFC1483"): + """ + Starts a packet capture. + + :param port_number: allocated port number + :param output_file: PCAP destination file for the capture + :param data_link_type: PCAP data link type (DLT_*), default is DLT_ATM_RFC1483 + """ + + if port_number not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port_number)) + + nio = self._nios[port_number] + + data_link_type = data_link_type.lower() + if data_link_type.startswith("dlt_"): + data_link_type = data_link_type[4:] + + if nio.input_filter[0] is not None and nio.output_filter[0] is not None: + raise DynamipsError("Port {} has already a filter applied".format(port_number)) + + yield from nio.bind_filter("both", "capture") + yield from nio.setup_filter("both", "{} {}".format(data_link_type, output_file)) + + log.info('ATM switch "{name}" [{id}]: starting packet capture on {port}'.format(name=self._name, + id=self._id, + port=port_number)) + + @asyncio.coroutine + def stop_capture(self, port_number): + """ + Stops a packet capture. + + :param port_number: allocated port number + """ + + if port_number not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port_number)) + + nio = self._nios[port_number] + yield from nio.unbind_filter("both") + log.info('ATM switch "{name}" [{id}]: stopping packet capture on {port}'.format(name=self._name, + id=self._id, + port=port_number)) diff --git a/gns3server/modules/dynamips/nodes/bridge.py b/gns3server/modules/dynamips/nodes/bridge.py new file mode 100644 index 00000000..0f00137c --- /dev/null +++ b/gns3server/modules/dynamips/nodes/bridge.py @@ -0,0 +1,104 @@ +# -*- 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 . + +""" +Interface for Dynamips NIO bridge module ("nio_bridge"). +http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L538 +""" + +import asyncio +from .device import Device + + +class Bridge(Device): + """ + Dynamips bridge. + + :param name: name for this bridge + :param node_id: Node instance identifier + :param project: Project instance + :param manager: Parent VM Manager + :param hypervisor: Dynamips hypervisor instance + """ + + def __init__(self, name, node_id, project, manager, hypervisor=None): + + super().__init__(name, node_id, project, manager, hypervisor) + self._nios = [] + + @asyncio.coroutine + def create(self): + + if self._hypervisor is None: + self._hypervisor = yield from self.manager.start_new_hypervisor() + + yield from self._hypervisor.send('nio_bridge create "{}"'.format(self._name)) + self._hypervisor.devices.append(self) + + @asyncio.coroutine + def set_name(self, new_name): + """ + Renames this bridge. + + :param new_name: New name for this bridge + """ + + yield from self._hypervisor.send('nio_bridge rename "{name}" "{new_name}"'.format(name=self._name, + new_name=new_name)) + + self._name = new_name + + @property + def nios(self): + """ + Returns all the NIOs member of this bridge. + + :returns: nio list + """ + + return self._nios + + @asyncio.coroutine + def delete(self): + """ + Deletes this bridge. + """ + + yield from self._hypervisor.send('nio_bridge delete "{}"'.format(self._name)) + self._hypervisor.devices.remove(self) + + @asyncio.coroutine + def add_nio(self, nio): + """ + Adds a NIO as new port on this bridge. + + :param nio: NIO instance to add + """ + + yield from self._hypervisor.send('nio_bridge add_nio "{name}" {nio}'.format(name=self._name, nio=nio)) + self._nios.append(nio) + + @asyncio.coroutine + def remove_nio(self, nio): + """ + Removes the specified NIO as member of this bridge. + + :param nio: NIO instance to remove + """ + + yield from self._hypervisor.send('nio_bridge remove_nio "{name}" {nio}'.format(name=self._name, nio=nio)) + self._nios.remove(nio) diff --git a/gns3server/modules/dynamips/nodes/device.py b/gns3server/modules/dynamips/nodes/device.py new file mode 100644 index 00000000..f5b7ee75 --- /dev/null +++ b/gns3server/modules/dynamips/nodes/device.py @@ -0,0 +1,54 @@ +# -*- 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 ...base_vm import BaseVM + + +class Device(BaseVM): + """ + Base device for switches and hubs + + :param name: name for this bridge + :param vm_id: Node instance identifier + :param project: Project instance + :param manager: Parent VM Manager + :param hypervisor: Dynamips hypervisor instance + """ + + def __init__(self, name, node_id, project, manager, hypervisor=None): + + super().__init__(name, node_id, project, manager) + self._hypervisor = hypervisor + + @property + def hypervisor(self): + """ + Returns the current hypervisor. + + :returns: hypervisor instance + """ + + return self._hypervisor + + def start(self): + + pass # Dynamips switches and hubs are always on + + def stop(self): + + pass # Dynamips switches and hubs are always on diff --git a/gns3server/modules/dynamips/nodes/ethernet_hub.py b/gns3server/modules/dynamips/nodes/ethernet_hub.py new file mode 100644 index 00000000..5f7228c7 --- /dev/null +++ b/gns3server/modules/dynamips/nodes/ethernet_hub.py @@ -0,0 +1,161 @@ +# -*- 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 . + +""" +Hub object that uses the Bridge interface to create a hub with ports. +""" + +import asyncio + +from .bridge import Bridge +from ..dynamips_error import DynamipsError + +import logging +log = logging.getLogger(__name__) + + +class EthernetHub(Bridge): + """ + Dynamips Ethernet hub (based on Bridge) + + :param name: name for this hub + :param node_id: Node instance identifier + :param project: Project instance + :param manager: Parent VM Manager + :param hypervisor: Dynamips hypervisor instance + """ + + def __init__(self, name, node_id, project, manager, hypervisor=None): + + Bridge.__init__(self, name, node_id, project, manager, hypervisor) + self._mapping = {} + + @asyncio.coroutine + def create(self): + + yield from Bridge.create() + log.info('Ethernet hub "{name}" [{id}] has been created'.format(name=self._name, id=self._id)) + + @property + def mapping(self): + """ + Returns port mapping + + :returns: mapping list + """ + + return self._mapping + + @asyncio.coroutine + def delete(self): + """ + Deletes this hub. + """ + + yield from Bridge.delete(self) + log.info('Ethernet hub "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id)) + self._instances.remove(self._id) + + @asyncio.coroutine + def add_nio(self, nio, port_number): + """ + Adds a NIO as new port on this hub. + + :param nio: NIO instance to add + :param port_number: port to allocate for the NIO + """ + + if port_number in self._mapping: + raise DynamipsError("Port {} isn't free".format(port_number)) + + yield from Bridge.add_nio(self, nio) + + log.info('Ethernet hub "{name}" [{id}]: NIO {nio} bound to port {port}'.format(name=self._name, + id=self._id, + nio=nio, + port=port_number)) + self._mapping[port_number] = nio + + @asyncio.coroutine + def remove_nio(self, port_number): + """ + Removes the specified NIO as member of this hub. + + :param port_number: allocated port number + + :returns: the NIO that was bound to the allocated port + """ + + if port_number not in self._mapping: + raise DynamipsError("Port {} is not allocated".format(port_number)) + + nio = self._mapping[port_number] + yield from Bridge.remove_nio(self, nio) + + log.info('Ethernet switch "{name}" [{id}]: NIO {nio} removed from port {port}'.format(name=self._name, + id=self._id, + nio=nio, + port=port_number)) + + del self._mapping[port_number] + return nio + + @asyncio.coroutine + def start_capture(self, port_number, output_file, data_link_type="DLT_EN10MB"): + """ + Starts a packet capture. + + :param port_number: allocated port number + :param output_file: PCAP destination file for the capture + :param data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB + """ + + if port_number not in self._mapping: + raise DynamipsError("Port {} is not allocated".format(port_number)) + + nio = self._mapping[port_number] + + data_link_type = data_link_type.lower() + if data_link_type.startswith("dlt_"): + data_link_type = data_link_type[4:] + + if nio.input_filter[0] is not None and nio.output_filter[0] is not None: + raise DynamipsError("Port {} has already a filter applied".format(port_number)) + + yield from nio.bind_filter("both", "capture") + yield from nio.setup_filter("both", "{} {}".format(data_link_type, output_file)) + + log.info('Ethernet hub "{name}" [{id}]: starting packet capture on {port}'.format(name=self._name, + id=self._id, + port=port_number)) + + @asyncio.coroutine + def stop_capture(self, port_number): + """ + Stops a packet capture. + + :param port_number: allocated port number + """ + + if port_number not in self._mapping: + raise DynamipsError("Port {} is not allocated".format(port_number)) + + nio = self._mapping[port_number] + yield from nio.unbind_filter("both") + log.info('Ethernet hub "{name}" [{id}]: stopping packet capture on {port}'.format(name=self._name, + id=self._id, + port=port_number)) diff --git a/gns3server/modules/dynamips/nodes/ethernet_switch.py b/gns3server/modules/dynamips/nodes/ethernet_switch.py new file mode 100644 index 00000000..24fe3020 --- /dev/null +++ b/gns3server/modules/dynamips/nodes/ethernet_switch.py @@ -0,0 +1,285 @@ +# -*- 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 . + +""" +Interface for Dynamips virtual Ethernet switch module ("ethsw"). +http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L558 +""" + +import asyncio + +from .device import Device +from ..dynamips_error import DynamipsError + + +import logging +log = logging.getLogger(__name__) + + +class EthernetSwitch(Device): + """ + Dynamips Ethernet switch. + + :param name: name for this switch + :param node_id: Node instance identifier + :param project: Project instance + :param manager: Parent VM Manager + :param hypervisor: Dynamips hypervisor instance + """ + + def __init__(self, name, node_id, project, manager, hypervisor=None): + + super().__init__(name, node_id, project, manager, hypervisor) + self._nios = {} + self._mapping = {} + + @asyncio.coroutine + def create(self): + + if self._hypervisor is None: + self._hypervisor = yield from self.manager.start_new_hypervisor() + + yield from self._hypervisor.send('ethsw create "{}"'.format(self._name)) + log.info('Ethernet switch "{name}" [{id}] has been created'.format(name=self._name, id=self._id)) + self._hypervisor.devices.append(self) + + @asyncio.coroutine + def set_name(self, new_name): + """ + Renames this Ethernet switch. + + :param new_name: New name for this switch + """ + + yield from self._hypervisor.send('ethsw rename "{name}" "{new_name}"'.format(name=self._name, new_name=new_name)) + log.info('Ethernet switch "{name}" [{id}]: renamed to "{new_name}"'.format(name=self._name, + id=self._id, + new_name=new_name)) + self._name = new_name + + @property + def nios(self): + """ + Returns all the NIOs member of this Ethernet switch. + + :returns: nio list + """ + + return self._nios + + @property + def mapping(self): + """ + Returns port mapping + + :returns: mapping list + """ + + return self._mapping + + @asyncio.coroutine + def delete(self): + """ + Deletes this Ethernet switch. + """ + + yield from self._hypervisor.send('ethsw delete "{}"'.format(self._name)) + log.info('Ethernet switch "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id)) + self._hypervisor.devices.remove(self) + self._instances.remove(self._id) + + @asyncio.coroutine + def add_nio(self, nio, port_number): + """ + Adds a NIO as new port on Ethernet switch. + + :param nio: NIO instance to add + :param port_number: port to allocate for the NIO + """ + + if port_number in self._nios: + raise DynamipsError("Port {} isn't free".format(port_number)) + + yield from self._hypervisor.send('ethsw add_nio "{name}" {nio}'.format(name=self._name, nio=nio)) + + log.info('Ethernet switch "{name}" [{id}]: NIO {nio} bound to port {port}'.format(name=self._name, + id=self._id, + nio=nio, + port=port_number)) + self._nios[port_number] = nio + + @asyncio.coroutine + def remove_nio(self, port_number): + """ + Removes the specified NIO as member of this Ethernet switch. + + :param port_number: allocated port number + + :returns: the NIO that was bound to the port + """ + + if port_number not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port_number)) + + nio = self._nios[port_number] + yield from self._hypervisor.send('ethsw remove_nio "{name}" {nio}'.format(name=self._name, nio=nio)) + + log.info('Ethernet switch "{name}" [{id}]: NIO {nio} removed from port {port}'.format(name=self._name, + id=self._id, + nio=nio, + port=port_number)) + + del self._nios[port_number] + if port_number in self._mapping: + del self._mapping[port_number] + + return nio + + @asyncio.coroutine + def set_access_port(self, port_number, vlan_id): + """ + Sets the specified port as an ACCESS port. + + :param port_number: allocated port number + :param vlan_id: VLAN number membership + """ + + if port_number not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port_number)) + + nio = self._nios[port_number] + yield from self._hypervisor.send('ethsw set_access_port "{name}" {nio} {vlan_id}'.format(name=self._name, + nio=nio, + vlan_id=vlan_id)) + + log.info('Ethernet switch "{name}" [{id}]: port {port} set as an access port in VLAN {vlan_id}'.format(name=self._name, + id=self._id, + port=port_number, + vlan_id=vlan_id)) + self._mapping[port_number] = ("access", vlan_id) + + @asyncio.coroutine + def set_dot1q_port(self, port_number, native_vlan): + """ + Sets the specified port as a 802.1Q trunk port. + + :param port_number: allocated port number + :param native_vlan: native VLAN for this trunk port + """ + + if port_number not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port_number)) + + nio = self._nios[port_number] + yield from self._hypervisor.send('ethsw set_dot1q_port "{name}" {nio} {native_vlan}'.format(name=self._name, + nio=nio, + native_vlan=native_vlan)) + + log.info('Ethernet switch "{name}" [{id}]: port {port} set as a 802.1Q port with native VLAN {vlan_id}'.format(name=self._name, + id=self._id, + port=port_number, + vlan_id=native_vlan)) + + self._mapping[port_number] = ("dot1q", native_vlan) + + @asyncio.coroutine + def set_qinq_port(self, port_number, outer_vlan): + """ + Sets the specified port as a trunk (QinQ) port. + + :param port_number: allocated port number + :param outer_vlan: outer VLAN (transport VLAN) for this QinQ port + """ + + if port_number not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port_number)) + + nio = self._nios[port_number] + yield from self._hypervisor.send('ethsw set_qinq_port "{name}" {nio} {outer_vlan}'.format(name=self._name, + nio=nio, + outer_vlan=outer_vlan)) + + log.info('Ethernet switch "{name}" [{id}]: port {port} set as a QinQ port with outer VLAN {vlan_id}'.format(name=self._name, + id=self._id, + port=port_number, + vlan_id=outer_vlan)) + self._mapping[port_number] = ("qinq", outer_vlan) + + @asyncio.coroutine + def get_mac_addr_table(self): + """ + Returns the MAC address table for this Ethernet switch. + + :returns: list of entries (Ethernet address, VLAN, NIO) + """ + + mac_addr_table = yield from self._hypervisor.send('ethsw show_mac_addr_table "{}"'.format(self._name)) + return mac_addr_table + + @asyncio.coroutine + def clear_mac_addr_table(self): + """ + Clears the MAC address table for this Ethernet switch. + """ + + yield from self._hypervisor.send('ethsw clear_mac_addr_table "{}"'.format(self._name)) + + @asyncio.coroutine + def start_capture(self, port_number, output_file, data_link_type="DLT_EN10MB"): + """ + Starts a packet capture. + + :param port_number: allocated port number + :param output_file: PCAP destination file for the capture + :param data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB + """ + + if port_number not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port_number)) + + nio = self._nios[port_number] + + data_link_type = data_link_type.lower() + if data_link_type.startswith("dlt_"): + data_link_type = data_link_type[4:] + + if nio.input_filter[0] is not None and nio.output_filter[0] is not None: + raise DynamipsError("Port {} has already a filter applied".format(port_number)) + + yield from nio.bind_filter("both", "capture") + yield from nio.setup_filter("both", "{} {}".format(data_link_type, output_file)) + + log.info('Ethernet switch "{name}" [{id}]: starting packet capture on {port}'.format(name=self._name, + id=self._id, + port=port_number)) + + @asyncio.coroutine + def stop_capture(self, port_number): + """ + Stops a packet capture. + + :param port_number: allocated port number + """ + + if port_number not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port_number)) + + nio = self._nios[port_number] + yield from nio.unbind_filter("both") + log.info('Ethernet switch "{name}" [{id}]: stopping packet capture on {port}'.format(name=self._name, + id=self._id, + port=port_number)) diff --git a/gns3server/modules/dynamips/nodes/frame_relay_switch.py b/gns3server/modules/dynamips/nodes/frame_relay_switch.py new file mode 100644 index 00000000..cdbeb997 --- /dev/null +++ b/gns3server/modules/dynamips/nodes/frame_relay_switch.py @@ -0,0 +1,267 @@ +# -*- 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 . + +""" +Interface for Dynamips virtual Frame-Relay switch module. +http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L642 +""" + +import asyncio + +from .device import Device +from ..dynamips_error import DynamipsError + +import logging +log = logging.getLogger(__name__) + + +class FrameRelaySwitch(Device): + """ + Dynamips Frame Relay switch. + + :param name: name for this switch + :param node_id: Node instance identifier + :param project: Project instance + :param manager: Parent VM Manager + :param hypervisor: Dynamips hypervisor instance + """ + + def __init__(self, name, node_id, project, manager, hypervisor=None): + + super().__init__(name, node_id, project, manager, hypervisor) + self._nios = {} + self._mapping = {} + + @asyncio.coroutine + def create(self): + + if self._hypervisor is None: + self._hypervisor = yield from self.manager.start_new_hypervisor() + + yield from self._hypervisor.send('frsw create "{}"'.format(self._name)) + log.info('Frame Relay switch "{name}" [{id}] has been created'.format(name=self._name, id=self._id)) + self._hypervisor.devices.append(self) + + @asyncio.coroutine + def set_name(self, new_name): + """ + Renames this Frame Relay switch. + + :param new_name: New name for this switch + """ + + yield from self._hypervisor.send('frsw rename "{name}" "{new_name}"'.format(name=self._name, new_name=new_name)) + log.info('Frame Relay switch "{name}" [{id}]: renamed to "{new_name}"'.format(name=self._name, + id=self._id, + new_name=new_name)) + self._name = new_name + + @property + def nios(self): + """ + Returns all the NIOs member of this Frame Relay switch. + + :returns: nio list + """ + + return self._nios + + @property + def mapping(self): + """ + Returns port mapping + + :returns: mapping list + """ + + return self._mapping + + @asyncio.coroutine + def delete(self): + """ + Deletes this Frame Relay switch. + """ + + yield from self._hypervisor.send('frsw delete "{}"'.format(self._name)) + log.info('Frame Relay switch "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id)) + self._hypervisor.devices.remove(self) + self._instances.remove(self._id) + + def has_port(self, port): + """ + Checks if a port exists on this Frame Relay switch. + + :returns: boolean + """ + + if port in self._nios: + return True + return False + + def add_nio(self, nio, port_number): + """ + Adds a NIO as new port on Frame Relay switch. + + :param nio: NIO instance to add + :param port_number: port to allocate for the NIO + """ + + if port_number in self._nios: + raise DynamipsError("Port {} isn't free".format(port_number)) + + log.info('Frame Relay switch "{name}" [{id}]: NIO {nio} bound to port {port}'.format(name=self._name, + id=self._id, + nio=nio, + port=port_number)) + + self._nios[port_number] = nio + + def remove_nio(self, port_number): + """ + Removes the specified NIO as member of this Frame Relay switch. + + :param port_number: allocated port number + + :returns: the NIO that was bound to the allocated port + """ + + if port_number not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port_number)) + + nio = self._nios[port_number] + log.info('Frame Relay switch "{name}" [{id}]: NIO {nio} removed from port {port}'.format(name=self._name, + id=self._id, + nio=nio, + port=port_number)) + + del self._nios[port_number] + return nio + + @asyncio.coroutine + def map_vc(self, port1, dlci1, port2, dlci2): + """ + Creates a new Virtual Circuit connection (unidirectional). + + :param port1: input port + :param dlci1: input DLCI + :param port2: output port + :param dlci2: output DLCI + """ + + if port1 not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port1)) + + if port2 not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port2)) + + nio1 = self._nios[port1] + nio2 = self._nios[port2] + + yield from self._hypervisor.send('frsw create_vc "{name}" {input_nio} {input_dlci} {output_nio} {output_dlci}'.format(name=self._name, + input_nio=nio1, + input_dlci=dlci1, + output_nio=nio2, + output_dlci=dlci2)) + + log.info('Frame Relay switch "{name}" [{id}]: VC from port {port1} DLCI {dlci1} to port {port2} DLCI {dlci2} created'.format(name=self._name, + id=self._id, + port1=port1, + dlci1=dlci1, + port2=port2, + dlci2=dlci2)) + + self._mapping[(port1, dlci1)] = (port2, dlci2) + + @asyncio.coroutine + def unmap_vc(self, port1, dlci1, port2, dlci2): + """ + Deletes a Virtual Circuit connection (unidirectional). + + :param port1: input port + :param dlci1: input DLCI + :param port2: output port + :param dlci2: output DLCI + """ + + if port1 not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port1)) + + if port2 not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port2)) + + nio1 = self._nios[port1] + nio2 = self._nios[port2] + + yield from self._hypervisor.send('frsw delete_vc "{name}" {input_nio} {input_dlci} {output_nio} {output_dlci}'.format(name=self._name, + input_nio=nio1, + input_dlci=dlci1, + output_nio=nio2, + output_dlci=dlci2)) + + log.info('Frame Relay switch "{name}" [{id}]: VC from port {port1} DLCI {dlci1} to port {port2} DLCI {dlci2} deleted'.format(name=self._name, + id=self._id, + port1=port1, + dlci1=dlci1, + port2=port2, + dlci2=dlci2)) + del self._mapping[(port1, dlci1)] + + @asyncio.coroutine + def start_capture(self, port_number, output_file, data_link_type="DLT_FRELAY"): + """ + Starts a packet capture. + + :param port_number: allocated port number + :param output_file: PCAP destination file for the capture + :param data_link_type: PCAP data link type (DLT_*), default is DLT_FRELAY + """ + + if port_number not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port_number)) + + nio = self._nios[port_number] + + data_link_type = data_link_type.lower() + if data_link_type.startswith("dlt_"): + data_link_type = data_link_type[4:] + + if nio.input_filter[0] is not None and nio.output_filter[0] is not None: + raise DynamipsError("Port {} has already a filter applied".format(port_number)) + + yield from nio.bind_filter("both", "capture") + yield from nio.setup_filter("both", "{} {}".format(data_link_type, output_file)) + + log.info('Frame relay switch "{name}" [{id}]: starting packet capture on {port}'.format(name=self._name, + id=self._id, + port=port_number)) + + @asyncio.coroutine + def stop_capture(self, port_number): + """ + Stops a packet capture. + + :param port_number: allocated port number + """ + + if port_number not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port_number)) + + nio = self._nios[port_number] + yield from nio.unbind_filter("both") + log.info('Frame relay switch "{name}" [{id}]: stopping packet capture on {port}'.format(name=self._name, + id=self._id, + port=port_number)) diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index 055cab13..34e28ac5 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -1310,12 +1310,6 @@ class Router(BaseVM): raise DynamipsError("Port {port_number} has already a filter applied on {adapter}".format(adapter=adapter, port_number=port_number)) - # FIXME: capture - # try: - # os.makedirs(os.path.dirname(output_file), exist_ok=True) - # except OSError as e: - # raise DynamipsError("Could not create captures directory {}".format(e)) - yield from nio.bind_filter("both", "capture") yield from nio.setup_filter("both", '{} "{}"'.format(data_link_type, output_file)) @@ -1411,6 +1405,7 @@ class Router(BaseVM): self._private_config = private_config + # TODO: rename # def rename(self, new_name): # """ # Renames this router. diff --git a/gns3server/schemas/dynamips.py b/gns3server/schemas/dynamips.py index c30d2920..2c4f755b 100644 --- a/gns3server/schemas/dynamips.py +++ b/gns3server/schemas/dynamips.py @@ -880,3 +880,281 @@ VM_OBJECT_SCHEMA = { "additionalProperties": False, "required": ["name", "vm_id", "project_id", "dynamips_id"] } + +DEVICE_CREATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to create a new Dynamips device instance", + "type": "object", + "properties": { + "name": { + "description": "Dynamips device name", + "type": "string", + "minLength": 1, + }, + "vm_id": { + "description": "Dynamips device instance identifier", + "type": "string", + "minLength": 36, + "maxLength": 36, + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + }, + }, + "additionalProperties": False, + "required": ["name"] +} + +ETHHUB_UPDATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to update an Ethernet hub instance", + "type": "object", + "properties": { + "id": { + "description": "Ethernet hub instance ID", + "type": "integer" + }, + "name": { + "description": "Ethernet hub name", + "type": "string", + "minLength": 1, + }, + }, + "additionalProperties": False, + "required": ["id"] +} + +ETHHUB_NIO_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to add a NIO for an Ethernet hub 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": "Ethernet hub instance ID", + "type": "integer" + }, + "port_id": { + "description": "Unique port identifier for the Ethernet hub instance", + "type": "integer" + }, + "port": { + "description": "Port number", + "type": "integer", + "minimum": 1, + }, + "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"] +} + +ETHHUB_DELETE_NIO_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to delete a NIO for an Ethernet hub instance", + "type": "object", + "properties": { + "id": { + "description": "Ethernet hub instance ID", + "type": "integer" + }, + "port": { + "description": "Port number", + "type": "integer", + "minimum": 1, + }, + }, + "additionalProperties": False, + "required": ["id", "port"] +} + +ETHHUB_START_CAPTURE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to start a packet capture on an Ethernet hub instance port", + "type": "object", + "properties": { + "id": { + "description": "Ethernet hub instance ID", + "type": "integer" + }, + "port_id": { + "description": "Unique port identifier for the Ethernet hub instance", + "type": "integer" + }, + "port": { + "description": "Port number", + "type": "integer", + "minimum": 1, + }, + "capture_file_name": { + "description": "Capture file name", + "type": "string", + "minLength": 1, + }, + "data_link_type": { + "description": "PCAP data link type", + "type": "string", + "minLength": 1, + }, + }, + "additionalProperties": False, + "required": ["id", "port_id", "port", "capture_file_name"] +} + +ETHHUB_STOP_CAPTURE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to stop a packet capture on an Ethernet hub instance port", + "type": "object", + "properties": { + "id": { + "description": "Ethernet hub instance ID", + "type": "integer" + }, + "port_id": { + "description": "Unique port identifier for the Ethernet hub instance", + "type": "integer" + }, + "port": { + "description": "Port number", + "type": "integer", + "minimum": 1, + }, + }, + "additionalProperties": False, + "required": ["id", "port_id", "port"] +} diff --git a/tests/api/test_virtualbox.py b/tests/api/test_virtualbox.py index dcd2e60f..c195b459 100644 --- a/tests/api/test_virtualbox.py +++ b/tests/api/test_virtualbox.py @@ -105,7 +105,7 @@ def test_vbox_nio_create_udp(server, vm): assert args[0] == 0 assert response.status == 201 - assert response.route == "/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/ports/{port_id:\d+}/nio" + assert response.route == "/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" assert response.json["type"] == "nio_udp" @@ -119,7 +119,7 @@ def test_vbox_delete_nio(server, vm): assert args[0] == 0 assert response.status == 204 - assert response.route == "/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/ports/{port_id:\d+}/nio" + assert response.route == "/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" def test_vbox_update(server, vm, free_console_port): From f99e834c376c7a4eabf895a2a55d7e9111d5aa13 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 15 Feb 2015 12:18:12 -0700 Subject: [PATCH 248/485] Dynamips devices support (packet capture to complete). --- gns3server/handlers/__init__.py | 3 +- .../handlers/dynamips_device_handler.py | 234 ++++++++++++ ...mips_handler.py => dynamips_vm_handler.py} | 15 +- gns3server/modules/adapters/serial_adapter.py | 2 +- gns3server/modules/base_manager.py | 3 +- gns3server/modules/dynamips/__init__.py | 84 ++++- .../modules/dynamips/dynamips_device.py | 14 +- .../modules/dynamips/dynamips_hypervisor.py | 171 +-------- .../modules/dynamips/nodes/atm_switch.py | 72 +++- gns3server/modules/dynamips/nodes/bridge.py | 2 +- gns3server/modules/dynamips/nodes/device.py | 75 +++- .../modules/dynamips/nodes/ethernet_hub.py | 52 +-- .../modules/dynamips/nodes/ethernet_switch.py | 67 +++- .../dynamips/nodes/frame_relay_switch.py | 53 ++- gns3server/modules/dynamips/nodes/router.py | 8 +- gns3server/modules/project.py | 36 +- gns3server/schemas/dynamips_device.py | 341 ++++++++++++++++++ .../schemas/{dynamips.py => dynamips_vm.py} | 278 -------------- 18 files changed, 971 insertions(+), 539 deletions(-) create mode 100644 gns3server/handlers/dynamips_device_handler.py rename gns3server/handlers/{dynamips_handler.py => dynamips_vm_handler.py} (97%) create mode 100644 gns3server/schemas/dynamips_device.py rename gns3server/schemas/{dynamips.py => dynamips_vm.py} (74%) diff --git a/gns3server/handlers/__init__.py b/gns3server/handlers/__init__.py index 78bb3ca6..03e6dcab 100644 --- a/gns3server/handlers/__init__.py +++ b/gns3server/handlers/__init__.py @@ -3,5 +3,6 @@ __all__ = ["version_handler", "vpcs_handler", "project_handler", "virtualbox_handler", - "dynamips_handler", + "dynamips_vm_handler", + "dynamips_device_handler", "iou_handler"] diff --git a/gns3server/handlers/dynamips_device_handler.py b/gns3server/handlers/dynamips_device_handler.py new file mode 100644 index 00000000..fb5f284e --- /dev/null +++ b/gns3server/handlers/dynamips_device_handler.py @@ -0,0 +1,234 @@ +# -*- 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 asyncio +from ..web.route import Route +from ..schemas.dynamips_device import DEVICE_CREATE_SCHEMA +from ..schemas.dynamips_device import DEVICE_UPDATE_SCHEMA +from ..schemas.dynamips_device import DEVICE_CAPTURE_SCHEMA +from ..schemas.dynamips_device import DEVICE_OBJECT_SCHEMA +from ..schemas.dynamips_device import DEVICE_NIO_SCHEMA +from ..modules.dynamips import Dynamips + + +class DynamipsDeviceHandler: + + """ + API entry points for Dynamips devices. + """ + + @classmethod + @Route.post( + r"/projects/{project_id}/dynamips/devices", + parameters={ + "project_id": "UUID for the project" + }, + status_codes={ + 201: "Instance created", + 400: "Invalid request", + 409: "Conflict" + }, + description="Create a new Dynamips device instance", + input=DEVICE_CREATE_SCHEMA, + output=DEVICE_OBJECT_SCHEMA) + def create(request, response): + + dynamips_manager = Dynamips.instance() + device = yield from dynamips_manager.create_device(request.json.pop("name"), + request.match_info["project_id"], + request.json.get("device_id"), + request.json.get("device_type")) + + response.set_status(201) + response.json(device) + + @classmethod + @Route.get( + r"/projects/{project_id}/dynamips/devices/{device_id}", + parameters={ + "project_id": "UUID for the project", + "device_id": "UUID for the instance" + }, + status_codes={ + 200: "Success", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Get a Dynamips device instance", + output=DEVICE_OBJECT_SCHEMA) + def show(request, response): + + dynamips_manager = Dynamips.instance() + device = dynamips_manager.get_device(request.match_info["device_id"], project_id=request.match_info["project_id"]) + response.json(device) + + @classmethod + @Route.put( + r"/projects/{project_id}/dynamips/devices/{device_id}", + parameters={ + "project_id": "UUID for the project", + "device_id": "UUID for the instance" + }, + status_codes={ + 200: "Instance updated", + 400: "Invalid request", + 404: "Instance doesn't exist", + 409: "Conflict" + }, + description="Update a Dynamips device instance", + input=DEVICE_UPDATE_SCHEMA, + output=DEVICE_OBJECT_SCHEMA) + def update(request, response): + + dynamips_manager = Dynamips.instance() + device = dynamips_manager.get_device(request.match_info["device_id"], project_id=request.match_info["project_id"]) + + if "name" in request.json: + yield from device.set_name(request.json["name"]) + + if "ports" in request.json: + for port in request.json["ports"]: + yield from device.set_port_settings(port["port"], port) + + response.json(device) + + @classmethod + @Route.delete( + r"/projects/{project_id}/dynamips/devices/{device_id}", + parameters={ + "project_id": "UUID for the project", + "device_id": "UUID for the instance" + }, + status_codes={ + 204: "Instance deleted", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Delete a Dynamips device instance") + def delete(request, response): + + dynamips_manager = Dynamips.instance() + yield from dynamips_manager.delete_device(request.match_info["device_id"]) + response.set_status(204) + + @Route.post( + r"/projects/{project_id}/dynamips/devices/{device_id}/ports/{port_number:\d+}/nio", + parameters={ + "project_id": "UUID for the project", + "device_id": "UUID for the instance", + "port_number": "Port on the device" + }, + status_codes={ + 201: "NIO created", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Add a NIO to a Dynamips device instance", + input=DEVICE_NIO_SCHEMA) + def create_nio(request, response): + + dynamips_manager = Dynamips.instance() + device = dynamips_manager.get_device(request.match_info["device_id"], project_id=request.match_info["project_id"]) + nio = yield from dynamips_manager.create_nio(device, request.json["nio"]) + port_number = int(request.match_info["port_number"]) + port_settings = request.json.get("port_settings") + mappings = request.json.get("mappings") + + if asyncio.iscoroutinefunction(device.add_nio): + yield from device.add_nio(nio, port_number) + else: + device.add_nio(nio, port_number) + + if port_settings: + yield from device.set_port_settings(port_number, port_settings) + elif mappings: + yield from device.set_mappings(mappings) + + response.set_status(201) + response.json(nio) + + @classmethod + @Route.delete( + r"/projects/{project_id}/dynamips/devices/{device_id}/ports/{port_number:\d+}/nio", + parameters={ + "project_id": "UUID for the project", + "device_id": "UUID for the instance", + "port_number": "Port on the device" + }, + status_codes={ + 204: "NIO deleted", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Remove a NIO from a Dynamips device instance") + def delete_nio(request, response): + + dynamips_manager = Dynamips.instance() + device = dynamips_manager.get_device(request.match_info["device_id"], project_id=request.match_info["project_id"]) + port_number = int(request.match_info["port_number"]) + yield from device.remove_nio(port_number) + response.set_status(204) + + # @Route.post( + # r"/projects/{project_id}/dynamips/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/start_capture", + # parameters={ + # "project_id": "UUID for the project", + # "vm_id": "UUID for the instance", + # "adapter_number": "Adapter to start a packet capture", + # "port_number": "Port on the adapter" + # }, + # status_codes={ + # 200: "Capture started", + # 400: "Invalid request", + # 404: "Instance doesn't exist" + # }, + # description="Start a packet capture on a Dynamips VM instance", + # input=VM_CAPTURE_SCHEMA) + # def start_capture(request, response): + # + # dynamips_manager = Dynamips.instance() + # vm = dynamips_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + # slot_number = int(request.match_info["adapter_number"]) + # port_number = int(request.match_info["port_number"]) + # pcap_file_path = os.path.join(vm.project.capture_working_directory(), request.json["capture_file_name"]) + # yield from vm.start_capture(slot_number, port_number, pcap_file_path, request.json["data_link_type"]) + # response.json({"pcap_file_path": pcap_file_path}) + # + # @Route.post( + # r"/projects/{project_id}/dynamips/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/stop_capture", + # parameters={ + # "project_id": "UUID for the project", + # "vm_id": "UUID for the instance", + # "adapter_number": "Adapter to stop a packet capture", + # "port_number": "Port on the adapter (always 0)" + # }, + # status_codes={ + # 204: "Capture stopped", + # 400: "Invalid request", + # 404: "Instance doesn't exist" + # }, + # description="Stop a packet capture on a Dynamips VM instance") + # def start_capture(request, response): + # + # dynamips_manager = Dynamips.instance() + # vm = dynamips_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + # slot_number = int(request.match_info["adapter_number"]) + # port_number = int(request.match_info["port_number"]) + # yield from vm.stop_capture(slot_number, port_number) + # response.set_status(204) + diff --git a/gns3server/handlers/dynamips_handler.py b/gns3server/handlers/dynamips_vm_handler.py similarity index 97% rename from gns3server/handlers/dynamips_handler.py rename to gns3server/handlers/dynamips_vm_handler.py index 3ff92b43..5981db43 100644 --- a/gns3server/handlers/dynamips_handler.py +++ b/gns3server/handlers/dynamips_vm_handler.py @@ -19,19 +19,19 @@ import os import asyncio from ..web.route import Route -from ..schemas.dynamips import VM_CREATE_SCHEMA -from ..schemas.dynamips import VM_UPDATE_SCHEMA -from ..schemas.dynamips import VM_NIO_SCHEMA -from ..schemas.dynamips import VM_CAPTURE_SCHEMA -from ..schemas.dynamips import VM_OBJECT_SCHEMA +from ..schemas.dynamips_vm import VM_CREATE_SCHEMA +from ..schemas.dynamips_vm import VM_UPDATE_SCHEMA +from ..schemas.dynamips_vm import VM_CAPTURE_SCHEMA +from ..schemas.dynamips_vm import VM_OBJECT_SCHEMA +from ..schemas.dynamips_vm import VM_NIO_SCHEMA from ..modules.dynamips import Dynamips from ..modules.project_manager import ProjectManager -class DynamipsHandler: +class DynamipsVMHandler: """ - API entry points for Dynamips. + API entry points for Dynamips VMs. """ @classmethod @@ -340,3 +340,4 @@ class DynamipsHandler: port_number = int(request.match_info["port_number"]) yield from vm.stop_capture(slot_number, port_number) response.set_status(204) + diff --git a/gns3server/modules/adapters/serial_adapter.py b/gns3server/modules/adapters/serial_adapter.py index 1ac39ce1..6a674c21 100644 --- a/gns3server/modules/adapters/serial_adapter.py +++ b/gns3server/modules/adapters/serial_adapter.py @@ -21,7 +21,7 @@ from .adapter import Adapter class SerialAdapter(Adapter): """ - Ethernet adapter. + Serial adapter. """ def __init__(self, interfaces=1): diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index a93cf1fd..fa633f8d 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -140,12 +140,11 @@ class BaseManager: raise aiohttp.web.HTTPNotFound(text="VM ID {} doesn't exist".format(vm_id)) vm = self._vms[vm_id] - if project_id: if vm.project.id != project.id: raise aiohttp.web.HTTPNotFound(text="Project ID {} doesn't belong to VM {}".format(project_id, vm.name)) - return self._vms[vm_id] + return vm @asyncio.coroutine def create_vm(self, name, project_id, vm_id, *args, **kwargs): diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index 07af76e4..55fc0597 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -32,11 +32,14 @@ log = logging.getLogger(__name__) from gns3server.utils.interfaces import get_windows_interfaces from pkg_resources import parse_version +from uuid import UUID, uuid4 from ..base_manager import BaseManager +from ..project_manager import ProjectManager from .dynamips_error import DynamipsError from .hypervisor import Hypervisor from .nodes.router import Router from .dynamips_vm import DynamipsVM +from .dynamips_device import DynamipsDevice # NIOs from .nios.nio_udp import NIOUDP @@ -54,10 +57,12 @@ from .nios.nio_null import NIONull class Dynamips(BaseManager): _VM_CLASS = DynamipsVM + _DEVICE_CLASS = DynamipsDevice def __init__(self): super().__init__() + self._devices = {} self._dynamips_path = None # FIXME: temporary @@ -67,7 +72,19 @@ class Dynamips(BaseManager): def unload(self): yield from BaseManager.unload(self) - Router.reset() + + tasks = [] + for device in self._devices.values(): + tasks.append(asyncio.async(device.hypervisor.stop())) + + if tasks: + done, _ = yield from asyncio.wait(tasks) + for future in done: + try: + future.result() + except Exception as e: + log.error("Could not stop device hypervisor {}".format(e), exc_info=1) + continue # files = glob.glob(os.path.join(self._working_dir, "dynamips", "*.ghost")) # files += glob.glob(os.path.join(self._working_dir, "dynamips", "*_lock")) @@ -92,6 +109,71 @@ class Dynamips(BaseManager): return self._dynamips_path + @asyncio.coroutine + def create_device(self, name, project_id, device_id, device_type, *args, **kwargs): + """ + Create a new Dynamips device. + + :param name: Device name + :param project_id: Project identifier + :param vm_id: restore a VM identifier + """ + + project = ProjectManager.instance().get_project(project_id) + if not device_id: + device_id = str(uuid4()) + + device = self._DEVICE_CLASS(name, device_id, project, self, device_type, *args, **kwargs) + yield from device.create() + self._devices[device.id] = device + project.add_device(device) + return device + + def get_device(self, device_id, project_id=None): + """ + Returns a device instance. + + :param device_id: Device identifier + :param project_id: Project identifier + + :returns: Device instance + """ + + if project_id: + # check the project_id exists + project = ProjectManager.instance().get_project(project_id) + + try: + UUID(device_id, version=4) + except ValueError: + raise aiohttp.web.HTTPBadRequest(text="Device ID} is not a valid UUID".format(device_id)) + + if device_id not in self._devices: + raise aiohttp.web.HTTPNotFound(text="Device ID {} doesn't exist".format(device_id)) + + device = self._devices[device_id] + if project_id: + if device.project.id != project.id: + raise aiohttp.web.HTTPNotFound(text="Project ID {} doesn't belong to device {}".format(project_id, device.name)) + + return device + + @asyncio.coroutine + def delete_device(self, device_id): + """ + Delete a device + + :param device_id: Device identifier + + :returns: Device instance + """ + + device = self.get_device(device_id) + yield from device.delete() + device.project.remove_device(device) + del self._devices[device.id] + return device + def find_dynamips(self): # look for Dynamips diff --git a/gns3server/modules/dynamips/dynamips_device.py b/gns3server/modules/dynamips/dynamips_device.py index bf5012d1..e8398c94 100644 --- a/gns3server/modules/dynamips/dynamips_device.py +++ b/gns3server/modules/dynamips/dynamips_device.py @@ -25,10 +25,10 @@ from .nodes.frame_relay_switch import FrameRelaySwitch import logging log = logging.getLogger(__name__) -DEVICES = {'atmsw': ATMSwitch, - 'frsw': FrameRelaySwitch, - 'ethsw': EthernetSwitch, - 'ethhub': EthernetHub} +DEVICES = {'atm_switch': ATMSwitch, + 'frame_relay_switch': FrameRelaySwitch, + 'ethernet_switch': EthernetSwitch, + 'ethernet_hub': EthernetHub} class DynamipsDevice: @@ -37,9 +37,9 @@ class DynamipsDevice: Factory to create an Device object based on the type """ - def __new__(cls, name, vm_id, project, manager, device_type, **kwargs): + def __new__(cls, name, device_id, project, manager, device_type, **kwargs): - if type not in DEVICES: + if device_type not in DEVICES: raise DynamipsError("Unknown device type: {}".format(device_type)) - return DEVICES[device_type](name, vm_id, project, manager, **kwargs) + return DEVICES[device_type](name, device_id, project, manager, **kwargs) diff --git a/gns3server/modules/dynamips/dynamips_hypervisor.py b/gns3server/modules/dynamips/dynamips_hypervisor.py index 071019f5..e4db5623 100644 --- a/gns3server/modules/dynamips/dynamips_hypervisor.py +++ b/gns3server/modules/dynamips/dynamips_hypervisor.py @@ -128,9 +128,16 @@ class DynamipsHypervisor: Stops this hypervisor (will no longer run). """ - yield from self.send("hypervisor stop") - yield from self._writer.drain() - self._writer.close() + try: + # try to properly stop the hypervisor + yield from self.send("hypervisor stop") + except DynamipsError: + pass + try: + yield from self._writer.drain() + self._writer.close() + except OSError as e: + log.debug("Stopping hypervisor {}:{} {}".format(self._host, self._port, e)) self._reader = self._writer = None self._nio_udp_auto_instances.clear() @@ -186,137 +193,6 @@ class DynamipsHypervisor: return self._devices - # @devices.setter - # def devices(self, devices): - # """ - # Sets the list of devices managed by this hypervisor instance. - # This method is for internal use. - # - # :param devices: a list of device objects - # """ - # - # self._devices = devices - - # @property - # def console_start_port_range(self): - # """ - # Returns the console start port range value - # - # :returns: console start port range value (integer) - # """ - # - # return self._console_start_port_range - - # @console_start_port_range.setter - # def console_start_port_range(self, console_start_port_range): - # """ - # Set a new console start port range value - # - # :param console_start_port_range: console start port range value (integer) - # """ - # - # self._console_start_port_range = console_start_port_range - # - # @property - # def console_end_port_range(self): - # """ - # Returns the console end port range value - # - # :returns: console end port range value (integer) - # """ - # - # return self._console_end_port_range - # - # @console_end_port_range.setter - # def console_end_port_range(self, console_end_port_range): - # """ - # Set a new console end port range value - # - # :param console_end_port_range: console end port range value (integer) - # """ - # - # self._console_end_port_range = console_end_port_range - - # @property - # def aux_start_port_range(self): - # """ - # Returns the auxiliary console start port range value - # - # :returns: auxiliary console start port range value (integer) - # """ - # - # return self._aux_start_port_range - # - # @aux_start_port_range.setter - # def aux_start_port_range(self, aux_start_port_range): - # """ - # Sets a new auxiliary console start port range value - # - # :param aux_start_port_range: auxiliary console start port range value (integer) - # """ - # - # self._aux_start_port_range = aux_start_port_range - # - # @property - # def aux_end_port_range(self): - # """ - # Returns the auxiliary console end port range value - # - # :returns: auxiliary console end port range value (integer) - # """ - # - # return self._aux_end_port_range - # - # @aux_end_port_range.setter - # def aux_end_port_range(self, aux_end_port_range): - # """ - # Sets a new auxiliary console end port range value - # - # :param aux_end_port_range: auxiliary console end port range value (integer) - # """ - # - # self._aux_end_port_range = aux_end_port_range - - # @property - # def udp_start_port_range(self): - # """ - # Returns the UDP start port range value - # - # :returns: UDP start port range value (integer) - # """ - # - # return self._udp_start_port_range - # - # @udp_start_port_range.setter - # def udp_start_port_range(self, udp_start_port_range): - # """ - # Sets a new UDP start port range value - # - # :param udp_start_port_range: UDP start port range value (integer) - # """ - # - # self._udp_start_port_range = udp_start_port_range - # - # @property - # def udp_end_port_range(self): - # """ - # Returns the UDP end port range value - # - # :returns: UDP end port range value (integer) - # """ - # - # return self._udp_end_port_range - # - # @udp_end_port_range.setter - # def udp_end_port_range(self, udp_end_port_range): - # """ - # Sets an new UDP end port range value - # - # :param udp_end_port_range: UDP end port range value (integer) - # """ - # - # self._udp_end_port_range = udp_end_port_range - @property def ghosts(self): """ @@ -337,26 +213,6 @@ class DynamipsHypervisor: self._ghosts[image_name] = router - @property - def jitsharing_groups(self): - """ - Returns a list of the JIT sharing groups hosted by this hypervisor. - - :returns: JIT sharing groups dict (image_name -> group number) - """ - - return self._jitsharing_groups - - def add_jitsharing_group(self, image_name, group_number): - """ - Adds a JIT blocks sharing group name to the list of groups created on this hypervisor. - - :param image_name: name of the ghost image - :param group_number: group (integer) - """ - - self._jitsharing_groups[image_name] = group_number - @property def port(self): """ @@ -462,6 +318,9 @@ class DynamipsHypervisor: while True: try: chunk = yield from self._reader.read(1024) # match to Dynamips' buffer size + if not chunk: + raise DynamipsError("No data returned from {host}:{port}, Dynamips process running: {run}" + .format(host=self._host, port=self._port, run=self.is_running())) buf += chunk.decode() except OSError as e: raise DynamipsError("Communication timed out with {host}:{port} :{error}, Dynamips process running: {run}" @@ -480,10 +339,6 @@ class DynamipsHypervisor: data.pop() buf = '' - if len(data) == 0: - raise DynamipsError("no data returned from {host}:{port}, Dynamips process running: {run}" - .format(host=self._host, port=self._port, run=self.is_running())) - # Does it contain an error code? if self.error_re.search(data[-1]): raise DynamipsError(data[-1][4:]) diff --git a/gns3server/modules/dynamips/nodes/atm_switch.py b/gns3server/modules/dynamips/nodes/atm_switch.py index b2186484..07094479 100644 --- a/gns3server/modules/dynamips/nodes/atm_switch.py +++ b/gns3server/modules/dynamips/nodes/atm_switch.py @@ -21,6 +21,7 @@ http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L593 """ import asyncio +import re from .device import Device from ..dynamips_error import DynamipsError @@ -34,17 +35,24 @@ class ATMSwitch(Device): Dynamips ATM switch. :param name: name for this switch - :param node_id: Node instance identifier + :param device_id: Device instance identifier :param project: Project instance :param manager: Parent VM Manager :param hypervisor: Dynamips hypervisor instance """ - def __init__(self, name, node_id, project, manager, hypervisor=None): + def __init__(self, name, device_id, project, manager, hypervisor=None): - super().__init__(name, node_id, project, manager) + super().__init__(name, device_id, project, manager, hypervisor) self._nios = {} - self._mapping = {} + self._mappings = {} + + def __json__(self): + + return {"name": self.name, + "device_id": self.id, + "project_id": self.project.id, + "mappings": self._mappings} @asyncio.coroutine def create(self): @@ -82,14 +90,14 @@ class ATMSwitch(Device): return self._nios @property - def mapping(self): + def mappings(self): """ - Returns port mapping + Returns port mappings - :returns: mapping list + :returns: mappings list """ - return self._mapping + return self._mappings @asyncio.coroutine def delete(self): @@ -97,10 +105,14 @@ class ATMSwitch(Device): Deletes this ATM switch. """ - yield from self._hypervisor.send('atmsw delete "{}"'.format(self._name)) - log.info('ATM switch "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id)) + try: + yield from self._hypervisor.send('atmsw delete "{}"'.format(self._name)) + log.info('ATM switch "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id)) + except DynamipsError: + log.debug("Could not properly delete ATM switch {}".format(self._name)) self._hypervisor.devices.remove(self) - self._instances.remove(self._id) + if self._hypervisor and not self._hypervisor.devices: + yield from self.hypervisor.stop() def has_port(self, port): """ @@ -150,6 +162,36 @@ class ATMSwitch(Device): del self._nios[port_number] return nio + @asyncio.coroutine + def set_mappings(self, mappings): + """ + Applies VC mappings + + :param mappings: mappings (dict) + """ + + pvc_entry = re.compile(r"""^([0-9]*):([0-9]*):([0-9]*)$""") + for source, destination in mappings.items(): + match_source_pvc = pvc_entry.search(source) + match_destination_pvc = pvc_entry.search(destination) + if match_source_pvc and match_destination_pvc: + # add the virtual channels + source_port, source_vpi, source_vci = map(int, match_source_pvc.group(1, 2, 3)) + destination_port, destination_vpi, destination_vci = map(int, match_destination_pvc.group(1, 2, 3)) + if self.has_port(destination_port): + if (source_port, source_vpi, source_vci) not in self.mapping and \ + (destination_port, destination_vpi, destination_vci) not in self.mappings: + yield from self.map_pvc(source_port, source_vpi, source_vci, destination_port, destination_vpi, destination_vci) + yield from self.map_pvc(destination_port, destination_vpi, destination_vci, source_port, source_vpi, source_vci) + else: + # add the virtual paths + source_port, source_vpi = map(int, source.split(':')) + destination_port, destination_vpi = map(int, destination.split(':')) + if self.has_port(destination_port): + if (source_port, source_vpi) not in self.mappings and (destination_port, destination_vpi) not in self.mappings: + yield from self.map_vp(source_port, source_vpi, destination_port, destination_vpi) + yield from self.map_vp(destination_port, destination_vpi, source_port, source_vpi) + @asyncio.coroutine def map_vp(self, port1, vpi1, port2, vpi2): """ @@ -183,7 +225,7 @@ class ATMSwitch(Device): port2=port2, vpi2=vpi2)) - self._mapping[(port1, vpi1)] = (port2, vpi2) + self._mappings[(port1, vpi1)] = (port2, vpi2) @asyncio.coroutine def unmap_vp(self, port1, vpi1, port2, vpi2): @@ -218,7 +260,7 @@ class ATMSwitch(Device): port2=port2, vpi2=vpi2)) - del self._mapping[(port1, vpi1)] + del self._mappings[(port1, vpi1)] @asyncio.coroutine def map_pvc(self, port1, vpi1, vci1, port2, vpi2, vci2): @@ -259,7 +301,7 @@ class ATMSwitch(Device): vpi2=vpi2, vci2=vci2)) - self._mapping[(port1, vpi1, vci1)] = (port2, vpi2, vci2) + self._mappings[(port1, vpi1, vci1)] = (port2, vpi2, vci2) @asyncio.coroutine def unmap_pvc(self, port1, vpi1, vci1, port2, vpi2, vci2): @@ -299,7 +341,7 @@ class ATMSwitch(Device): port2=port2, vpi2=vpi2, vci2=vci2)) - del self._mapping[(port1, vpi1, vci1)] + del self._mappings[(port1, vpi1, vci1)] @asyncio.coroutine def start_capture(self, port_number, output_file, data_link_type="DLT_ATM_RFC1483"): diff --git a/gns3server/modules/dynamips/nodes/bridge.py b/gns3server/modules/dynamips/nodes/bridge.py index 0f00137c..f5c410d4 100644 --- a/gns3server/modules/dynamips/nodes/bridge.py +++ b/gns3server/modules/dynamips/nodes/bridge.py @@ -78,8 +78,8 @@ class Bridge(Device): Deletes this bridge. """ - yield from self._hypervisor.send('nio_bridge delete "{}"'.format(self._name)) self._hypervisor.devices.remove(self) + yield from self._hypervisor.send('nio_bridge delete "{}"'.format(self._name)) @asyncio.coroutine def add_nio(self, nio): diff --git a/gns3server/modules/dynamips/nodes/device.py b/gns3server/modules/dynamips/nodes/device.py index f5b7ee75..cbf755bd 100644 --- a/gns3server/modules/dynamips/nodes/device.py +++ b/gns3server/modules/dynamips/nodes/device.py @@ -16,23 +16,23 @@ # along with this program. If not, see . -from ...base_vm import BaseVM - - -class Device(BaseVM): +class Device: """ Base device for switches and hubs - :param name: name for this bridge - :param vm_id: Node instance identifier + :param name: name for this device + :param device_id: Device instance identifier :param project: Project instance - :param manager: Parent VM Manager + :param manager: Parent manager :param hypervisor: Dynamips hypervisor instance """ - def __init__(self, name, node_id, project, manager, hypervisor=None): + def __init__(self, name, device_id, project, manager, hypervisor=None): - super().__init__(name, node_id, project, manager) + self._name = name + self._id = device_id + self._project = project + self._manager = manager self._hypervisor = hypervisor @property @@ -45,10 +45,59 @@ class Device(BaseVM): return self._hypervisor - def start(self): + @property + def project(self): + """ + Returns the device current project. + + :returns: Project instance. + """ + + return self._project + + @property + def name(self): + """ + Returns the name for this device. + + :returns: name + """ - pass # Dynamips switches and hubs are always on + return self._name + + @name.setter + def name(self, new_name): + """ + Sets the name of this VM. + + :param new_name: name + """ - def stop(self): + self._name = new_name + + @property + def id(self): + """ + Returns the ID for this device. + + :returns: device identifier (string) + """ + + return self._id + + @property + def manager(self): + """ + Returns the manager for this device. + + :returns: instance of manager + """ + + return self._manager + + def create(self): + """ + Creates the device. + """ - pass # Dynamips switches and hubs are always on + raise NotImplementedError diff --git a/gns3server/modules/dynamips/nodes/ethernet_hub.py b/gns3server/modules/dynamips/nodes/ethernet_hub.py index 5f7228c7..806b8d3f 100644 --- a/gns3server/modules/dynamips/nodes/ethernet_hub.py +++ b/gns3server/modules/dynamips/nodes/ethernet_hub.py @@ -33,32 +33,38 @@ class EthernetHub(Bridge): Dynamips Ethernet hub (based on Bridge) :param name: name for this hub - :param node_id: Node instance identifier + :param device_id: Device instance identifier :param project: Project instance :param manager: Parent VM Manager :param hypervisor: Dynamips hypervisor instance """ - def __init__(self, name, node_id, project, manager, hypervisor=None): + def __init__(self, name, device_id, project, manager, hypervisor=None): - Bridge.__init__(self, name, node_id, project, manager, hypervisor) - self._mapping = {} + Bridge.__init__(self, name, device_id, project, manager, hypervisor) + self._mappings = {} + + def __json__(self): + + return {"name": self.name, + "device_id": self.id, + "project_id": self.project.id} @asyncio.coroutine def create(self): - yield from Bridge.create() + yield from Bridge.create(self) log.info('Ethernet hub "{name}" [{id}] has been created'.format(name=self._name, id=self._id)) @property - def mapping(self): + def mappings(self): """ - Returns port mapping + Returns port mappings - :returns: mapping list + :returns: mappings list """ - return self._mapping + return self._mappings @asyncio.coroutine def delete(self): @@ -66,9 +72,13 @@ class EthernetHub(Bridge): Deletes this hub. """ - yield from Bridge.delete(self) - log.info('Ethernet hub "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id)) - self._instances.remove(self._id) + try: + yield from Bridge.delete(self) + log.info('Ethernet hub "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id)) + except DynamipsError: + log.debug("Could not properly delete Ethernet hub {}".format(self._name)) + if self._hypervisor and not self._hypervisor.devices: + yield from self.hypervisor.stop() @asyncio.coroutine def add_nio(self, nio, port_number): @@ -79,7 +89,7 @@ class EthernetHub(Bridge): :param port_number: port to allocate for the NIO """ - if port_number in self._mapping: + if port_number in self._mappings: raise DynamipsError("Port {} isn't free".format(port_number)) yield from Bridge.add_nio(self, nio) @@ -88,7 +98,7 @@ class EthernetHub(Bridge): id=self._id, nio=nio, port=port_number)) - self._mapping[port_number] = nio + self._mappings[port_number] = nio @asyncio.coroutine def remove_nio(self, port_number): @@ -100,10 +110,10 @@ class EthernetHub(Bridge): :returns: the NIO that was bound to the allocated port """ - if port_number not in self._mapping: + if port_number not in self._mappings: raise DynamipsError("Port {} is not allocated".format(port_number)) - nio = self._mapping[port_number] + nio = self._mappings[port_number] yield from Bridge.remove_nio(self, nio) log.info('Ethernet switch "{name}" [{id}]: NIO {nio} removed from port {port}'.format(name=self._name, @@ -111,7 +121,7 @@ class EthernetHub(Bridge): nio=nio, port=port_number)) - del self._mapping[port_number] + del self._mappings[port_number] return nio @asyncio.coroutine @@ -124,10 +134,10 @@ class EthernetHub(Bridge): :param data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB """ - if port_number not in self._mapping: + if port_number not in self._mappings: raise DynamipsError("Port {} is not allocated".format(port_number)) - nio = self._mapping[port_number] + nio = self._mappings[port_number] data_link_type = data_link_type.lower() if data_link_type.startswith("dlt_"): @@ -151,10 +161,10 @@ class EthernetHub(Bridge): :param port_number: allocated port number """ - if port_number not in self._mapping: + if port_number not in self._mappings: raise DynamipsError("Port {} is not allocated".format(port_number)) - nio = self._mapping[port_number] + nio = self._mappings[port_number] yield from nio.unbind_filter("both") log.info('Ethernet hub "{name}" [{id}]: stopping packet capture on {port}'.format(name=self._name, id=self._id, diff --git a/gns3server/modules/dynamips/nodes/ethernet_switch.py b/gns3server/modules/dynamips/nodes/ethernet_switch.py index 24fe3020..bf919068 100644 --- a/gns3server/modules/dynamips/nodes/ethernet_switch.py +++ b/gns3server/modules/dynamips/nodes/ethernet_switch.py @@ -35,17 +35,32 @@ class EthernetSwitch(Device): Dynamips Ethernet switch. :param name: name for this switch - :param node_id: Node instance identifier + :param device_id: Device instance identifier :param project: Project instance :param manager: Parent VM Manager :param hypervisor: Dynamips hypervisor instance """ - def __init__(self, name, node_id, project, manager, hypervisor=None): + def __init__(self, name, device_id, project, manager, hypervisor=None): - super().__init__(name, node_id, project, manager, hypervisor) + super().__init__(name, device_id, project, manager, hypervisor) self._nios = {} - self._mapping = {} + self._mappings = {} + + def __json__(self): + + ethernet_switch_info = {"name": self.name, + "device_id": self.id, + "project_id": self.project.id} + + ports = [] + for port_number, settings in self._mappings.items(): + ports.append({"port": port_number, + "type": settings[0], + "vlan": settings[1]}) + + ethernet_switch_info["ports"] = ports + return ethernet_switch_info @asyncio.coroutine def create(self): @@ -82,14 +97,14 @@ class EthernetSwitch(Device): return self._nios @property - def mapping(self): + def mappings(self): """ - Returns port mapping + Returns port mappings - :returns: mapping list + :returns: mappings list """ - return self._mapping + return self._mappings @asyncio.coroutine def delete(self): @@ -97,10 +112,14 @@ class EthernetSwitch(Device): Deletes this Ethernet switch. """ - yield from self._hypervisor.send('ethsw delete "{}"'.format(self._name)) - log.info('Ethernet switch "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id)) + try: + yield from self._hypervisor.send('ethsw delete "{}"'.format(self._name)) + log.info('Ethernet switch "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id)) + except DynamipsError: + log.debug("Could not properly delete Ethernet switch {}".format(self._name)) self._hypervisor.devices.remove(self) - self._instances.remove(self._id) + if self._hypervisor and not self._hypervisor.devices: + yield from self.hypervisor.stop() @asyncio.coroutine def add_nio(self, nio, port_number): @@ -144,11 +163,27 @@ class EthernetSwitch(Device): port=port_number)) del self._nios[port_number] - if port_number in self._mapping: - del self._mapping[port_number] + if port_number in self._mappings: + del self._mappings[port_number] return nio + @asyncio.coroutine + def set_port_settings(self, port_number, settings): + """ + Applies port settings to a specific port. + + :param port_number: port number to set the settings + :param settings: port settings + """ + + if settings["type"] == "access": + yield from self.set_access_port(port_number, settings["vlan"]) + elif settings["type"] == "dot1q": + yield from self.set_dot1q_port(port_number, settings["vlan"]) + elif settings["type"] == "qinq": + yield from self.set_qinq_port(port_number, settings["vlan"]) + @asyncio.coroutine def set_access_port(self, port_number, vlan_id): """ @@ -170,7 +205,7 @@ class EthernetSwitch(Device): id=self._id, port=port_number, vlan_id=vlan_id)) - self._mapping[port_number] = ("access", vlan_id) + self._mappings[port_number] = ("access", vlan_id) @asyncio.coroutine def set_dot1q_port(self, port_number, native_vlan): @@ -194,7 +229,7 @@ class EthernetSwitch(Device): port=port_number, vlan_id=native_vlan)) - self._mapping[port_number] = ("dot1q", native_vlan) + self._mappings[port_number] = ("dot1q", native_vlan) @asyncio.coroutine def set_qinq_port(self, port_number, outer_vlan): @@ -217,7 +252,7 @@ class EthernetSwitch(Device): id=self._id, port=port_number, vlan_id=outer_vlan)) - self._mapping[port_number] = ("qinq", outer_vlan) + self._mappings[port_number] = ("qinq", outer_vlan) @asyncio.coroutine def get_mac_addr_table(self): diff --git a/gns3server/modules/dynamips/nodes/frame_relay_switch.py b/gns3server/modules/dynamips/nodes/frame_relay_switch.py index cdbeb997..301bd732 100644 --- a/gns3server/modules/dynamips/nodes/frame_relay_switch.py +++ b/gns3server/modules/dynamips/nodes/frame_relay_switch.py @@ -34,17 +34,24 @@ class FrameRelaySwitch(Device): Dynamips Frame Relay switch. :param name: name for this switch - :param node_id: Node instance identifier + :param device_id: Device instance identifier :param project: Project instance :param manager: Parent VM Manager :param hypervisor: Dynamips hypervisor instance """ - def __init__(self, name, node_id, project, manager, hypervisor=None): + def __init__(self, name, device_id, project, manager, hypervisor=None): - super().__init__(name, node_id, project, manager, hypervisor) + super().__init__(name, device_id, project, manager, hypervisor) self._nios = {} - self._mapping = {} + self._mappings = {} + + def __json__(self): + + return {"name": self.name, + "device_id": self.id, + "project_id": self.project.id, + "mappings": self._mappings} @asyncio.coroutine def create(self): @@ -81,14 +88,14 @@ class FrameRelaySwitch(Device): return self._nios @property - def mapping(self): + def mappings(self): """ - Returns port mapping + Returns port mappings - :returns: mapping list + :returns: mappings list """ - return self._mapping + return self._mappings @asyncio.coroutine def delete(self): @@ -96,10 +103,14 @@ class FrameRelaySwitch(Device): Deletes this Frame Relay switch. """ - yield from self._hypervisor.send('frsw delete "{}"'.format(self._name)) - log.info('Frame Relay switch "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id)) + try: + yield from self._hypervisor.send('frsw delete "{}"'.format(self._name)) + log.info('Frame Relay switch "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id)) + except DynamipsError: + log.debug("Could not properly delete Frame relay switch {}".format(self._name)) self._hypervisor.devices.remove(self) - self._instances.remove(self._id) + if self._hypervisor and not self._hypervisor.devices: + yield from self.hypervisor.stop() def has_port(self, port): """ @@ -151,6 +162,22 @@ class FrameRelaySwitch(Device): del self._nios[port_number] return nio + @asyncio.coroutine + def set_mappings(self, mappings): + """ + Applies VC mappings + + :param mappings: mappings (dict) + """ + + for source, destination in mappings.items(): + source_port, source_dlci = map(int, source.split(':')) + destination_port, destination_dlci = map(int, destination.split(':')) + if self.has_port(destination_port): + if (source_port, source_dlci) not in self.mappings and (destination_port, destination_dlci) not in self.mappings: + yield from self.map_vc(source_port, source_dlci, destination_port, destination_dlci) + yield from self.map_vc(destination_port, destination_dlci, source_port, source_dlci) + @asyncio.coroutine def map_vc(self, port1, dlci1, port2, dlci2): """ @@ -184,7 +211,7 @@ class FrameRelaySwitch(Device): port2=port2, dlci2=dlci2)) - self._mapping[(port1, dlci1)] = (port2, dlci2) + self._mappings[(port1, dlci1)] = (port2, dlci2) @asyncio.coroutine def unmap_vc(self, port1, dlci1, port2, dlci2): @@ -218,7 +245,7 @@ class FrameRelaySwitch(Device): dlci1=dlci1, port2=port2, dlci2=dlci2)) - del self._mapping[(port1, dlci1)] + del self._mappings[(port1, dlci1)] @asyncio.coroutine def start_capture(self, port_number, output_file, data_link_type="DLT_FRELAY"): diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index 34e28ac5..2b4b3d7d 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -294,8 +294,12 @@ class Router(BaseVM): # router is already closed return - if self._hypervisor: - yield from self.stop() + self._hypervisor.devices.remove(self) + if self._hypervisor and not self._hypervisor.devices: + try: + yield from self.stop() + except DynamipsError: + pass yield from self.hypervisor.stop() if self._console: diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index 8fa0dc85..1bc9280c 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -59,6 +59,7 @@ class Project: self._vms = set() self._vms_to_destroy = set() + self._devices = set() self.temporary = temporary @@ -128,6 +129,11 @@ class Project: return self._vms + @property + def devices(self): + + return self._devices + @property def temporary(self): @@ -213,7 +219,7 @@ class Project: Add a VM to the project. In theory this should be called by the VM manager. - :param vm: A VM instance + :param vm: VM instance """ self._vms.add(vm) @@ -223,12 +229,33 @@ class Project: Remove a VM from the project. In theory this should be called by the VM manager. - :param vm: A VM instance + :param vm: VM instance """ if vm in self._vms: self._vms.remove(vm) + def add_device(self, device): + """ + Add a device to the project. + In theory this should be called by the VM manager. + + :param device: Device instance + """ + + self._devices.add(device) + + def remove_device(self, device): + """ + Remove a device from the project. + In theory this should be called by the VM manager. + + :param device: Device instance + """ + + if device in self._devices: + self._devices.remove(device) + @asyncio.coroutine def close(self): """Close the project, but keep information on disk""" @@ -250,13 +277,16 @@ class Project: else: vm.close() + for device in self._devices: + tasks.append(asyncio.async(device.delete())) + if tasks: done, _ = yield from asyncio.wait(tasks) for future in done: try: future.result() except Exception as e: - log.error("Could not close VM {}".format(e), exc_info=1) + log.error("Could not close VM or device {}".format(e), exc_info=1) if cleanup and os.path.exists(self.path): try: diff --git a/gns3server/schemas/dynamips_device.py b/gns3server/schemas/dynamips_device.py new file mode 100644 index 00000000..4e44afb4 --- /dev/null +++ b/gns3server/schemas/dynamips_device.py @@ -0,0 +1,341 @@ +# -*- 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 . + + +DEVICE_CREATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to create a new Dynamips device instance", + "type": "object", + "properties": { + "name": { + "description": "Dynamips device name", + "type": "string", + "minLength": 1, + }, + "device_id": { + "description": "Dynamips device instance identifier", + "type": "string", + "minLength": 36, + "maxLength": 36, + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + }, + "device_type": { + "description": "Dynamips device type", + "type": "string", + "minLength": 1, + }, + }, + "additionalProperties": False, + "required": ["name", "device_type"] +} + +DEVICE_UPDATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Dynamips device instance", + "type": "object", + "definitions": { + "EthernetSwitchPort": { + "description": "Ethernet switch port", + "properties": { + "port": { + "description": "Port number", + "type": "integer", + "minimum": 1 + }, + "type": { + "description": "Port type", + "enum": ["access", "dot1q", "qinq"], + }, + "vlan": {"description": "VLAN number", + "type": "integer", + "minimum": 1 + }, + }, + "required": ["port", "type", "vlan"], + "additionalProperties": False + }, + }, + "properties": { + "name": { + "description": "Dynamips device instance name", + "type": "string", + "minLength": 1, + }, + "ports": { + "type": "array", + "items": [ + {"type": "object", + "oneOf": [ + {"$ref": "#/definitions/EthernetSwitchPort"} + ]}, + ] + } + }, + "additionalProperties": False, +} + +DEVICE_OBJECT_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Dynamips device instance", + "type": "object", + "definitions": { + "EthernetSwitchPort": { + "description": "Ethernet switch port", + "properties": { + "port": { + "description": "Port number", + "type": "integer", + "minimum": 1 + }, + "type": { + "description": "Port type", + "enum": ["access", "dot1q", "qinq"], + }, + "vlan": {"description": "VLAN number", + "type": "integer", + "minimum": 1 + }, + }, + "required": ["port", "type", "vlan"], + "additionalProperties": False + }, + }, + "properties": { + "device_id": { + "description": "Dynamips router instance UUID", + "type": "string", + "minLength": 36, + "maxLength": 36, + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + }, + "project_id": { + "description": "Project UUID", + "type": "string", + "minLength": 36, + "maxLength": 36, + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + }, + "name": { + "description": "Dynamips device instance name", + "type": "string", + "minLength": 1, + }, + "ports": { + # only Ethernet switches have ports + "type": "array", + "items": [ + {"type": "object", + "oneOf": [ + {"$ref": "#/definitions/EthernetSwitchPort"} + ]}, + ] + }, + "mappings": { + # only Frame-Relay and ATM switches have mappings + "type": "object", + } + }, + "additionalProperties": False, + "required": ["name", "device_id", "project_id"] +} + +DEVICE_NIO_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to add a NIO for a Dynamips device 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": { + "nio": { + "type": "object", + "oneOf": [ + {"$ref": "#/definitions/UDP"}, + {"$ref": "#/definitions/Ethernet"}, + {"$ref": "#/definitions/LinuxEthernet"}, + {"$ref": "#/definitions/TAP"}, + {"$ref": "#/definitions/UNIX"}, + {"$ref": "#/definitions/VDE"}, + {"$ref": "#/definitions/NULL"}, + ] + }, + "port_settings": { + # only Ethernet switches have port settings + "type": "object", + "description": "Ethernet switch", + "properties": { + "type": { + "description": "Port type", + "enum": ["access", "dot1q", "qinq"], + }, + "vlan": {"description": "VLAN number", + "type": "integer", + "minimum": 1 + }, + }, + "required": ["type", "vlan"], + "additionalProperties": False + }, + "mappings": { + # only Frame-Relay and ATM switches have mappings + "type": "object", + } + }, + "additionalProperties": False, + "required": ["nio"] +} + +DEVICE_CAPTURE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to start a packet capture on an Device instance port", + "type": "object", + "properties": { + "capture_file_name": { + "description": "Capture file name", + "type": "string", + "minLength": 1, + }, + "data_link_type": { + "description": "PCAP data link type", + "type": "string", + "minLength": 1, + }, + }, + "additionalProperties": False, + "required": ["capture_file_name"] +} diff --git a/gns3server/schemas/dynamips.py b/gns3server/schemas/dynamips_vm.py similarity index 74% rename from gns3server/schemas/dynamips.py rename to gns3server/schemas/dynamips_vm.py index 2c4f755b..c30d2920 100644 --- a/gns3server/schemas/dynamips.py +++ b/gns3server/schemas/dynamips_vm.py @@ -880,281 +880,3 @@ VM_OBJECT_SCHEMA = { "additionalProperties": False, "required": ["name", "vm_id", "project_id", "dynamips_id"] } - -DEVICE_CREATE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to create a new Dynamips device instance", - "type": "object", - "properties": { - "name": { - "description": "Dynamips device name", - "type": "string", - "minLength": 1, - }, - "vm_id": { - "description": "Dynamips device instance identifier", - "type": "string", - "minLength": 36, - "maxLength": 36, - "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" - }, - }, - "additionalProperties": False, - "required": ["name"] -} - -ETHHUB_UPDATE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to update an Ethernet hub instance", - "type": "object", - "properties": { - "id": { - "description": "Ethernet hub instance ID", - "type": "integer" - }, - "name": { - "description": "Ethernet hub name", - "type": "string", - "minLength": 1, - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -ETHHUB_NIO_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to add a NIO for an Ethernet hub 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": "Ethernet hub instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the Ethernet hub instance", - "type": "integer" - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 1, - }, - "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"] -} - -ETHHUB_DELETE_NIO_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to delete a NIO for an Ethernet hub instance", - "type": "object", - "properties": { - "id": { - "description": "Ethernet hub instance ID", - "type": "integer" - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 1, - }, - }, - "additionalProperties": False, - "required": ["id", "port"] -} - -ETHHUB_START_CAPTURE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to start a packet capture on an Ethernet hub instance port", - "type": "object", - "properties": { - "id": { - "description": "Ethernet hub instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the Ethernet hub instance", - "type": "integer" - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 1, - }, - "capture_file_name": { - "description": "Capture file name", - "type": "string", - "minLength": 1, - }, - "data_link_type": { - "description": "PCAP data link type", - "type": "string", - "minLength": 1, - }, - }, - "additionalProperties": False, - "required": ["id", "port_id", "port", "capture_file_name"] -} - -ETHHUB_STOP_CAPTURE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to stop a packet capture on an Ethernet hub instance port", - "type": "object", - "properties": { - "id": { - "description": "Ethernet hub instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the Ethernet hub instance", - "type": "integer" - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 1, - }, - }, - "additionalProperties": False, - "required": ["id", "port_id", "port"] -} From 26f7195288b60977621545564ad510a13733a760 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 15 Feb 2015 17:45:53 -0700 Subject: [PATCH 249/485] Dynamips devices packet capture. --- .../handlers/dynamips_device_handler.py | 91 +++++++++---------- .../modules/dynamips/nodes/atm_switch.py | 12 +-- .../modules/dynamips/nodes/ethernet_hub.py | 12 +-- .../modules/dynamips/nodes/ethernet_switch.py | 12 +-- .../dynamips/nodes/frame_relay_switch.py | 12 +-- gns3server/schemas/dynamips_device.py | 2 +- 6 files changed, 68 insertions(+), 73 deletions(-) diff --git a/gns3server/handlers/dynamips_device_handler.py b/gns3server/handlers/dynamips_device_handler.py index fb5f284e..11c3392c 100644 --- a/gns3server/handlers/dynamips_device_handler.py +++ b/gns3server/handlers/dynamips_device_handler.py @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - +import os import asyncio from ..web.route import Route from ..schemas.dynamips_device import DEVICE_CREATE_SCHEMA @@ -184,51 +184,46 @@ class DynamipsDeviceHandler: yield from device.remove_nio(port_number) response.set_status(204) - # @Route.post( - # r"/projects/{project_id}/dynamips/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/start_capture", - # parameters={ - # "project_id": "UUID for the project", - # "vm_id": "UUID for the instance", - # "adapter_number": "Adapter to start a packet capture", - # "port_number": "Port on the adapter" - # }, - # status_codes={ - # 200: "Capture started", - # 400: "Invalid request", - # 404: "Instance doesn't exist" - # }, - # description="Start a packet capture on a Dynamips VM instance", - # input=VM_CAPTURE_SCHEMA) - # def start_capture(request, response): - # - # dynamips_manager = Dynamips.instance() - # vm = dynamips_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) - # slot_number = int(request.match_info["adapter_number"]) - # port_number = int(request.match_info["port_number"]) - # pcap_file_path = os.path.join(vm.project.capture_working_directory(), request.json["capture_file_name"]) - # yield from vm.start_capture(slot_number, port_number, pcap_file_path, request.json["data_link_type"]) - # response.json({"pcap_file_path": pcap_file_path}) - # - # @Route.post( - # r"/projects/{project_id}/dynamips/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/stop_capture", - # parameters={ - # "project_id": "UUID for the project", - # "vm_id": "UUID for the instance", - # "adapter_number": "Adapter to stop a packet capture", - # "port_number": "Port on the adapter (always 0)" - # }, - # status_codes={ - # 204: "Capture stopped", - # 400: "Invalid request", - # 404: "Instance doesn't exist" - # }, - # description="Stop a packet capture on a Dynamips VM instance") - # def start_capture(request, response): - # - # dynamips_manager = Dynamips.instance() - # vm = dynamips_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) - # slot_number = int(request.match_info["adapter_number"]) - # port_number = int(request.match_info["port_number"]) - # yield from vm.stop_capture(slot_number, port_number) - # response.set_status(204) + @Route.post( + r"/projects/{project_id}/dynamips/devices/{device_id}/ports/{port_number:\d+}/start_capture", + parameters={ + "project_id": "UUID for the project", + "device_id": "UUID for the instance", + "port_number": "Port on the device" + }, + status_codes={ + 200: "Capture started", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Start a packet capture on a Dynamips device instance", + input=DEVICE_CAPTURE_SCHEMA) + def start_capture(request, response): + + dynamips_manager = Dynamips.instance() + device = dynamips_manager.get_device(request.match_info["device_id"], project_id=request.match_info["project_id"]) + port_number = int(request.match_info["port_number"]) + pcap_file_path = os.path.join(device.project.capture_working_directory(), request.json["capture_file_name"]) + yield from device.start_capture(port_number, pcap_file_path, request.json["data_link_type"]) + response.json({"pcap_file_path": pcap_file_path}) + + @Route.post( + r"/projects/{project_id}/dynamips/devices/{device_id}/ports/{port_number:\d+}/stop_capture", + parameters={ + "project_id": "UUID for the project", + "device_id": "UUID for the instance", + "port_number": "Port on the device" + }, + status_codes={ + 204: "Capture stopped", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Stop a packet capture on a Dynamips device instance") + def start_capture(request, response): + dynamips_manager = Dynamips.instance() + device = dynamips_manager.get_device(request.match_info["device_id"], project_id=request.match_info["project_id"]) + port_number = int(request.match_info["port_number"]) + yield from device.stop_capture(port_number) + response.set_status(204) diff --git a/gns3server/modules/dynamips/nodes/atm_switch.py b/gns3server/modules/dynamips/nodes/atm_switch.py index 07094479..0416fa08 100644 --- a/gns3server/modules/dynamips/nodes/atm_switch.py +++ b/gns3server/modules/dynamips/nodes/atm_switch.py @@ -368,9 +368,9 @@ class ATMSwitch(Device): yield from nio.bind_filter("both", "capture") yield from nio.setup_filter("both", "{} {}".format(data_link_type, output_file)) - log.info('ATM switch "{name}" [{id}]: starting packet capture on {port}'.format(name=self._name, - id=self._id, - port=port_number)) + log.info('ATM switch "{name}" [{id}]: starting packet capture on port {port}'.format(name=self._name, + id=self._id, + port=port_number)) @asyncio.coroutine def stop_capture(self, port_number): @@ -385,6 +385,6 @@ class ATMSwitch(Device): nio = self._nios[port_number] yield from nio.unbind_filter("both") - log.info('ATM switch "{name}" [{id}]: stopping packet capture on {port}'.format(name=self._name, - id=self._id, - port=port_number)) + log.info('ATM switch "{name}" [{id}]: stopping packet capture on port {port}'.format(name=self._name, + id=self._id, + port=port_number)) diff --git a/gns3server/modules/dynamips/nodes/ethernet_hub.py b/gns3server/modules/dynamips/nodes/ethernet_hub.py index 806b8d3f..33c523ab 100644 --- a/gns3server/modules/dynamips/nodes/ethernet_hub.py +++ b/gns3server/modules/dynamips/nodes/ethernet_hub.py @@ -149,9 +149,9 @@ class EthernetHub(Bridge): yield from nio.bind_filter("both", "capture") yield from nio.setup_filter("both", "{} {}".format(data_link_type, output_file)) - log.info('Ethernet hub "{name}" [{id}]: starting packet capture on {port}'.format(name=self._name, - id=self._id, - port=port_number)) + log.info('Ethernet hub "{name}" [{id}]: starting packet capture on port {port}'.format(name=self._name, + id=self._id, + port=port_number)) @asyncio.coroutine def stop_capture(self, port_number): @@ -166,6 +166,6 @@ class EthernetHub(Bridge): nio = self._mappings[port_number] yield from nio.unbind_filter("both") - log.info('Ethernet hub "{name}" [{id}]: stopping packet capture on {port}'.format(name=self._name, - id=self._id, - port=port_number)) + log.info('Ethernet hub "{name}" [{id}]: stopping packet capture on port {port}'.format(name=self._name, + id=self._id, + port=port_number)) diff --git a/gns3server/modules/dynamips/nodes/ethernet_switch.py b/gns3server/modules/dynamips/nodes/ethernet_switch.py index bf919068..65fe07c1 100644 --- a/gns3server/modules/dynamips/nodes/ethernet_switch.py +++ b/gns3server/modules/dynamips/nodes/ethernet_switch.py @@ -298,9 +298,9 @@ class EthernetSwitch(Device): yield from nio.bind_filter("both", "capture") yield from nio.setup_filter("both", "{} {}".format(data_link_type, output_file)) - log.info('Ethernet switch "{name}" [{id}]: starting packet capture on {port}'.format(name=self._name, - id=self._id, - port=port_number)) + log.info('Ethernet switch "{name}" [{id}]: starting packet capture on port {port}'.format(name=self._name, + id=self._id, + port=port_number)) @asyncio.coroutine def stop_capture(self, port_number): @@ -315,6 +315,6 @@ class EthernetSwitch(Device): nio = self._nios[port_number] yield from nio.unbind_filter("both") - log.info('Ethernet switch "{name}" [{id}]: stopping packet capture on {port}'.format(name=self._name, - id=self._id, - port=port_number)) + log.info('Ethernet switch "{name}" [{id}]: stopping packet capture on port {port}'.format(name=self._name, + id=self._id, + port=port_number)) diff --git a/gns3server/modules/dynamips/nodes/frame_relay_switch.py b/gns3server/modules/dynamips/nodes/frame_relay_switch.py index 301bd732..67d2813d 100644 --- a/gns3server/modules/dynamips/nodes/frame_relay_switch.py +++ b/gns3server/modules/dynamips/nodes/frame_relay_switch.py @@ -272,9 +272,9 @@ class FrameRelaySwitch(Device): yield from nio.bind_filter("both", "capture") yield from nio.setup_filter("both", "{} {}".format(data_link_type, output_file)) - log.info('Frame relay switch "{name}" [{id}]: starting packet capture on {port}'.format(name=self._name, - id=self._id, - port=port_number)) + log.info('Frame relay switch "{name}" [{id}]: starting packet capture on port {port}'.format(name=self._name, + id=self._id, + port=port_number)) @asyncio.coroutine def stop_capture(self, port_number): @@ -289,6 +289,6 @@ class FrameRelaySwitch(Device): nio = self._nios[port_number] yield from nio.unbind_filter("both") - log.info('Frame relay switch "{name}" [{id}]: stopping packet capture on {port}'.format(name=self._name, - id=self._id, - port=port_number)) + log.info('Frame relay switch "{name}" [{id}]: stopping packet capture on port {port}'.format(name=self._name, + id=self._id, + port=port_number)) diff --git a/gns3server/schemas/dynamips_device.py b/gns3server/schemas/dynamips_device.py index 4e44afb4..28ba32be 100644 --- a/gns3server/schemas/dynamips_device.py +++ b/gns3server/schemas/dynamips_device.py @@ -337,5 +337,5 @@ DEVICE_CAPTURE_SCHEMA = { }, }, "additionalProperties": False, - "required": ["capture_file_name"] + "required": ["capture_file_name", "data_link_type"] } From 78ffe313fd113f1ec842c42d995736d613f7837a Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 15 Feb 2015 22:13:24 -0700 Subject: [PATCH 250/485] Dynamips VM & device deletion and ghost support. --- gns3server/handlers/dynamips_vm_handler.py | 1 + gns3server/handlers/project_handler.py | 3 + gns3server/modules/base_manager.py | 10 ++ gns3server/modules/base_vm.py | 20 +++ gns3server/modules/dynamips/__init__.py | 153 +++++++++++------- .../modules/dynamips/dynamips_hypervisor.py | 29 ---- .../modules/dynamips/nodes/atm_switch.py | 3 +- gns3server/modules/dynamips/nodes/bridge.py | 3 +- .../modules/dynamips/nodes/ethernet_switch.py | 3 +- .../dynamips/nodes/frame_relay_switch.py | 3 +- gns3server/modules/dynamips/nodes/router.py | 104 +++++++----- gns3server/modules/project.py | 78 ++++----- 12 files changed, 225 insertions(+), 185 deletions(-) diff --git a/gns3server/handlers/dynamips_vm_handler.py b/gns3server/handlers/dynamips_vm_handler.py index 5981db43..44d9a707 100644 --- a/gns3server/handlers/dynamips_vm_handler.py +++ b/gns3server/handlers/dynamips_vm_handler.py @@ -67,6 +67,7 @@ class DynamipsVMHandler: else: setter(value) + yield from dynamips_manager.ghost_ios_support(vm) response.set_status(201) response.json(vm) diff --git a/gns3server/handlers/project_handler.py b/gns3server/handlers/project_handler.py index 0ab4d681..880b5473 100644 --- a/gns3server/handlers/project_handler.py +++ b/gns3server/handlers/project_handler.py @@ -18,6 +18,7 @@ from ..web.route import Route from ..schemas.project import PROJECT_OBJECT_SCHEMA, PROJECT_CREATE_SCHEMA, PROJECT_UPDATE_SCHEMA from ..modules.project_manager import ProjectManager +from ..modules import MODULES class ProjectHandler: @@ -112,6 +113,8 @@ class ProjectHandler: pm = ProjectManager.instance() project = pm.get_project(request.match_info["project_id"]) yield from project.close() + for module in MODULES: + yield from module.instance().project_closed(project.path) response.set_status(204) @classmethod diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index fa633f8d..4013dad3 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -205,6 +205,16 @@ class BaseManager: vm.close() return vm + @asyncio.coroutine + def project_closed(self, project_dir): + """ + Called when a project is closed. + + :param project_dir: project directory + """ + + pass + @asyncio.coroutine def delete_vm(self, vm_id): """ diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index acf04f12..bc4386e6 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -15,7 +15,14 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os import logging +import aiohttp +import shutil +import asyncio + +from ..utils.asyncio import wait_run_in_executor + log = logging.getLogger(__name__) @@ -107,6 +114,19 @@ class BaseVM: name=self.name, id=self.id)) + @asyncio.coroutine + def delete(self): + """ + Delete the VM (including all its files). + """ + + directory = self.project.vm_working_dir(self) + if os.path.exists(directory): + try: + yield from wait_run_in_executor(shutil.rmtree, directory) + except OSError as e: + raise aiohttp.web.HTTPInternalServerError(text="Could not delete the VM working directory: {}".format(e)) + def start(self): """ Starts the VM process. diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index 55fc0597..579d8bc3 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -26,11 +26,14 @@ import shutil import socket import time import asyncio +import tempfile +import glob import logging log = logging.getLogger(__name__) from gns3server.utils.interfaces import get_windows_interfaces +from gns3server.utils.asyncio import wait_run_in_executor from pkg_resources import parse_version from uuid import UUID, uuid4 from ..base_manager import BaseManager @@ -63,11 +66,9 @@ class Dynamips(BaseManager): super().__init__() self._devices = {} + self._ghost_files = set() self._dynamips_path = None - # FIXME: temporary - self._working_dir = "/tmp" - @asyncio.coroutine def unload(self): @@ -86,18 +87,43 @@ class Dynamips(BaseManager): log.error("Could not stop device hypervisor {}".format(e), exc_info=1) continue -# files = glob.glob(os.path.join(self._working_dir, "dynamips", "*.ghost")) -# files += glob.glob(os.path.join(self._working_dir, "dynamips", "*_lock")) -# files += glob.glob(os.path.join(self._working_dir, "dynamips", "ilt_*")) -# files += glob.glob(os.path.join(self._working_dir, "dynamips", "c[0-9][0-9][0-9][0-9]_*_rommon_vars")) -# files += glob.glob(os.path.join(self._working_dir, "dynamips", "c[0-9][0-9][0-9][0-9]_*_ssa")) -# for file in files: -# try: -# log.debug("deleting file {}".format(file)) -# os.remove(file) -# except OSError as e: -# log.warn("could not delete file {}: {}".format(file, e)) -# continue + @asyncio.coroutine + def project_closed(self, project_dir): + """ + Called when a project is closed. + + :param project_dir: project directory + """ + + # delete the Dynamips devices + tasks = [] + for device in self._devices.values(): + tasks.append(asyncio.async(device.delete())) + + if tasks: + done, _ = yield from asyncio.wait(tasks) + for future in done: + try: + future.result() + except Exception as e: + log.error("Could not delete device {}".format(e), exc_info=1) + + # delete useless files + project_dir = os.path.join(project_dir, 'project-files', self.module_name.lower()) + files = glob.glob(os.path.join(project_dir, "*.ghost")) + files += glob.glob(os.path.join(project_dir, "*_lock")) + files += glob.glob(os.path.join(project_dir, "ilt_*")) + files += glob.glob(os.path.join(project_dir, "c[0-9][0-9][0-9][0-9]_*_rommon_vars")) + files += glob.glob(os.path.join(project_dir, "c[0-9][0-9][0-9][0-9]_*_ssa")) + for file in files: + try: + log.debug("Deleting file {}".format(file)) + if file in self._ghost_files: + self._ghost_files.remove(file) + yield from wait_run_in_executor(os.remove, file) + except OSError as e: + log.warn("Could not delete file {}: {}".format(file, e)) + continue @property def dynamips_path(self): @@ -126,7 +152,6 @@ class Dynamips(BaseManager): device = self._DEVICE_CLASS(name, device_id, project, self, device_type, *args, **kwargs) yield from device.create() self._devices[device.id] = device - project.add_device(device) return device def get_device(self, device_id, project_id=None): @@ -170,7 +195,6 @@ class Dynamips(BaseManager): device = self.get_device(device_id) yield from device.delete() - device.project.remove_device(device) del self._devices[device.id] return device @@ -220,16 +244,21 @@ class Dynamips(BaseManager): log.info("Dynamips server ready after {:.4f} seconds".format(time.time() - begin)) @asyncio.coroutine - def start_new_hypervisor(self): + def start_new_hypervisor(self, working_dir=None): """ Creates a new Dynamips process and start it. + :param working_dir: working directory + :returns: the new hypervisor instance """ if not self._dynamips_path: self.find_dynamips() + if not working_dir: + working_dir = tempfile.gettempdir() + try: # let the OS find an unused port for the Dynamips hypervisor with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: @@ -238,9 +267,9 @@ class Dynamips(BaseManager): except OSError as e: raise DynamipsError("Could not find free port for the Dynamips hypervisor: {}".format(e)) - hypervisor = Hypervisor(self._dynamips_path, self._working_dir, "127.0.0.1", port) + hypervisor = Hypervisor(self._dynamips_path, working_dir, "127.0.0.1", port) - log.info("Ceating new hypervisor {}:{} with working directory {}".format(hypervisor.host, hypervisor.port, self._working_dir)) + log.info("Ceating new hypervisor {}:{} with working directory {}".format(hypervisor.host, hypervisor.port, working_dir)) yield from hypervisor.start() yield from self._wait_for_hypervisor("127.0.0.1", port) @@ -252,6 +281,13 @@ class Dynamips(BaseManager): return hypervisor + @asyncio.coroutine + def ghost_ios_support(self, vm): + + ghost_ios_support = self.config.get_section_config("Dynamips").get("ghost_ios_support", True) + if ghost_ios_support: + yield from self._set_ghost_ios(vm) + @asyncio.coroutine def create_nio(self, node, nio_settings): """ @@ -317,45 +353,44 @@ class Dynamips(BaseManager): yield from nio.create() return nio -# def set_ghost_ios(self, router): -# """ -# Manages Ghost IOS support. -# -# :param router: Router instance -# """ -# -# if not router.mmap: -# raise DynamipsError("mmap support is required to enable ghost IOS support") -# -# ghost_instance = router.formatted_ghost_file() -# all_ghosts = [] -# -# # search of an existing ghost instance across all hypervisors -# for hypervisor in self._hypervisor_manager.hypervisors: -# all_ghosts.extend(hypervisor.ghosts) -# -# if ghost_instance not in all_ghosts: -# # create a new ghost IOS instance -# ghost = Router(router.hypervisor, "ghost-" + ghost_instance, router.platform, ghost_flag=True) -# ghost.image = router.image -# # for 7200s, the NPE must be set when using an NPE-G2. -# if router.platform == "c7200": -# ghost.npe = router.npe -# ghost.ghost_status = 1 -# ghost.ghost_file = ghost_instance -# ghost.ram = router.ram -# try: -# ghost.start() -# ghost.stop() -# except DynamipsError: -# raise -# finally: -# ghost.clean_delete() -# -# if router.ghost_file != ghost_instance: -# # set the ghost file to the router -# router.ghost_status = 2 -# router.ghost_file = ghost_instance + @asyncio.coroutine + def _set_ghost_ios(self, vm): + """ + Manages Ghost IOS support. + + :param vm: VM instance + """ + + if not vm.mmap: + raise DynamipsError("mmap support is required to enable ghost IOS support") + + ghost_file = vm.formatted_ghost_file() + ghost_file_path = os.path.join(vm.hypervisor.working_dir, ghost_file) + if ghost_file_path not in self._ghost_files: + # create a new ghost IOS instance + ghost_id = str(uuid4()) + ghost = Router("ghost-" + ghost_file, ghost_id, vm.project, vm.manager, platform=vm.platform, hypervisor=vm.hypervisor, ghost_flag=True) + yield from ghost.create() + yield from ghost.set_image(vm.image) + # for 7200s, the NPE must be set when using an NPE-G2. + if vm.platform == "c7200": + yield from ghost.set_npe(vm.npe) + yield from ghost.set_ghost_status(1) + yield from ghost.set_ghost_file(ghost_file) + yield from ghost.set_ram(vm.ram) + try: + yield from ghost.start() + yield from ghost.stop() + self._ghost_files.add(ghost_file_path) + except DynamipsError: + raise + finally: + yield from ghost.clean_delete() + + if vm.ghost_file != ghost_file: + # set the ghost file to the router + yield from vm.set_ghost_status(2) + yield from vm.set_ghost_file(ghost_file) # # def create_config_from_file(self, local_base_config, router, destination_config_path): # """ diff --git a/gns3server/modules/dynamips/dynamips_hypervisor.py b/gns3server/modules/dynamips/dynamips_hypervisor.py index e4db5623..b6c9304f 100644 --- a/gns3server/modules/dynamips/dynamips_hypervisor.py +++ b/gns3server/modules/dynamips/dynamips_hypervisor.py @@ -20,7 +20,6 @@ Interface for Dynamips hypervisor management module ("hypervisor") http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L46 """ -import socket import re import logging import asyncio @@ -53,15 +52,7 @@ class DynamipsHypervisor: self._port = port self._devices = [] - self._ghosts = {} - self._jitsharing_groups = {} self._working_dir = working_dir - # self._console_start_port_range = 2001 - # self._console_end_port_range = 2500 - # self._aux_start_port_range = 2501 - # self._aux_end_port_range = 3000 - # self._udp_start_port_range = 10001 - # self._udp_end_port_range = 20000 self._nio_udp_auto_instances = {} self._version = "N/A" self._timeout = timeout @@ -193,26 +184,6 @@ class DynamipsHypervisor: return self._devices - @property - def ghosts(self): - """ - Returns a list of the ghosts hosted by this hypervisor. - - :returns: Ghosts dict (image_name -> device) - """ - - return self._ghosts - - def add_ghost(self, image_name, router): - """ - Adds a ghost name to the list of ghosts created on this hypervisor. - - :param image_name: name of the ghost image - :param router: Router instance - """ - - self._ghosts[image_name] = router - @property def port(self): """ diff --git a/gns3server/modules/dynamips/nodes/atm_switch.py b/gns3server/modules/dynamips/nodes/atm_switch.py index 0416fa08..32867883 100644 --- a/gns3server/modules/dynamips/nodes/atm_switch.py +++ b/gns3server/modules/dynamips/nodes/atm_switch.py @@ -58,7 +58,8 @@ class ATMSwitch(Device): def create(self): if self._hypervisor is None: - self._hypervisor = yield from self.manager.start_new_hypervisor() + module_workdir = self.project.module_working_directory(self.manager.module_name.lower()) + self._hypervisor = yield from self.manager.start_new_hypervisor(working_dir=module_workdir) yield from self._hypervisor.send('atmsw create "{}"'.format(self._name)) log.info('ATM switch "{name}" [{id}] has been created'.format(name=self._name, id=self._id)) diff --git a/gns3server/modules/dynamips/nodes/bridge.py b/gns3server/modules/dynamips/nodes/bridge.py index f5c410d4..5f056101 100644 --- a/gns3server/modules/dynamips/nodes/bridge.py +++ b/gns3server/modules/dynamips/nodes/bridge.py @@ -44,7 +44,8 @@ class Bridge(Device): def create(self): if self._hypervisor is None: - self._hypervisor = yield from self.manager.start_new_hypervisor() + module_workdir = self.project.module_working_directory(self.manager.module_name.lower()) + self._hypervisor = yield from self.manager.start_new_hypervisor(working_dir=module_workdir) yield from self._hypervisor.send('nio_bridge create "{}"'.format(self._name)) self._hypervisor.devices.append(self) diff --git a/gns3server/modules/dynamips/nodes/ethernet_switch.py b/gns3server/modules/dynamips/nodes/ethernet_switch.py index 65fe07c1..b0e425b8 100644 --- a/gns3server/modules/dynamips/nodes/ethernet_switch.py +++ b/gns3server/modules/dynamips/nodes/ethernet_switch.py @@ -66,7 +66,8 @@ class EthernetSwitch(Device): def create(self): if self._hypervisor is None: - self._hypervisor = yield from self.manager.start_new_hypervisor() + module_workdir = self.project.module_working_directory(self.manager.module_name.lower()) + self._hypervisor = yield from self.manager.start_new_hypervisor(working_dir=module_workdir) yield from self._hypervisor.send('ethsw create "{}"'.format(self._name)) log.info('Ethernet switch "{name}" [{id}] has been created'.format(name=self._name, id=self._id)) diff --git a/gns3server/modules/dynamips/nodes/frame_relay_switch.py b/gns3server/modules/dynamips/nodes/frame_relay_switch.py index 67d2813d..16572871 100644 --- a/gns3server/modules/dynamips/nodes/frame_relay_switch.py +++ b/gns3server/modules/dynamips/nodes/frame_relay_switch.py @@ -57,7 +57,8 @@ class FrameRelaySwitch(Device): def create(self): if self._hypervisor is None: - self._hypervisor = yield from self.manager.start_new_hypervisor() + module_workdir = self.project.module_working_directory(self.manager.module_name.lower()) + self._hypervisor = yield from self.manager.start_new_hypervisor(working_dir=module_workdir) yield from self._hypervisor.send('frsw create "{}"'.format(self._name)) log.info('Frame Relay switch "{name}" [{id}] has been created'.format(name=self._name, id=self._id)) diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index 2b4b3d7d..31c124a7 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -20,17 +20,19 @@ Interface for Dynamips virtual Machine module ("vm") http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L77 """ -from ...base_vm import BaseVM -from ..dynamips_error import DynamipsError - import asyncio import time import sys import os +import glob import logging log = logging.getLogger(__name__) +from ...base_vm import BaseVM +from ..dynamips_error import DynamipsError +from gns3server.utils.asyncio import wait_run_in_executor + class Router(BaseVM): @@ -51,11 +53,11 @@ class Router(BaseVM): 2: "running", 3: "suspended"} - def __init__(self, name, vm_id, project, manager, dynamips_id=None, platform="c7200", ghost_flag=False): + def __init__(self, name, vm_id, project, manager, dynamips_id=None, platform="c7200", hypervisor=None, ghost_flag=False): super().__init__(name, vm_id, project, manager) - self._hypervisor = None + self._hypervisor = hypervisor self._dynamips_id = dynamips_id self._closed = False self._name = name @@ -113,7 +115,7 @@ class Router(BaseVM): else: self._aux = self._manager.port_manager.get_free_console_port() else: - log.info("creating a new ghost IOS file") + log.info("Creating a new ghost IOS instance") self._dynamips_id = 0 self._name = "Ghost" @@ -166,10 +168,22 @@ class Router(BaseVM): cls._dynamips_ids.clear() + @property + def dynamips_id(self): + """ + Returns the Dynamips VM ID. + + :return: Dynamips VM identifier + """ + + return self._dynamips_id + @asyncio.coroutine def create(self): - self._hypervisor = yield from self.manager.start_new_hypervisor() + if not self._hypervisor: + module_workdir = self.project.module_working_directory(self.manager.module_name.lower()) + self._hypervisor = yield from self.manager.start_new_hypervisor(working_dir=module_workdir) yield from self._hypervisor.send('vm create "{name}" {id} {platform}'.format(name=self._name, id=self._dynamips_id, @@ -300,6 +314,7 @@ class Router(BaseVM): yield from self.stop() except DynamipsError: pass + yield from self._hypervisor.send('vm delete "{}"'.format(self._name)) yield from self.hypervisor.stop() if self._console: @@ -315,17 +330,6 @@ class Router(BaseVM): self._closed = True - @asyncio.coroutine - def delete(self): - """ - Deletes this router. - """ - - yield from self.close() - yield from self._hypervisor.send('vm delete "{}"'.format(self._name)) - self._hypervisor.devices.remove(self) - log.info('Router "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id)) - @property def platform(self): """ @@ -398,7 +402,6 @@ class Router(BaseVM): :param image: path to IOS image file """ - # encase image in quotes to protect spaces in the path yield from self._hypervisor.send('vm set_ios "{name}" "{image}"'.format(name=self._name, image=image)) log.info('Router "{name}" [{id}]: has a new IOS image set: "{image}"'.format(name=self._name, @@ -707,10 +710,6 @@ class Router(BaseVM): self._ghost_file = ghost_file - # this is a ghost instance, track this as a hosted ghost instance by this hypervisor - if self.ghost_status == 1: - self._hypervisor.add_ghost(ghost_file, self) - def formatted_ghost_file(self): """ Returns a properly formatted ghost file name. @@ -1548,24 +1547,41 @@ class Router(BaseVM): # except OSError as e: # raise DynamipsError("Could not save the private configuration {}: {}".format(config_path, e)) - # def clean_delete(self): - # """ - # Deletes this router & associated files (nvram, disks etc.) - # """ - # - # self._hypervisor.send("vm clean_delete {}".format(self._name)) - # self._hypervisor.devices.remove(self) - # - # if self._startup_config: - # # delete the startup-config - # startup_config_path = os.path.join(self.hypervisor.working_dir, "configs", "{}.cfg".format(self.name)) - # if os.path.isfile(startup_config_path): - # os.remove(startup_config_path) - # - # if self._private_config: - # # delete the private-config - # private_config_path = os.path.join(self.hypervisor.working_dir, "configs", "{}-private.cfg".format(self.name)) - # if os.path.isfile(private_config_path): - # os.remove(private_config_path) - # - # log.info("router {name} [id={id}] has been deleted (including associated files)".format(name=self._name, id=self._id)) + def delete(self): + """ + Delete the VM (including all its files). + """ + + # delete the VM files + project_dir = os.path.join(self.project.module_working_directory(self.manager.module_name.lower())) + files = glob.glob(os.path.join(project_dir, "{}_i{}*".format(self._platform, self._dynamips_id))) + for file in files: + try: + log.debug("Deleting file {}".format(file)) + yield from wait_run_in_executor(os.remove, file) + except OSError as e: + log.warn("Could not delete file {}: {}".format(file, e)) + continue + + @asyncio.coroutine + def clean_delete(self, stop_hypervisor=False): + """ + Deletes this router & associated files (nvram, disks etc.) + """ + + yield from self._hypervisor.send('vm clean_delete "{}"'.format(self._name)) + self._hypervisor.devices.remove(self) + + # if self._startup_config: + # # delete the startup-config + # startup_config_path = os.path.join(self.hypervisor.working_dir, "configs", "{}.cfg".format(self.name)) + # if os.path.isfile(startup_config_path): + # os.remove(startup_config_path) + # + # if self._private_config: + # # delete the private-config + # private_config_path = os.path.join(self.hypervisor.working_dir, "configs", "{}-private.cfg".format(self.name)) + # if os.path.isfile(private_config_path): + # os.remove(private_config_path) + + log.info('Router "{name}" [{id}] has been deleted (including associated files)'.format(name=self._name, id=self._id)) diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index 1bc9280c..973ce58e 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -19,8 +19,8 @@ import aiohttp import os import shutil import asyncio -from uuid import UUID, uuid4 +from uuid import UUID, uuid4 from ..config import Config from ..utils.asyncio import wait_run_in_executor @@ -59,8 +59,6 @@ class Project: self._vms = set() self._vms_to_destroy = set() - self._devices = set() - self.temporary = temporary if path is None: @@ -73,6 +71,15 @@ class Project: log.debug("Create project {id} in directory {path}".format(path=self._path, id=self._id)) + def __json__(self): + + return { + "project_id": self._id, + "location": self._location, + "temporary": self._temporary, + "path": self._path, + } + def _config(self): return Config.instance().get_section_config("Server") @@ -129,11 +136,6 @@ class Project: return self._vms - @property - def devices(self): - - return self._devices - @property def temporary(self): @@ -167,13 +169,29 @@ class Project: if os.path.exists(os.path.join(self._path, ".gns3_temporary")): os.remove(os.path.join(self._path, ".gns3_temporary")) + def module_working_directory(self, module_name): + """ + Return a working directory for the module + If the directory doesn't exist, the directory is created. + + :param module_name: name for the module + :returns: working directory + """ + + workdir = os.path.join(self._path, 'project-files', module_name) + try: + os.makedirs(workdir, exist_ok=True) + except OSError as e: + raise aiohttp.web.HTTPInternalServerError(text="Could not create module working directory: {}".format(e)) + return workdir + def vm_working_directory(self, vm): """ Return a working directory for a specific VM. If the directory doesn't exist, the directory is created. - :param vm: An instance of VM - :returns: A string with a VM working directory + :param vm: VM instance + :returns: VM working directory """ workdir = os.path.join(self._path, 'project-files', vm.manager.module_name.lower(), vm.id) @@ -205,15 +223,6 @@ class Project: self.remove_vm(vm) self._vms_to_destroy.add(vm) - def __json__(self): - - return { - "project_id": self._id, - "location": self._location, - "temporary": self._temporary, - "path": self._path, - } - def add_vm(self, vm): """ Add a VM to the project. @@ -235,27 +244,6 @@ class Project: if vm in self._vms: self._vms.remove(vm) - def add_device(self, device): - """ - Add a device to the project. - In theory this should be called by the VM manager. - - :param device: Device instance - """ - - self._devices.add(device) - - def remove_device(self, device): - """ - Remove a device from the project. - In theory this should be called by the VM manager. - - :param device: Device instance - """ - - if device in self._devices: - self._devices.remove(device) - @asyncio.coroutine def close(self): """Close the project, but keep information on disk""" @@ -277,9 +265,6 @@ class Project: else: vm.close() - for device in self._devices: - tasks.append(asyncio.async(device.delete())) - if tasks: done, _ = yield from asyncio.wait(tasks) for future in done: @@ -300,12 +285,7 @@ class Project: while self._vms_to_destroy: vm = self._vms_to_destroy.pop() - directory = self.vm_working_directory(vm) - if os.path.exists(directory): - try: - yield from wait_run_in_executor(shutil.rmtree, directory) - except OSError as e: - raise aiohttp.web.HTTPInternalServerError(text="Could not delete the project directory: {}".format(e)) + yield from vm.delete() self.remove_vm(vm) @asyncio.coroutine From 605afa1d330f9457ba1ed1c2408e27574aaf56ba Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 16 Feb 2015 10:05:17 +0100 Subject: [PATCH 251/485] Fix bad execption name in IOU --- gns3server/modules/iou/iou_vm.py | 4 ++-- tests/api/test_iou.py | 10 ++++++---- tests/modules/iou/test_iou_vm.py | 9 +++++---- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 5f953638..fb050608 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -848,7 +848,7 @@ class IOUVM(BaseVM): with open(config_file) as f: return f.read() except OSError as e: - raise VPCSError("Can't read configuration file '{}'".format(config_file)) + raise IOUError("Can't read configuration file '{}'".format(config_file)) @initial_config.setter def initial_config(self, initial_config): @@ -867,7 +867,7 @@ class IOUVM(BaseVM): initial_config = initial_config.replace("%h", self._name) f.write(initial_config) except OSError as e: - raise VPCSError("Can't write initial configuration file '{}'".format(self.script_file)) + raise IOUError("Can't write initial configuration file '{}'".format(self.script_file)) @property def initial_config_file(self): diff --git a/tests/api/test_iou.py b/tests/api/test_iou.py index eaffed13..2676c59b 100644 --- a/tests/api/test_iou.py +++ b/tests/api/test_iou.py @@ -49,6 +49,7 @@ def vm(server, project, base_params): def initial_config_file(project, vm): return os.path.join(project.path, "project-files", "iou", vm["vm_id"], "initial-config.cfg") + def test_iou_create(server, project, base_params): response = server.post("/projects/{project_id}/iou/vms".format(project_id=project.id), base_params) assert response.status == 201 @@ -59,7 +60,7 @@ def test_iou_create(server, project, base_params): assert response.json["ethernet_adapters"] == 2 assert response.json["ram"] == 256 assert response.json["nvram"] == 128 - assert response.json["l1_keepalives"] == False + assert response.json["l1_keepalives"] is False def test_iou_create_with_params(server, project, base_params): @@ -80,7 +81,7 @@ def test_iou_create_with_params(server, project, base_params): assert response.json["ethernet_adapters"] == 0 assert response.json["ram"] == 1024 assert response.json["nvram"] == 512 - assert response.json["l1_keepalives"] == True + assert response.json["l1_keepalives"] is True with open(initial_config_file(project, response.json)) as f: assert f.read() == params["initial_config"] @@ -95,7 +96,7 @@ def test_iou_get(server, project, vm): assert response.json["ethernet_adapters"] == 2 assert response.json["ram"] == 256 assert response.json["nvram"] == 128 - assert response.json["l1_keepalives"] == False + assert response.json["l1_keepalives"] is False def test_iou_start(server, vm): @@ -145,10 +146,11 @@ def test_iou_update(server, vm, tmpdir, free_console_port, project): assert response.json["serial_adapters"] == 0 assert response.json["ram"] == 512 assert response.json["nvram"] == 2048 - assert response.json["l1_keepalives"] == True + assert response.json["l1_keepalives"] is True with open(initial_config_file(project, response.json)) as f: assert f.read() == "hostname test" + def test_iou_nio_create_udp(server, vm): response = server.post("/projects/{project_id}/iou/vms/{vm_id}/adapters/1/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", "lport": 4242, diff --git a/tests/modules/iou/test_iou_vm.py b/tests/modules/iou/test_iou_vm.py index 55d80a00..e805c5cc 100644 --- a/tests/modules/iou/test_iou_vm.py +++ b/tests/modules/iou/test_iou_vm.py @@ -70,10 +70,11 @@ def test_vm(project, manager): def test_vm_initial_config(project, manager): - vm = IOUVM("test", "00010203-0405-0607-0808-0a0b0c0d0e0f", project, manager, initial_config="hostname %h") - assert vm.name == "test" - assert vm.initial_config == "hostname test" - assert vm.id == "00010203-0405-0607-0808-0a0b0c0d0e0f" + vm = IOUVM("test", "00010203-0405-0607-0808-0a0b0c0d0e0f", project, manager, initial_config="hostname %h") + assert vm.name == "test" + assert vm.initial_config == "hostname test" + assert vm.id == "00010203-0405-0607-0808-0a0b0c0d0e0f" + @patch("gns3server.config.Config.get_section_config", return_value={"iouyap_path": "/bin/test_fake"}) def test_vm_invalid_iouyap_path(project, manager, loop): From 3ceb43fa62b9320b0d25c7ccb652057835e32609 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 16 Feb 2015 10:11:46 +0100 Subject: [PATCH 252/485] Fix tests --- gns3server/modules/base_vm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index bc4386e6..c6a49069 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -120,7 +120,7 @@ class BaseVM: Delete the VM (including all its files). """ - directory = self.project.vm_working_dir(self) + directory = self.project.vm_working_directory(self) if os.path.exists(directory): try: yield from wait_run_in_executor(shutil.rmtree, directory) From d32323452087a96c252a4a54381c7c488be002e1 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 16 Feb 2015 10:18:03 +0100 Subject: [PATCH 253/485] Harmonisation of slot, adapter notion --- gns3server/modules/iou/iou_vm.py | 70 ++++++++++++++++---------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index fb050608..977fd693 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -738,59 +738,59 @@ class IOUVM(BaseVM): self._slots = self._ethernet_adapters + self._serial_adapters - def slot_add_nio_binding(self, slot_id, port_id, nio): + def slot_add_nio_binding(self, adapter_number, port_number, nio): """ Adds a slot NIO binding. - :param slot_id: slot ID - :param port_id: port ID + :param adapter_number: slot ID + :param port_number: port ID :param nio: NIO instance to add to the slot/port """ try: - adapter = self._slots[slot_id] + adapter = self._slots[adapter_number] except IndexError: - raise IOUError("Slot {slot_id} doesn't exist on IOU {name}".format(name=self._name, - slot_id=slot_id)) - - if not adapter.port_exists(port_id): - raise IOUError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, - port_id=port_id)) - - adapter.add_nio(port_id, nio) - log.info("IOU {name} [id={id}]: {nio} added to {slot_id}/{port_id}".format(name=self._name, - id=self._id, - nio=nio, - slot_id=slot_id, - port_id=port_id)) + raise IOUError("Slot {adapter_number} doesn't exist on IOU {name}".format(name=self._name, + adapter_number=adapter_number)) + + if not adapter.port_exists(port_number): + raise IOUError("Port {port_number} doesn't exist in adapter {adapter}".format(adapter=adapter, + port_number=port_number)) + + adapter.add_nio(port_number, nio) + log.info("IOU {name} [id={id}]: {nio} added to {adapter_number}/{port_number}".format(name=self._name, + id=self._id, + nio=nio, + adapter_number=adapter_number, + port_number=port_number)) if self.is_iouyap_running(): self._update_iouyap_config() os.kill(self._iouyap_process.pid, signal.SIGHUP) - def slot_remove_nio_binding(self, slot_id, port_id): + def slot_remove_nio_binding(self, adapter_number, port_number): """ Removes a slot NIO binding. - :param slot_id: slot ID - :param port_id: port ID + :param adapter_number: slot ID + :param port_number: port ID :returns: NIO instance """ try: - adapter = self._slots[slot_id] + adapter = self._slots[adapter_number] except IndexError: - raise IOUError("Slot {slot_id} doesn't exist on IOU {name}".format(name=self._name, - slot_id=slot_id)) - - if not adapter.port_exists(port_id): - raise IOUError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, - port_id=port_id)) - - nio = adapter.get_nio(port_id) - adapter.remove_nio(port_id) - log.info("IOU {name} [id={id}]: {nio} removed from {slot_id}/{port_id}".format(name=self._name, - id=self._id, - nio=nio, - slot_id=slot_id, - port_id=port_id)) + raise IOUError("Slot {adapter_number} doesn't exist on IOU {name}".format(name=self._name, + adapter_number=adapter_number)) + + if not adapter.port_exists(port_number): + raise IOUError("Port {port_number} doesn't exist in adapter {adapter}".format(adapter=adapter, + port_number=port_number)) + + nio = adapter.get_nio(port_number) + adapter.remove_nio(port_number) + log.info("IOU {name} [id={id}]: {nio} removed from {adapter_number}/{port_number}".format(name=self._name, + id=self._id, + nio=nio, + adapter_number=adapter_number, + port_number=port_number)) if self.is_iouyap_running(): self._update_iouyap_config() os.kill(self._iouyap_process.pid, signal.SIGHUP) From 15f89776d3744e7eb781a72e73344803101ef022 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 16 Feb 2015 17:20:07 +0100 Subject: [PATCH 254/485] All current iou code is async --- gns3server/modules/iou/iou_vm.py | 39 +++++++++++++++++------------- gns3server/modules/vpcs/vpcs_vm.py | 9 ++----- gns3server/utils/asyncio.py | 19 +++++++++++++++ tests/conftest.py | 11 +++++++++ tests/modules/iou/test_iou_vm.py | 35 ++++++++++++++++++++++++--- tests/modules/vpcs/test_vpcs_vm.py | 2 +- tests/test_asyncio.py | 0 tests/utils/test_asyncio.py | 12 ++++++++- 8 files changed, 97 insertions(+), 30 deletions(-) create mode 100644 tests/test_asyncio.py diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 977fd693..6d2caf55 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -22,10 +22,10 @@ order to run an IOU instance. import os import sys -import subprocess import signal import re import asyncio +import subprocess import shutil import argparse import threading @@ -40,6 +40,7 @@ from ..nios.nio_udp import NIO_UDP from ..nios.nio_tap import NIO_TAP from ..base_vm import BaseVM from .ioucon import start_ioucon +import gns3server.utils.asyncio import logging @@ -90,6 +91,7 @@ class IOUVM(BaseVM): self._console_host = console_host # IOU settings + self._iourc = None self._ethernet_adapters = [] self._serial_adapters = [] self.ethernet_adapters = 2 if ethernet_adapters is None else ethernet_adapters # one adapter = 4 interfaces @@ -327,20 +329,21 @@ class IOUVM(BaseVM): def application_id(self): return self._manager.get_application_id(self.id) - # TODO: ASYNCIO + @asyncio.coroutine def _library_check(self): """ Checks for missing shared library dependencies in the IOU image. """ try: - output = subprocess.check_output(["ldd", self._path]) + output = yield from gns3server.utils.asyncio.subprocess_check_output("ldd", self._path) except (FileNotFoundError, subprocess.SubprocessError) as e: - log.warn("could not determine the shared library dependencies for {}: {}".format(self._path, e)) + log.warn("Could not determine the shared library dependencies for {}: {}".format(self._path, e)) return + print(output) p = re.compile("([\.\w]+)\s=>\s+not found") - missing_libs = p.findall(output.decode("utf-8")) + missing_libs = p.findall(output) if missing_libs: raise IOUError("The following shared library dependencies cannot be found for IOU image {}: {}".format(self._path, ", ".join(missing_libs))) @@ -354,10 +357,9 @@ class IOUVM(BaseVM): self._check_requirements() if not self.is_running(): - self._rename_nvram_file() + yield from self._library_check() - # TODO: ASYNC - # self._library_check() + self._rename_nvram_file() if self._iourc_path and not os.path.isfile(self._iourc_path): raise IOUError("A valid iourc file is necessary to start IOU") @@ -371,7 +373,7 @@ class IOUVM(BaseVM): env = os.environ.copy() if self._iourc_path: env["IOURC"] = self._iourc_path - self._command = self._build_command() + self._command = yield from self._build_command() try: log.info("Starting IOU: {}".format(self._command)) self._iou_stdout_file = os.path.join(self.working_dir, "iou.log") @@ -394,7 +396,7 @@ class IOUVM(BaseVM): # start console support self._start_ioucon() # connections support - self._start_iouyap() + yield from self._start_iouyap() def _rename_nvram_file(self): """ @@ -405,6 +407,7 @@ class IOUVM(BaseVM): for file_path in glob.glob(os.path.join(self.working_dir, "nvram_*")): shutil.move(file_path, destination) + @asyncio.coroutine def _start_iouyap(self): """ Starts iouyap (handles connections to and from this IOU device). @@ -417,10 +420,10 @@ class IOUVM(BaseVM): self._iouyap_stdout_file = os.path.join(self.working_dir, "iouyap.log") log.info("logging to {}".format(self._iouyap_stdout_file)) with open(self._iouyap_stdout_file, "w") as fd: - self._iouyap_process = subprocess.Popen(command, - stdout=fd, - stderr=subprocess.STDOUT, - cwd=self.working_dir) + self._iouyap_process = yield from asyncio.create_subprocess_exec(*command, + stdout=fd, + stderr=subprocess.STDOUT, + cwd=self.working_dir) log.info("iouyap started PID={}".format(self._iouyap_process.pid)) except (OSError, subprocess.SubprocessError) as e: @@ -596,6 +599,7 @@ class IOUVM(BaseVM): except OSError as e: raise IOUError("Could not create {}: {}".format(netmap_path, e)) + @asyncio.coroutine def _build_command(self): """ Command to start the IOU process. @@ -639,7 +643,7 @@ class IOUVM(BaseVM): if initial_config_file: command.extend(["-c", initial_config_file]) if self._l1_keepalives: - self._enable_l1_keepalives(command) + yield from self._enable_l1_keepalives(command) command.extend([str(self.application_id)]) return command @@ -819,6 +823,7 @@ class IOUVM(BaseVM): else: log.info("IOU {name} [id={id}]: has deactivated layer 1 keepalive messages".format(name=self._name, id=self._id)) + @asyncio.coroutine def _enable_l1_keepalives(self, command): """ Enables L1 keepalive messages if supported. @@ -828,8 +833,8 @@ class IOUVM(BaseVM): env = os.environ.copy() env["IOURC"] = self._iourc try: - output = subprocess.check_output([self._path, "-h"], stderr=subprocess.STDOUT, cwd=self._working_dir, env=env) - if re.search("-l\s+Enable Layer 1 keepalive messages", output.decode("utf-8")): + output = yield from gns3server.utils.asyncio.subprocess_check_output(self._path, "-h", cwd=self.working_dir, env=env) + if re.search("-l\s+Enable Layer 1 keepalive messages", output): command.extend(["-l"]) else: raise IOUError("layer 1 keepalive messages are not supported by {}".format(os.path.basename(self._path))) diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index 1d1678d3..1b85bf1f 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -32,6 +32,7 @@ from pkg_resources import parse_version from .vpcs_error import VPCSError from ..adapters.ethernet_adapter import EthernetAdapter from ..base_vm import BaseVM +from ...utils.asyncio import subprocess_check_output import logging @@ -195,7 +196,7 @@ class VPCSVM(BaseVM): Checks if the VPCS executable version is >= 0.5b1. """ try: - output = yield from self._get_vpcs_welcome() + output = yield from subprocess_check_output(self.vpcs_path, "-v", cwd=self.working_dir) match = re.search("Welcome to Virtual PC Simulator, version ([0-9a-z\.]+)", output) if match: version = match.group(1) @@ -206,12 +207,6 @@ class VPCSVM(BaseVM): except (OSError, subprocess.SubprocessError) as e: raise VPCSError("Error while looking for the VPCS version: {}".format(e)) - @asyncio.coroutine - def _get_vpcs_welcome(self): - proc = yield from asyncio.create_subprocess_exec(self.vpcs_path, "-v", stdout=asyncio.subprocess.PIPE, cwd=self.working_dir) - out = yield from proc.stdout.read() - return out.decode("utf-8") - @asyncio.coroutine def start(self): """ diff --git a/gns3server/utils/asyncio.py b/gns3server/utils/asyncio.py index 0554593a..a627cb3e 100644 --- a/gns3server/utils/asyncio.py +++ b/gns3server/utils/asyncio.py @@ -17,6 +17,7 @@ import asyncio +import shutil @asyncio.coroutine @@ -34,3 +35,21 @@ def wait_run_in_executor(func, *args): future = loop.run_in_executor(None, func, *args) yield from asyncio.wait([future]) return future.result() + + +@asyncio.coroutine +def subprocess_check_output(*args, working_dir=None, env=None): + """ + Run a command and capture output + + :param *args: List of command arguments + :param working_dir: Working directory + :param env: Command environment + :returns: Command output + """ + + proc = yield from asyncio.create_subprocess_exec(*args, stdout=asyncio.subprocess.PIPE, cwd=working_dir, env=env) + output = yield from proc.stdout.read() + if output is None: + return "" + return output.decode("utf-8") diff --git a/tests/conftest.py b/tests/conftest.py index d7e4da6c..cf146e81 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -37,6 +37,17 @@ from tests.api.base import Query os.environ["PATH"] = tempfile.mkdtemp() +@pytest.yield_fixture +def restore_original_path(): + """ + Temporary restore a standard path environnement. This allow + to run external binaries. + """ + os.environ["PATH"] = "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" + yield + os.environ["PATH"] = tempfile.mkdtemp() + + @pytest.fixture(scope="session") def loop(request): """Return an event loop and destroy it at the end of test""" diff --git a/tests/modules/iou/test_iou_vm.py b/tests/modules/iou/test_iou_vm.py index e805c5cc..46aed183 100644 --- a/tests/modules/iou/test_iou_vm.py +++ b/tests/modules/iou/test_iou_vm.py @@ -184,18 +184,18 @@ def test_create_netmap_config(vm): assert "513:15/3 1:15/3" in content -def test_build_command(vm): +def test_build_command(vm, loop): - assert vm._build_command() == [vm.path, "-L", str(vm.application_id)] + assert loop.run_until_complete(asyncio.async(vm._build_command())) == [vm.path, "-L", str(vm.application_id)] -def test_build_command_initial_config(vm): +def test_build_command_initial_config(vm, loop): filepath = os.path.join(vm.working_dir, "initial-config.cfg") with open(filepath, "w+") as f: f.write("service timestamps debug datetime msec\nservice timestamps log datetime msec\nno service password-encryption") - assert vm._build_command() == [vm.path, "-L", "-c", vm.initial_config_file, str(vm.application_id)] + assert loop.run_until_complete(asyncio.async(vm._build_command())) == [vm.path, "-L", "-c", vm.initial_config_file, str(vm.application_id)] def test_get_initial_config(vm): @@ -231,3 +231,30 @@ def test_change_name(vm, tmpdir): assert vm.name == "hello" with open(path) as f: assert f.read() == "hostname hello" + + +def test_library_check(loop, vm): + + with asyncio_patch("gns3server.utils.asyncio.subprocess_check_output", return_value="") as mock: + + loop.run_until_complete(asyncio.async(vm._library_check())) + + with asyncio_patch("gns3server.utils.asyncio.subprocess_check_output", return_value="libssl => not found") as mock: + with pytest.raises(IOUError): + loop.run_until_complete(asyncio.async(vm._library_check())) + + +def test_enable_l1_keepalives(loop, vm): + + with asyncio_patch("gns3server.utils.asyncio.subprocess_check_output", return_value="***************************************************************\n\n-l Enable Layer 1 keepalive messages\n-u UDP port base for distributed networks\n") as mock: + + command = ["test"] + loop.run_until_complete(asyncio.async(vm._enable_l1_keepalives(command))) + assert command == ["test", "-l"] + + with asyncio_patch("gns3server.utils.asyncio.subprocess_check_output", return_value="***************************************************************\n\n-u UDP port base for distributed networks\n") as mock: + + command = ["test"] + with pytest.raises(IOUError): + loop.run_until_complete(asyncio.async(vm._enable_l1_keepalives(command))) + assert command == ["test"] diff --git a/tests/modules/vpcs/test_vpcs_vm.py b/tests/modules/vpcs/test_vpcs_vm.py index 2848139f..01b77e12 100644 --- a/tests/modules/vpcs/test_vpcs_vm.py +++ b/tests/modules/vpcs/test_vpcs_vm.py @@ -47,7 +47,7 @@ def test_vm(project, manager): def test_vm_invalid_vpcs_version(loop, project, manager): - with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM._get_vpcs_welcome", return_value="Welcome to Virtual PC Simulator, version 0.1"): + with asyncio_patch("gns3server.utils.asyncio.subprocess_check_output", return_value="Welcome to Virtual PC Simulator, version 0.1"): with pytest.raises(VPCSError): vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) nio = manager.create_nio(vm.vpcs_path, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) diff --git a/tests/test_asyncio.py b/tests/test_asyncio.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/utils/test_asyncio.py b/tests/utils/test_asyncio.py index a6a9cc3e..38a1795f 100644 --- a/tests/utils/test_asyncio.py +++ b/tests/utils/test_asyncio.py @@ -19,7 +19,7 @@ import asyncio import pytest -from gns3server.utils.asyncio import wait_run_in_executor +from gns3server.utils.asyncio import wait_run_in_executor, subprocess_check_output def test_wait_run_in_executor(loop): @@ -40,3 +40,13 @@ def test_exception_wait_run_in_executor(loop): exec = wait_run_in_executor(raise_exception) with pytest.raises(Exception): result = loop.run_until_complete(asyncio.async(exec)) + + +def test_subprocess_check_output(loop, tmpdir, restore_original_path): + + path = str(tmpdir / "test") + with open(path, "w+") as f: + f.write("TEST") + exec = subprocess_check_output("cat", path) + result = loop.run_until_complete(asyncio.async(exec)) + assert result == "TEST" From 018e3c145176ba226f90460c85145aed1fc204f6 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 16 Feb 2015 17:40:13 +0100 Subject: [PATCH 255/485] Fix IOU closing --- gns3server/modules/iou/iou_vm.py | 16 +++++++++------- gns3server/modules/iou/ioucon.py | 1 + gns3server/modules/vpcs/vpcs_vm.py | 1 + 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 6d2caf55..b3e6c855 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -110,8 +110,10 @@ class IOUVM(BaseVM): else: self._console = self._manager.port_manager.get_free_console_port() + @asyncio.coroutine def close(self): + yield from self.stop() if self._console: self._manager.port_manager.release_console_port(self._console) self._console = None @@ -498,14 +500,14 @@ class IOUVM(BaseVM): Stops the IOU process. """ - # stop console support - if self._ioucon_thread: - self._ioucon_thread_stop_event.set() - if self._ioucon_thread.is_alive(): - self._ioucon_thread.join(timeout=3.0) # wait for the thread to free the console port - self._ioucon_thread = None - if self.is_running(): + # stop console support + if self._ioucon_thread: + self._ioucon_thread_stop_event.set() + if self._ioucon_thread.is_alive(): + self._ioucon_thread.join(timeout=3.0) # wait for the thread to free the console port + self._ioucon_thread = None + self._terminate_process_iou() try: yield from asyncio.wait_for(self._iou_process.wait(), timeout=3) diff --git a/gns3server/modules/iou/ioucon.py b/gns3server/modules/iou/ioucon.py index 6dbd782d..d4889433 100644 --- a/gns3server/modules/iou/ioucon.py +++ b/gns3server/modules/iou/ioucon.py @@ -357,6 +357,7 @@ class TelnetServer(Console): sock_fd.listen(socket.SOMAXCONN) self.sock_fd = sock_fd log.info("Telnet server ready for connections on {}:{}".format(self.addr, self.port)) + log.info(self.stop_event.is_set()) return self diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index 1b85bf1f..0d0bab91 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -73,6 +73,7 @@ class VPCSVM(BaseVM): else: self._console = self._manager.port_manager.get_free_console_port() + @asyncio.coroutine def close(self): self._terminate_process() From ff7f014423f1b2ebacb2ac133c5b363500f56d37 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 16 Feb 2015 19:14:45 +0100 Subject: [PATCH 256/485] Fix test --- tests/modules/iou/test_iou_vm.py | 4 ++-- tests/modules/vpcs/test_vpcs_vm.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/modules/iou/test_iou_vm.py b/tests/modules/iou/test_iou_vm.py index 46aed183..0ef6ed98 100644 --- a/tests/modules/iou/test_iou_vm.py +++ b/tests/modules/iou/test_iou_vm.py @@ -142,12 +142,12 @@ def test_reload(loop, vm, fake_iou_bin): process.terminate.assert_called_with() -def test_close(vm, port_manager): +def test_close(vm, port_manager, loop): with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._check_requirements", return_value=True): with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): vm.start() port = vm.console - vm.close() + loop.run_until_complete(asyncio.async(vm.close())) # Raise an exception if the port is not free port_manager.reserve_console_port(port) assert vm.is_running() is False diff --git a/tests/modules/vpcs/test_vpcs_vm.py b/tests/modules/vpcs/test_vpcs_vm.py index 01b77e12..990a386e 100644 --- a/tests/modules/vpcs/test_vpcs_vm.py +++ b/tests/modules/vpcs/test_vpcs_vm.py @@ -212,12 +212,12 @@ def test_change_name(vm, tmpdir): assert f.read() == "name hello" -def test_close(vm, port_manager): +def test_close(vm, port_manager, loop): with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM._check_requirements", return_value=True): with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): vm.start() port = vm.console - vm.close() + loop.run_until_complete(asyncio.async(vm.close())) # Raise an exception if the port is not free port_manager.reserve_console_port(port) assert vm.is_running() is False From 6c3a926ce3b840ac84ec689079f871a6c3e99006 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 16 Feb 2015 20:08:04 +0100 Subject: [PATCH 257/485] Capture is OK on server side --- gns3server/handlers/iou_handler.py | 56 +++++++++++++++- gns3server/modules/iou/iou_vm.py | 102 ++++++++++++++++++++++++----- gns3server/modules/nios/nio.py | 20 ++++-- gns3server/schemas/iou.py | 24 +++++-- tests/api/test_iou.py | 22 +++++++ tests/modules/iou/test_iou_vm.py | 20 ++++++ 6 files changed, 217 insertions(+), 27 deletions(-) diff --git a/gns3server/handlers/iou_handler.py b/gns3server/handlers/iou_handler.py index 04177f73..356b72ce 100644 --- a/gns3server/handlers/iou_handler.py +++ b/gns3server/handlers/iou_handler.py @@ -15,12 +15,16 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os + + from ..web.route import Route from ..modules.port_manager import PortManager from ..schemas.iou import IOU_CREATE_SCHEMA from ..schemas.iou import IOU_UPDATE_SCHEMA from ..schemas.iou import IOU_OBJECT_SCHEMA from ..schemas.iou import IOU_NIO_SCHEMA +from ..schemas.iou import IOU_CAPTURE_SCHEMA from ..modules.iou import IOU @@ -216,7 +220,7 @@ class IOUHandler: iou_manager = IOU.instance() vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) nio = iou_manager.create_nio(vm.iouyap_path, request.json) - vm.slot_add_nio_binding(int(request.match_info["adapter_number"]), int(request.match_info["port_number"]), nio) + vm.adapter_add_nio_binding(int(request.match_info["adapter_number"]), int(request.match_info["port_number"]), nio) response.set_status(201) response.json(nio) @@ -239,5 +243,53 @@ class IOUHandler: iou_manager = IOU.instance() vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) - vm.slot_remove_nio_binding(int(request.match_info["adapter_number"]), int(request.match_info["port_number"])) + vm.adapter_remove_nio_binding(int(request.match_info["adapter_number"]), int(request.match_info["port_number"])) + response.set_status(204) + + @Route.post( + r"/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/start_capture", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance", + "adapter_number": "Adapter to start a packet capture", + "port_number": "Port on the adapter" + }, + status_codes={ + 200: "Capture started", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Start a packet capture on a IOU VM instance", + input=IOU_CAPTURE_SCHEMA) + def start_capture(request, response): + + iou_manager = IOU.instance() + vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + adapter_number = int(request.match_info["adapter_number"]) + port_number = int(request.match_info["port_number"]) + pcap_file_path = os.path.join(vm.project.capture_working_directory(), request.json["capture_file_name"]) + yield from vm.start_capture(adapter_number, port_number, pcap_file_path, request.json["data_link_type"]) + response.json({"pcap_file_path": pcap_file_path}) + + @Route.post( + r"/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/stop_capture", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance", + "adapter_number": "Adapter to stop a packet capture", + "port_number": "Port on the adapter (always 0)" + }, + status_codes={ + 204: "Capture stopped", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Stop a packet capture on a IOU VM instance") + def stop_capture(request, response): + + iou_manager = IOU.instance() + vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + adapter_number = int(request.match_info["adapter_number"]) + port_number = int(request.match_info["port_number"]) + yield from vm.stop_capture(adapter_number, port_number) response.set_status(204) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index b3e6c855..73a919c8 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -445,7 +445,7 @@ class IOUVM(BaseVM): "base_port": "49000"} bay_id = 0 - for adapter in self._slots: + for adapter in self._adapters: unit_id = 0 for unit in adapter.ports.keys(): nio = adapter.get_nio(unit) @@ -716,7 +716,7 @@ class IOUVM(BaseVM): id=self._id, adapters=len(self._ethernet_adapters))) - self._slots = self._ethernet_adapters + self._serial_adapters + self._adapters = self._ethernet_adapters + self._serial_adapters @property def serial_adapters(self): @@ -742,21 +742,21 @@ class IOUVM(BaseVM): id=self._id, adapters=len(self._serial_adapters))) - self._slots = self._ethernet_adapters + self._serial_adapters + self._adapters = self._ethernet_adapters + self._serial_adapters - def slot_add_nio_binding(self, adapter_number, port_number, nio): + def adapter_add_nio_binding(self, adapter_number, port_number, nio): """ - Adds a slot NIO binding. - :param adapter_number: slot ID + Adds a adapter NIO binding. + :param adapter_number: adapter ID :param port_number: port ID - :param nio: NIO instance to add to the slot/port + :param nio: NIO instance to add to the adapter/port """ try: - adapter = self._slots[adapter_number] + adapter = self._adapters[adapter_number] except IndexError: - raise IOUError("Slot {adapter_number} doesn't exist on IOU {name}".format(name=self._name, - adapter_number=adapter_number)) + raise IOUError("Adapter {adapter_number} doesn't exist on IOU {name}".format(name=self._name, + adapter_number=adapter_number)) if not adapter.port_exists(port_number): raise IOUError("Port {port_number} doesn't exist in adapter {adapter}".format(adapter=adapter, @@ -772,19 +772,19 @@ class IOUVM(BaseVM): self._update_iouyap_config() os.kill(self._iouyap_process.pid, signal.SIGHUP) - def slot_remove_nio_binding(self, adapter_number, port_number): + def adapter_remove_nio_binding(self, adapter_number, port_number): """ - Removes a slot NIO binding. - :param adapter_number: slot ID + Removes a adapter NIO binding. + :param adapter_number: adapter ID :param port_number: port ID :returns: NIO instance """ try: - adapter = self._slots[adapter_number] + adapter = self._adapters[adapter_number] except IndexError: - raise IOUError("Slot {adapter_number} doesn't exist on IOU {name}".format(name=self._name, - adapter_number=adapter_number)) + raise IOUError("Adapter {adapter_number} doesn't exist on IOU {name}".format(name=self._name, + adapter_number=adapter_number)) if not adapter.port_exists(port_number): raise IOUError("Port {port_number} doesn't exist in adapter {adapter}".format(adapter=adapter, @@ -889,3 +889,73 @@ class IOUVM(BaseVM): return path else: return None + + def start_capture(self, adapter_number, port_number, output_file, data_link_type="DLT_EN10MB"): + """ + Starts a packet capture. + :param adapter_number: adapter ID + :param port_number: port ID + :param port: allocated port + :param output_file: PCAP destination file for the capture + :param data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB + """ + + try: + adapter = self._adapters[adapter_number] + except IndexError: + raise IOUError("Adapter {adapter_number} doesn't exist on IOU {name}".format(name=self._name, + adapter_number=adapter_number)) + + if not adapter.port_exists(port_number): + raise IOUError("Port {port_number} doesn't exist in adapter {adapter}".format(adapter=adapter, + port_number=port_number)) + + nio = adapter.get_nio(port_number) + if nio.capturing: + raise IOUError("Packet capture is already activated on {adapter_number}/{port_number}".format(adapter_number=adapter_number, + port_number=port_number)) + + try: + os.makedirs(os.path.dirname(output_file)) + except FileExistsError: + pass + except OSError as e: + raise IOUError("Could not create captures directory {}".format(e)) + + nio.startPacketCapture(output_file, data_link_type) + + log.info("IOU {name} [id={id}]: starting packet capture on {adapter_number}/{port_number}".format(name=self._name, + id=self._id, + adapter_number=adapter_number, + port_number=port_number)) + + if self.is_iouyap_running(): + self._update_iouyap_config() + os.kill(self._iouyap_process.pid, signal.SIGHUP) + + def stop_capture(self, adapter_number, port_number): + """ + Stops a packet capture. + :param adapter_number: adapter ID + :param port_number: port ID + """ + + try: + adapter = self._adapters[adapter_number] + except IndexError: + raise IOUError("Adapter {adapter_number} doesn't exist on IOU {name}".format(name=self._name, + adapter_number=adapter_number)) + + if not adapter.port_exists(port_number): + raise IOUError("Port {port_number} doesn't exist in adapter {adapter}".format(adapter=adapter, + port_number=port_number)) + + nio = adapter.get_nio(port_number) + nio.stopPacketCapture() + log.info("IOU {name} [id={id}]: stopping packet capture on {adapter_number}/{port_number}".format(name=self._name, + id=self._id, + adapter_number=adapter_number, + port_number=port_number)) + if self.is_iouyap_running(): + self._update_iouyap_config() + os.kill(self._iouyap_process.pid, signal.SIGHUP) diff --git a/gns3server/modules/nios/nio.py b/gns3server/modules/nios/nio.py index 3c8a6b9e..b1ab24ae 100644 --- a/gns3server/modules/nios/nio.py +++ b/gns3server/modules/nios/nio.py @@ -23,33 +23,35 @@ Base interface for NIOs. class NIO(object): """ - Network Input/Output. + IOU NIO. """ def __init__(self): self._capturing = False self._pcap_output_file = "" + self._pcap_data_link_type = "" - def startPacketCapture(self, pcap_output_file): + def startPacketCapture(self, pcap_output_file, pcap_data_link_type="DLT_EN10MB"): """ - :param pcap_output_file: PCAP destination file for the capture + :param pcap_data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB """ self._capturing = True self._pcap_output_file = pcap_output_file + self._pcap_data_link_type = pcap_data_link_type def stopPacketCapture(self): self._capturing = False self._pcap_output_file = "" + self._pcap_data_link_type = "" @property def capturing(self): """ Returns either a capture is configured on this NIO. - :returns: boolean """ @@ -59,8 +61,16 @@ class NIO(object): def pcap_output_file(self): """ Returns the path to the PCAP output file. - :returns: path to the PCAP output file """ return self._pcap_output_file + + @property + def pcap_data_link_type(self): + """ + Returns the PCAP data link type + :returns: PCAP data link type (DLT_* value) + """ + + return self._pcap_data_link_type diff --git a/gns3server/schemas/iou.py b/gns3server/schemas/iou.py index 857208c3..3e7234d2 100644 --- a/gns3server/schemas/iou.py +++ b/gns3server/schemas/iou.py @@ -103,10 +103,6 @@ IOU_UPDATE_SCHEMA = { "description": "Path of iourc", "type": ["string", "null"] }, - "initial_config": { - "description": "Initial configuration path", - "type": ["string", "null"] - }, "serial_adapters": { "description": "How many serial adapters are connected to the IOU", "type": ["integer", "null"] @@ -265,3 +261,23 @@ IOU_NIO_SCHEMA = { "additionalProperties": True, "required": ["type"] } + +IOU_CAPTURE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to start a packet capture on a IOU instance", + "type": "object", + "properties": { + "capture_file_name": { + "description": "Capture file name", + "type": "string", + "minLength": 1, + }, + "data_link_type": { + "description": "PCAP data link type", + "type": "string", + "minLength": 1, + }, + }, + "additionalProperties": False, + "required": ["capture_file_name", "data_link_type"] +} diff --git a/tests/api/test_iou.py b/tests/api/test_iou.py index 2676c59b..1c913592 100644 --- a/tests/api/test_iou.py +++ b/tests/api/test_iou.py @@ -201,3 +201,25 @@ def test_iou_delete_nio(server, vm): response = server.delete("/projects/{project_id}/iou/vms/{vm_id}/adapters/1/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert response.status == 204 assert response.route == "/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" + + +def test_iou_start_capture(server, vm, tmpdir): + + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM.start_capture", return_value=True) as mock: + + params = {"capture_file_name": "test.pcap", "data_link_type": "DLT_EN10MB"} + response = server.post("/projects/{project_id}/iou/vms/{vm_id}/adapters/0/ports/0/start_capture".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), body=params) + + assert mock.called + assert response.status == 200 + assert "test.pcap" in response.json["pcap_file_path"] + + +def test_iou_stop_capture(server, vm, tmpdir): + + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM.stop_capture", return_value=True) as mock: + + response = server.post("/projects/{project_id}/iou/vms/{vm_id}/adapters/0/ports/0/stop_capture".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + + assert mock.called + assert response.status == 204 diff --git a/tests/modules/iou/test_iou_vm.py b/tests/modules/iou/test_iou_vm.py index 0ef6ed98..137bd984 100644 --- a/tests/modules/iou/test_iou_vm.py +++ b/tests/modules/iou/test_iou_vm.py @@ -258,3 +258,23 @@ def test_enable_l1_keepalives(loop, vm): with pytest.raises(IOUError): loop.run_until_complete(asyncio.async(vm._enable_l1_keepalives(command))) assert command == ["test"] + + +def test_start_capture(vm, tmpdir, manager, free_console_port): + + output_file = str(tmpdir / "test.pcap") + nio = manager.create_nio(vm.iouyap_path, {"type": "nio_udp", "lport": free_console_port, "rport": free_console_port, "rhost": "192.168.1.2"}) + vm.adapter_add_nio_binding(0, 0, nio) + vm.start_capture(0, 0, output_file) + assert vm._adapters[0].get_nio(0).capturing + + +def test_stop_capture(vm, tmpdir, manager, free_console_port): + + output_file = str(tmpdir / "test.pcap") + nio = manager.create_nio(vm.iouyap_path, {"type": "nio_udp", "lport": free_console_port, "rport": free_console_port, "rhost": "192.168.1.2"}) + vm.adapter_add_nio_binding(0, 0, nio) + vm.start_capture(0, 0, output_file) + assert vm._adapters[0].get_nio(0).capturing + vm.stop_capture(0, 0) + assert vm._adapters[0].get_nio(0).capturing == False From 3e95bb9748c5dcb2e4eeba4aa92137c9ce37c708 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 16 Feb 2015 16:53:50 -0700 Subject: [PATCH 258/485] Adapter settings and configs for Dynamips VMs. --- .../handlers/dynamips_device_handler.py | 2 +- gns3server/handlers/dynamips_vm_handler.py | 25 +- gns3server/handlers/virtualbox_handler.py | 2 +- gns3server/modules/dynamips/__init__.py | 199 +++++++---- gns3server/modules/dynamips/nodes/router.py | 312 +++++++++--------- gns3server/schemas/dynamips_vm.py | 8 + 6 files changed, 300 insertions(+), 248 deletions(-) diff --git a/gns3server/handlers/dynamips_device_handler.py b/gns3server/handlers/dynamips_device_handler.py index 11c3392c..4be6be4c 100644 --- a/gns3server/handlers/dynamips_device_handler.py +++ b/gns3server/handlers/dynamips_device_handler.py @@ -220,7 +220,7 @@ class DynamipsDeviceHandler: 404: "Instance doesn't exist" }, description="Stop a packet capture on a Dynamips device instance") - def start_capture(request, response): + def stop_capture(request, response): dynamips_manager = Dynamips.instance() device = dynamips_manager.get_device(request.match_info["device_id"], project_id=request.match_info["project_id"]) diff --git a/gns3server/handlers/dynamips_vm_handler.py b/gns3server/handlers/dynamips_vm_handler.py index 44d9a707..f88c9297 100644 --- a/gns3server/handlers/dynamips_vm_handler.py +++ b/gns3server/handlers/dynamips_vm_handler.py @@ -57,17 +57,11 @@ class DynamipsVMHandler: request.json.get("dynamips_id"), request.json.pop("platform")) - # set VM settings - for name, value in request.json.items(): - if hasattr(vm, name) and getattr(vm, name) != value: - if hasattr(vm, "set_{}".format(name)): - setter = getattr(vm, "set_{}".format(name)) - if asyncio.iscoroutinefunction(vm.close): - yield from setter(value) - else: - setter(value) - + yield from dynamips_manager.update_vm_settings(vm, request.json) yield from dynamips_manager.ghost_ios_support(vm) + yield from dynamips_manager.create_vm_configs(vm, + request.json.get("startup_config_content"), + request.json.get("private_config_content")) response.set_status(201) response.json(vm) @@ -112,14 +106,7 @@ class DynamipsVMHandler: dynamips_manager = Dynamips.instance() vm = dynamips_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) - # set VM settings - for name, value in request.json.items(): - if hasattr(vm, name) and getattr(vm, name) != value: - setter = getattr(vm, "set_{}".format(name)) - if asyncio.iscoroutinefunction(vm.close): - yield from setter(value) - else: - setter(value) + yield from dynamips_manager.update_vm_settings(vm, request.json) response.json(vm) @classmethod @@ -333,7 +320,7 @@ class DynamipsVMHandler: 404: "Instance doesn't exist" }, description="Stop a packet capture on a Dynamips VM instance") - def start_capture(request, response): + def stop_capture(request, response): dynamips_manager = Dynamips.instance() vm = dynamips_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) diff --git a/gns3server/handlers/virtualbox_handler.py b/gns3server/handlers/virtualbox_handler.py index ae8d296e..51768bd3 100644 --- a/gns3server/handlers/virtualbox_handler.py +++ b/gns3server/handlers/virtualbox_handler.py @@ -336,7 +336,7 @@ class VirtualBoxHandler: 404: "Instance doesn't exist" }, description="Stop a packet capture on a VirtualBox VM instance") - def start_capture(request, response): + def stop_capture(request, response): vbox_manager = VirtualBox.instance() vm = vbox_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index 579d8bc3..d76e77ee 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -56,6 +56,51 @@ from .nios.nio_fifo import NIOFIFO from .nios.nio_mcast import NIOMcast from .nios.nio_null import NIONull +# Adapters +from .adapters.c7200_io_2fe import C7200_IO_2FE +from .adapters.c7200_io_fe import C7200_IO_FE +from .adapters.c7200_io_ge_e import C7200_IO_GE_E +from .adapters.nm_16esw import NM_16ESW +from .adapters.nm_1e import NM_1E +from .adapters.nm_1fe_tx import NM_1FE_TX +from .adapters.nm_4e import NM_4E +from .adapters.nm_4t import NM_4T +from .adapters.pa_2fe_tx import PA_2FE_TX +from .adapters.pa_4e import PA_4E +from .adapters.pa_4t import PA_4T +from .adapters.pa_8e import PA_8E +from .adapters.pa_8t import PA_8T +from .adapters.pa_a1 import PA_A1 +from .adapters.pa_fe_tx import PA_FE_TX +from .adapters.pa_ge import PA_GE +from .adapters.pa_pos_oc3 import PA_POS_OC3 +from .adapters.wic_1enet import WIC_1ENET +from .adapters.wic_1t import WIC_1T +from .adapters.wic_2t import WIC_2T + + +ADAPTER_MATRIX = {"C7200-IO-2FE": C7200_IO_2FE, + "C7200-IO-FE": C7200_IO_FE, + "C7200-IO-GE-E": C7200_IO_GE_E, + "NM-16ESW": NM_16ESW, + "NM-1E": NM_1E, + "NM-1FE-TX": NM_1FE_TX, + "NM-4E": NM_4E, + "NM-4T": NM_4T, + "PA-2FE-TX": PA_2FE_TX, + "PA-4E": PA_4E, + "PA-4T+": PA_4T, + "PA-8E": PA_8E, + "PA-8T": PA_8T, + "PA-A1": PA_A1, + "PA-FE-TX": PA_FE_TX, + "PA-GE": PA_GE, + "PA-POS-OC3": PA_POS_OC3} + +WIC_MATRIX = {"WIC-1ENET": WIC_1ENET, + "WIC-1T": WIC_1T, + "WIC-2T": WIC_2T} + class Dynamips(BaseManager): @@ -391,67 +436,93 @@ class Dynamips(BaseManager): # set the ghost file to the router yield from vm.set_ghost_status(2) yield from vm.set_ghost_file(ghost_file) -# -# def create_config_from_file(self, local_base_config, router, destination_config_path): -# """ -# Creates a config file from a local base config -# -# :param local_base_config: path the a local base config -# :param router: router instance -# :param destination_config_path: path to the destination config file -# -# :returns: relative path to the created config file -# """ -# -# log.info("creating config file {} from {}".format(destination_config_path, local_base_config)) -# config_path = destination_config_path -# config_dir = os.path.dirname(destination_config_path) -# try: -# os.makedirs(config_dir) -# except FileExistsError: -# pass -# except OSError as e: -# raise DynamipsError("Could not create configs directory: {}".format(e)) -# -# try: -# with open(local_base_config, "r", errors="replace") as f: -# config = f.read() -# with open(config_path, "w") as f: -# config = "!\n" + config.replace("\r", "") -# config = config.replace('%h', router.name) -# f.write(config) -# except OSError as e: -# raise DynamipsError("Could not save the configuration from {} to {}: {}".format(local_base_config, config_path, e)) -# return "configs" + os.sep + os.path.basename(config_path) -# -# def create_config_from_base64(self, config_base64, router, destination_config_path): -# """ -# Creates a config file from a base64 encoded config. -# -# :param config_base64: base64 encoded config -# :param router: router instance -# :param destination_config_path: path to the destination config file -# -# :returns: relative path to the created config file -# """ -# -# log.info("creating config file {} from base64".format(destination_config_path)) -# config = base64.decodebytes(config_base64.encode("utf-8")).decode("utf-8") -# config = "!\n" + config.replace("\r", "") -# config = config.replace('%h', router.name) -# config_dir = os.path.dirname(destination_config_path) -# try: -# os.makedirs(config_dir) -# except FileExistsError: -# pass -# except OSError as e: -# raise DynamipsError("Could not create configs directory: {}".format(e)) -# -# config_path = destination_config_path -# try: -# with open(config_path, "w") as f: -# log.info("saving startup-config to {}".format(config_path)) -# f.write(config) -# except OSError as e: -# raise DynamipsError("Could not save the configuration {}: {}".format(config_path, e)) -# return "configs" + os.sep + os.path.basename(config_path) + + @asyncio.coroutine + def update_vm_settings(self, vm, settings): + """ + Updates the VM settings. + + :param vm: VM instance + :param settings: settings to update (dict) + """ + + for name, value in settings.items(): + if hasattr(vm, name) and getattr(vm, name) != value: + if hasattr(vm, "set_{}".format(name)): + setter = getattr(vm, "set_{}".format(name)) + if asyncio.iscoroutinefunction(vm.close): + yield from setter(value) + else: + setter(value) + elif name.startswith("slot") and value in ADAPTER_MATRIX: + slot_id = int(name[-1]) + adapter_name = value + adapter = ADAPTER_MATRIX[adapter_name]() + if vm.slots[slot_id] and type(vm.slots[slot_id]) != type(adapter): + yield from vm.slot_remove_binding(slot_id) + yield from vm.slot_add_binding(slot_id, adapter) + elif name.startswith("slot") and value is None: + slot_id = int(name[-1]) + if vm.slots[slot_id]: + yield from vm.slot_remove_binding(slot_id) + elif name.startswith("wic") and value in WIC_MATRIX: + wic_slot_id = int(name[-1]) + wic_name = value + wic = WIC_MATRIX[wic_name]() + if vm.slots[0].wics[wic_slot_id] and type(vm.slots[0].wics[wic_slot_id]) != type(wic): + yield from vm.uninstall_wic(wic_slot_id) + yield from vm.install_wic(wic_slot_id, wic) + elif name.startswith("wic") and value is None: + wic_slot_id = int(name[-1]) + if vm.slots[0].wics and vm.slots[0].wics[wic_slot_id]: + yield from vm.uninstall_wic(wic_slot_id) + + @asyncio.coroutine + def create_vm_configs(self, vm, startup_config_content, private_config_content): + """ + Creates VM configs from pushed content. + + :param vm: VM instance + :param startup_config_content: content of the startup-config + :param private_config_content: content of the private-config + """ + + default_startup_config_path = os.path.join(vm.project.vm_working_directory(vm), "configs", "i{}_startup-config.cfg".format(vm.dynamips_id)) + default_private_config_path = os.path.join(vm.project.vm_working_directory(vm), "configs", "i{}_private-config.cfg".format(vm.dynamips_id)) + + if startup_config_content: + startup_config_path = self._create_config(vm, startup_config_content, default_startup_config_path) + yield from vm.set_config(startup_config_path) + + if private_config_content: + private_config_path = self._create_config(vm, private_config_content, default_private_config_path) + yield from vm.set_config(vm.startup_config, private_config_path) + + def _create_config(self, vm, content, path): + """ + Creates a config file. + + :param vm: VM instance + :param content: config content + :param path: path to the destination config file + + :returns: relative path to the created config file + """ + + log.info("Creating config file {}".format(path)) + content = "!\n" + content.replace("\r", "") + content = content.replace('%h', vm.name) + config_dir = os.path.dirname(path) + try: + os.makedirs(config_dir, exist_ok=True) + except OSError as e: + raise DynamipsError("Could not create Dynamips configs directory: {}".format(e)) + + try: + with open(path, "w") as f: + log.info("Creating config file {}".format(path)) + f.write(content) + except OSError as e: + raise DynamipsError("Could not create config file {}: {}".format(path, e)) + + return os.path.join("configs", os.path.basename(path)) diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index 31c124a7..4f4cb4b9 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -25,6 +25,7 @@ import time import sys import os import glob +import base64 import logging log = logging.getLogger(__name__) @@ -146,17 +147,19 @@ class Router(BaseVM): "mac_addr": self._mac_addr, "system_id": self._system_id} - # FIXME: add default slots/wics - # slot_number = 0 - # for slot in self._slots: - # if slot: - # slot = str(slot) - # router_defaults["slot" + str(slot_number)] = slot - # slot_number += 1 - - # if self._slots[0] and self._slots[0].wics: - # for wic_slot_number in range(0, len(self._slots[0].wics)): - # router_defaults["wic" + str(wic_slot_number)] = None + # add the slots + slot_number = 0 + for slot in self._slots: + if slot: + slot = str(slot) + router_info["slot" + str(slot_number)] = slot + slot_number += 1 + + # add the wics + if self._slots[0] and self._slots[0].wics: + for wic_slot_number in range(0, len(self._slots[0].wics)): + if self._slots[0].wics[wic_slot_number]: + router_info["wic" + str(wic_slot_number)] = str(self._slots[0].wics[wic_slot_number]) return router_info @@ -312,9 +315,9 @@ class Router(BaseVM): if self._hypervisor and not self._hypervisor.devices: try: yield from self.stop() + yield from self._hypervisor.send('vm delete "{}"'.format(self._name)) except DynamipsError: pass - yield from self._hypervisor.send('vm delete "{}"'.format(self._name)) yield from self.hypervisor.stop() if self._console: @@ -1408,144 +1411,126 @@ class Router(BaseVM): self._private_config = private_config - # TODO: rename - # def rename(self, new_name): - # """ - # Renames this router. - # - # :param new_name: new name string - # """ - # - # if self._startup_config: - # # change the hostname in the startup-config - # startup_config_path = os.path.join(self.hypervisor.working_dir, "configs", "i{}_startup-config.cfg".format(self.id)) - # if os.path.isfile(startup_config_path): - # try: - # with open(startup_config_path, "r+", errors="replace") as f: - # old_config = f.read() - # new_config = old_config.replace(self.name, new_name) - # f.seek(0) - # f.write(new_config) - # except OSError as e: - # raise DynamipsError("Could not amend the configuration {}: {}".format(startup_config_path, e)) - # - # if self._private_config: - # # change the hostname in the private-config - # private_config_path = os.path.join(self.hypervisor.working_dir, "configs", "i{}_private-config.cfg".format(self.id)) - # if os.path.isfile(private_config_path): - # try: - # with open(private_config_path, "r+", errors="replace") as f: - # old_config = f.read() - # new_config = old_config.replace(self.name, new_name) - # f.seek(0) - # f.write(new_config) - # except OSError as e: - # raise DynamipsError("Could not amend the configuration {}: {}".format(private_config_path, e)) - # - # new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces - # self._hypervisor.send("vm rename {name} {new_name}".format(name=self._name, - # new_name=new_name)) - # - # log.info("router {name} [id={id}]: renamed to {new_name}".format(name=self._name, - # id=self._id, - # new_name=new_name)) - # self._name = new_name - - # def set_config(self, startup_config, private_config=''): - # """ - # Sets the config files that are pushed to startup-config and - # private-config in NVRAM when the instance is started. - # - # :param startup_config: path to statup-config file - # :param private_config: path to private-config file - # (keep existing data when if an empty string) - # """ - # - # if self._startup_config != startup_config or self._private_config != private_config: - # - # self._hypervisor.send("vm set_config {name} {startup} {private}".format(name=self._name, - # startup='"' + startup_config + '"', - # private='"' + private_config + '"')) - # - # log.info("router {name} [id={id}]: has a startup-config set: {startup}".format(name=self._name, - # id=self._id, - # startup='"' + startup_config + '"')) - # - # self._startup_config = startup_config - # - # if private_config: - # log.info("router {name} [id={id}]: has a private-config set: {private}".format(name=self._name, - # id=self._id, - # private='"' + private_config + '"')) - # - # self._private_config = private_config - # - # def extract_config(self): - # """ - # Gets the contents of the config files - # startup-config and private-config from NVRAM. - # - # :returns: tuple (startup-config, private-config) base64 encoded - # """ - # - # try: - # reply = self._hypervisor.send("vm extract_config {}".format(self._name))[0].rsplit(' ', 2)[-2:] - # except IOError: - # #for some reason Dynamips gets frozen when it does not find the magic number in the NVRAM file. - # return None, None - # startup_config = reply[0][1:-1] # get statup-config and remove single quotes - # private_config = reply[1][1:-1] # get private-config and remove single quotes - # return startup_config, private_config - # - # def push_config(self, startup_config, private_config='(keep)'): - # """ - # Pushes configuration to the config files startup-config and private-config in NVRAM. - # The data is a Base64 encoded string, or '(keep)' to keep existing data. - # - # :param startup_config: statup-config string base64 encoded - # :param private_config: private-config string base64 encoded - # (keep existing data when if the value is ('keep')) - # """ - # - # self._hypervisor.send("vm push_config {name} {startup} {private}".format(name=self._name, - # startup=startup_config, - # private=private_config)) - # - # log.info("router {name} [id={id}]: new startup-config pushed".format(name=self._name, - # id=self._id)) - # - # if private_config != '(keep)': - # log.info("router {name} [id={id}]: new private-config pushed".format(name=self._name, - # id=self._id)) - # - # def save_configs(self): - # """ - # Saves the startup-config and private-config to files. - # """ - # - # if self.startup_config or self.private_config: - # startup_config_base64, private_config_base64 = self.extract_config() - # if startup_config_base64: - # try: - # config = base64.decodebytes(startup_config_base64.encode("utf-8")).decode("utf-8") - # config = "!\n" + config.replace("\r", "") - # config_path = os.path.join(self.hypervisor.working_dir, self.startup_config) - # with open(config_path, "w") as f: - # log.info("saving startup-config to {}".format(self.startup_config)) - # f.write(config) - # except OSError as e: - # raise DynamipsError("Could not save the startup configuration {}: {}".format(config_path, e)) - # - # if private_config_base64: - # try: - # config = base64.decodebytes(private_config_base64.encode("utf-8")).decode("utf-8") - # config = "!\n" + config.replace("\r", "") - # config_path = os.path.join(self.hypervisor.working_dir, self.private_config) - # with open(config_path, "w") as f: - # log.info("saving private-config to {}".format(self.private_config)) - # f.write(config) - # except OSError as e: - # raise DynamipsError("Could not save the private configuration {}: {}".format(config_path, e)) + @asyncio.coroutine + def set_name(self, new_name): + """ + Renames this router. + + :param new_name: new name string + """ + + module_workdir = self.project.module_working_directory(self.manager.module_name.lower()) + if self._startup_config: + # change the hostname in the startup-config + startup_config_path = os.path.join(module_workdir, "configs", "i{}_startup-config.cfg".format(self._dynamips_id)) + if os.path.isfile(startup_config_path): + try: + with open(startup_config_path, "r+", errors="replace") as f: + old_config = f.read() + new_config = old_config.replace(self.name, new_name) + f.seek(0) + f.write(new_config) + except OSError as e: + raise DynamipsError("Could not amend the configuration {}: {}".format(startup_config_path, e)) + + if self._private_config: + # change the hostname in the private-config + private_config_path = os.path.join(module_workdir, "configs", "i{}_private-config.cfg".format(self._dynamips_id)) + if os.path.isfile(private_config_path): + try: + with open(private_config_path, "r+", errors="replace") as f: + old_config = f.read() + new_config = old_config.replace(self.name, new_name) + f.seek(0) + f.write(new_config) + except OSError as e: + raise DynamipsError("Could not amend the configuration {}: {}".format(private_config_path, e)) + + yield from self._hypervisor.send('vm rename "{name}" "{new_name}"'.format(name=self._name, new_name=new_name)) + log.info('Router "{name}" [{id}]: renamed to "{new_name}"'.format(name=self._name, id=self._id, new_name=new_name)) + self._name = new_name + + @asyncio.coroutine + def set_config(self, startup_config, private_config=''): + """ + Sets the config files that are pushed to startup-config and + private-config in NVRAM when the instance is started. + + :param startup_config: path to statup-config file + :param private_config: path to private-config file + (keep existing data when if an empty string) + """ + + startup_config = startup_config.replace("\\", '/') + private_config = private_config.replace("\\", '/') + + if self._startup_config != startup_config or self._private_config != private_config: + + yield from self._hypervisor.send('vm set_config "{name}" "{startup}" "{private}"'.format(name=self._name, + startup=startup_config, + private=private_config)) + + log.info('Router "{name}" [{id}]: has a new startup-config set: "{startup}"'.format(name=self._name, + id=self._id, + startup=startup_config)) + + self._startup_config = startup_config + + if private_config: + log.info('Router "{name}" [{id}]: has a new private-config set: "{private}"'.format(name=self._name, + id=self._id, + private=private_config)) + + self._private_config = private_config + + @asyncio.coroutine + def extract_config(self): + """ + Gets the contents of the config files + startup-config and private-config from NVRAM. + + :returns: tuple (startup-config, private-config) base64 encoded + """ + + try: + reply = yield from self._hypervisor.send("vm extract_config {}".format(self._name))[0].rsplit(' ', 2)[-2:] + except IOError: + #for some reason Dynamips gets frozen when it does not find the magic number in the NVRAM file. + return None, None + startup_config = reply[0][1:-1] # get statup-config and remove single quotes + private_config = reply[1][1:-1] # get private-config and remove single quotes + return startup_config, private_config + + @asyncio.coroutine + def save_configs(self): + """ + Saves the startup-config and private-config to files. + """ + + if self.startup_config or self.private_config: + module_workdir = self.project.module_working_directory(self.manager.module_name.lower()) + startup_config_base64, private_config_base64 = yield from self.extract_config() + if startup_config_base64: + try: + config = base64.decodebytes(startup_config_base64.encode("utf-8")).decode("utf-8") + config = "!\n" + config.replace("\r", "") + config_path = os.path.join(module_workdir, self.startup_config) + with open(config_path, "w") as f: + log.info("saving startup-config to {}".format(self.startup_config)) + f.write(config) + except OSError as e: + raise DynamipsError("Could not save the startup configuration {}: {}".format(config_path, e)) + + if private_config_base64: + try: + config = base64.decodebytes(private_config_base64.encode("utf-8")).decode("utf-8") + config = "!\n" + config.replace("\r", "") + config_path = os.path.join(module_workdir, self.private_config) + with open(config_path, "w") as f: + log.info("saving private-config to {}".format(self.private_config)) + f.write(config) + except OSError as e: + raise DynamipsError("Could not save the private configuration {}: {}".format(config_path, e)) def delete(self): """ @@ -1555,6 +1540,20 @@ class Router(BaseVM): # delete the VM files project_dir = os.path.join(self.project.module_working_directory(self.manager.module_name.lower())) files = glob.glob(os.path.join(project_dir, "{}_i{}*".format(self._platform, self._dynamips_id))) + + module_workdir = self.project.module_working_directory(self.manager.module_name.lower()) + # delete the startup-config + if self._startup_config: + startup_config_path = os.path.join(module_workdir, "configs", "i{}_startup-config.cfg".format(self._dynamips_id)) + if os.path.isfile(startup_config_path): + files.append(startup_config_path) + + # delete the private-config + if self._private_config: + private_config_path = os.path.join(module_workdir, "configs", "i{}_private-config.cfg".format(self._dynamips_id)) + if os.path.isfile(private_config_path): + files.append(private_config_path) + for file in files: try: log.debug("Deleting file {}".format(file)) @@ -1571,17 +1570,4 @@ class Router(BaseVM): yield from self._hypervisor.send('vm clean_delete "{}"'.format(self._name)) self._hypervisor.devices.remove(self) - - # if self._startup_config: - # # delete the startup-config - # startup_config_path = os.path.join(self.hypervisor.working_dir, "configs", "{}.cfg".format(self.name)) - # if os.path.isfile(startup_config_path): - # os.remove(startup_config_path) - # - # if self._private_config: - # # delete the private-config - # private_config_path = os.path.join(self.hypervisor.working_dir, "configs", "{}-private.cfg".format(self.name)) - # if os.path.isfile(private_config_path): - # os.remove(private_config_path) - log.info('Router "{name}" [{id}] has been deleted (including associated files)'.format(name=self._name, id=self._id)) diff --git a/gns3server/schemas/dynamips_vm.py b/gns3server/schemas/dynamips_vm.py index c30d2920..0d767dd9 100644 --- a/gns3server/schemas/dynamips_vm.py +++ b/gns3server/schemas/dynamips_vm.py @@ -62,11 +62,19 @@ VM_CREATE_SCHEMA = { "type": "string", "minLength": 1, }, + "startup_config_content": { + "description": "Content of IOS startup configuration file", + "type": "string", + }, "private_config": { "description": "path to the IOS private configuration file", "type": "string", "minLength": 1, }, + "private_config_content": { + "description": "Content of IOS private configuration file", + "type": "string", + }, "ram": { "description": "amount of RAM in MB", "type": "integer" From e6fd471dd5d2f6069a1b569cd60db412933e9d2f Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 16 Feb 2015 18:21:10 -0700 Subject: [PATCH 259/485] Save Dynamips VM configs when closing a project. --- gns3server/modules/dynamips/__init__.py | 44 ++++++++++++--------- gns3server/modules/dynamips/nodes/router.py | 3 +- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index d76e77ee..2477a0b6 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -160,6 +160,8 @@ class Dynamips(BaseManager): files += glob.glob(os.path.join(project_dir, "ilt_*")) files += glob.glob(os.path.join(project_dir, "c[0-9][0-9][0-9][0-9]_*_rommon_vars")) files += glob.glob(os.path.join(project_dir, "c[0-9][0-9][0-9][0-9]_*_ssa")) + files += glob.glob(os.path.join(project_dir, "dynamips_log*")) + files += glob.glob(os.path.join(project_dir, "*_log.txt")) for file in files: try: log.debug("Deleting file {}".format(file)) @@ -415,24 +417,27 @@ class Dynamips(BaseManager): # create a new ghost IOS instance ghost_id = str(uuid4()) ghost = Router("ghost-" + ghost_file, ghost_id, vm.project, vm.manager, platform=vm.platform, hypervisor=vm.hypervisor, ghost_flag=True) - yield from ghost.create() - yield from ghost.set_image(vm.image) - # for 7200s, the NPE must be set when using an NPE-G2. - if vm.platform == "c7200": - yield from ghost.set_npe(vm.npe) - yield from ghost.set_ghost_status(1) - yield from ghost.set_ghost_file(ghost_file) - yield from ghost.set_ram(vm.ram) try: - yield from ghost.start() - yield from ghost.stop() - self._ghost_files.add(ghost_file_path) - except DynamipsError: - raise - finally: - yield from ghost.clean_delete() - - if vm.ghost_file != ghost_file: + yield from ghost.create() + yield from ghost.set_image(vm.image) + # for 7200s, the NPE must be set when using an NPE-G2. + if vm.platform == "c7200": + yield from ghost.set_npe(vm.npe) + yield from ghost.set_ghost_status(1) + yield from ghost.set_ghost_file(ghost_file) + yield from ghost.set_ram(vm.ram) + try: + yield from ghost.start() + yield from ghost.stop() + self._ghost_files.add(ghost_file_path) + except DynamipsError: + raise + finally: + yield from ghost.clean_delete() + except DynamipsError as e: + log.warn("Could not create ghost instance: {}".format(e)) + + if vm.ghost_file != ghost_file and os.path.isfile(ghost_file): # set the ghost file to the router yield from vm.set_ghost_status(2) yield from vm.set_ghost_file(ghost_file) @@ -487,8 +492,9 @@ class Dynamips(BaseManager): :param private_config_content: content of the private-config """ - default_startup_config_path = os.path.join(vm.project.vm_working_directory(vm), "configs", "i{}_startup-config.cfg".format(vm.dynamips_id)) - default_private_config_path = os.path.join(vm.project.vm_working_directory(vm), "configs", "i{}_private-config.cfg".format(vm.dynamips_id)) + module_workdir = vm.project.module_working_directory(self.module_name.lower()) + default_startup_config_path = os.path.join(module_workdir, "configs", "i{}_startup-config.cfg".format(vm.dynamips_id)) + default_private_config_path = os.path.join(module_workdir, "configs", "i{}_private-config.cfg".format(vm.dynamips_id)) if startup_config_content: startup_config_path = self._create_config(vm, startup_config_content, default_startup_config_path) diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index 4f4cb4b9..456d4bdf 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -315,6 +315,7 @@ class Router(BaseVM): if self._hypervisor and not self._hypervisor.devices: try: yield from self.stop() + yield from self.save_configs() yield from self._hypervisor.send('vm delete "{}"'.format(self._name)) except DynamipsError: pass @@ -1563,7 +1564,7 @@ class Router(BaseVM): continue @asyncio.coroutine - def clean_delete(self, stop_hypervisor=False): + def clean_delete(self): """ Deletes this router & associated files (nvram, disks etc.) """ From dc4df68c7a609b36a05566387ac53ad445834f04 Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 16 Feb 2015 21:30:31 -0700 Subject: [PATCH 260/485] Keep Dynamips logs. --- gns3server/modules/dynamips/__init__.py | 2 -- gns3server/modules/dynamips/hypervisor.py | 4 ++-- gns3server/modules/dynamips/nodes/router.py | 3 ++- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index 2477a0b6..c10fce52 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -160,8 +160,6 @@ class Dynamips(BaseManager): files += glob.glob(os.path.join(project_dir, "ilt_*")) files += glob.glob(os.path.join(project_dir, "c[0-9][0-9][0-9][0-9]_*_rommon_vars")) files += glob.glob(os.path.join(project_dir, "c[0-9][0-9][0-9][0-9]_*_ssa")) - files += glob.glob(os.path.join(project_dir, "dynamips_log*")) - files += glob.glob(os.path.join(project_dir, "*_log.txt")) for file in files: try: log.debug("Deleting file {}".format(file)) diff --git a/gns3server/modules/dynamips/hypervisor.py b/gns3server/modules/dynamips/hypervisor.py index 778c490b..5d1867d0 100644 --- a/gns3server/modules/dynamips/hypervisor.py +++ b/gns3server/modules/dynamips/hypervisor.py @@ -42,7 +42,7 @@ class Hypervisor(DynamipsHypervisor): :param host: host/address for this hypervisor """ - _instance_count = 0 + _instance_count = 1 def __init__(self, path, working_dir, host, port): @@ -181,7 +181,7 @@ class Hypervisor(DynamipsHypervisor): command = [self._path] command.extend(["-N1"]) # use instance IDs for filenames - command.extend(["-l", "dynamips_log_{}.txt".format(self._port)]) # log file + command.extend(["-l", "dynamips_i{}_log.txt".format(self._id)]) # log file if self._host != "0.0.0.0" and self._host != "::": command.extend(["-H", "{}:{}".format(self._host, self._port)]) else: diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index 456d4bdf..910430a9 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -1494,7 +1494,8 @@ class Router(BaseVM): """ try: - reply = yield from self._hypervisor.send("vm extract_config {}".format(self._name))[0].rsplit(' ', 2)[-2:] + reply = yield from self._hypervisor.send("vm extract_config {}".format(self._name)) + reply = reply[0].rsplit(' ', 2)[-2:] except IOError: #for some reason Dynamips gets frozen when it does not find the magic number in the NVRAM file. return None, None From 70ad9fff2630fa38b5c7b7b03ba590a5a95ed750 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 17 Feb 2015 09:46:18 +0100 Subject: [PATCH 261/485] Documentation update --- ...svmidadaptersadapteriddportsportiddnio.txt | 13 ------ docs/api/examples/get_projectsprojectid.txt | 4 +- .../get_projectsprojectidiouvmsvmid.txt | 4 +- ...get_projectsprojectidvirtualboxvmsvmid.txt | 2 +- .../get_projectsprojectidvpcsvmsvmid.txt | 2 +- .../examples/post_projectsprojectidiouvms.txt | 11 ++--- .../post_projectsprojectidvirtualboxvms.txt | 2 +- ...svmidadaptersadapteriddportsportiddnio.txt | 25 ------------ .../post_projectsprojectidvpcsvms.txt | 2 +- docs/api/examples/put_projectsprojectid.txt | 4 +- docs/api/v1projectsprojectiddynamipsvms.rst | 4 ++ .../v1projectsprojectiddynamipsvmsvmid.rst | 3 ++ docs/api/v1projectsprojectidiouvms.rst | 1 + docs/api/v1projectsprojectidiouvmsvmid.rst | 2 +- ...ptersadapternumberdportsportnumberdnio.rst | 4 +- ...svmidadaptersadapteriddportsportiddnio.rst | 40 ------------------- ...xvmsvmidadaptersadapteriddstartcapture.rst | 30 -------------- ...oxvmsvmidadaptersadapteriddstopcapture.rst | 21 ---------- ...ptersadapternumberdportsportnumberdnio.rst | 4 +- 19 files changed, 29 insertions(+), 149 deletions(-) delete mode 100644 docs/api/examples/delete_projectsprojectidvirtualboxvmsvmidadaptersadapteriddportsportiddnio.txt delete mode 100644 docs/api/examples/post_projectsprojectidvirtualboxvmsvmidadaptersadapteriddportsportiddnio.txt delete mode 100644 docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddportsportiddnio.rst delete mode 100644 docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddstartcapture.rst delete mode 100644 docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddstopcapture.rst diff --git a/docs/api/examples/delete_projectsprojectidvirtualboxvmsvmidadaptersadapteriddportsportiddnio.txt b/docs/api/examples/delete_projectsprojectidvirtualboxvmsvmidadaptersadapteriddportsportiddnio.txt deleted file mode 100644 index 0c732706..00000000 --- a/docs/api/examples/delete_projectsprojectidvirtualboxvmsvmidadaptersadapteriddportsportiddnio.txt +++ /dev/null @@ -1,13 +0,0 @@ -curl -i -X DELETE 'http://localhost:8000/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/ports/{port_id:\d+}/nio' - -DELETE /projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/ports/{port_id:\d+}/nio HTTP/1.1 - - - -HTTP/1.1 204 -CONNECTION: keep-alive -CONTENT-LENGTH: 0 -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /v1/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/ports/{port_id:\d+}/nio - diff --git a/docs/api/examples/get_projectsprojectid.txt b/docs/api/examples/get_projectsprojectid.txt index 2cfcf9d8..0823bf08 100644 --- a/docs/api/examples/get_projectsprojectid.txt +++ b/docs/api/examples/get_projectsprojectid.txt @@ -13,8 +13,8 @@ SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /v1/projects/{project_id} { - "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmp5cndh7nh", - "path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmp5cndh7nh/00010203-0405-0607-0809-0a0b0c0d0e0f", + "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpd4_jlup7", + "path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpd4_jlup7/00010203-0405-0607-0809-0a0b0c0d0e0f", "project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f", "temporary": false } diff --git a/docs/api/examples/get_projectsprojectidiouvmsvmid.txt b/docs/api/examples/get_projectsprojectidiouvmsvmid.txt index 3cd4491d..e702d104 100644 --- a/docs/api/examples/get_projectsprojectidiouvmsvmid.txt +++ b/docs/api/examples/get_projectsprojectidiouvmsvmid.txt @@ -18,9 +18,9 @@ X-ROUTE: /v1/projects/{project_id}/iou/vms/{vm_id} "l1_keepalives": false, "name": "PC TEST 1", "nvram": 128, - "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2262/test_iou_get0/iou.bin", + "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2628/test_iou_get0/iou.bin", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "ram": 256, "serial_adapters": 2, - "vm_id": "548ed6e5-2ab6-421d-8f85-9dee638617fb" + "vm_id": "8d4ce7ee-9c5e-4ac0-9106-f8cf28a12b5d" } diff --git a/docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt b/docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt index fc9f1ff6..d87a9f2a 100644 --- a/docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt +++ b/docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt @@ -21,6 +21,6 @@ X-ROUTE: /v1/projects/{project_id}/virtualbox/vms/{vm_id} "name": "VMTEST", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "use_any_adapter": false, - "vm_id": "f70dc1dd-563e-46cc-9cf7-bb169730b8e2", + "vm_id": "591dcdfc-f25f-4a87-bd06-6091551c6f8e", "vmname": "VMTEST" } diff --git a/docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt b/docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt index cde6eea4..83c6f71d 100644 --- a/docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt +++ b/docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt @@ -17,5 +17,5 @@ X-ROUTE: /v1/projects/{project_id}/vpcs/vms/{vm_id} "name": "PC TEST 1", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "startup_script": null, - "vm_id": "ed3e4572-df65-4581-9332-324e749d480a" + "vm_id": "37726103-1521-42c3-8925-fd4fabea9caf" } diff --git a/docs/api/examples/post_projectsprojectidiouvms.txt b/docs/api/examples/post_projectsprojectidiouvms.txt index 0e129869..9f9a6d91 100644 --- a/docs/api/examples/post_projectsprojectidiouvms.txt +++ b/docs/api/examples/post_projectsprojectidiouvms.txt @@ -1,13 +1,14 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/iou/vms' -d '{"ethernet_adapters": 0, "iourc_path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2262/test_iou_create_with_params0/iourc", "l1_keepalives": true, "name": "PC TEST 1", "nvram": 512, "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2262/test_iou_create_with_params0/iou.bin", "ram": 1024, "serial_adapters": 4}' +curl -i -X POST 'http://localhost:8000/projects/{project_id}/iou/vms' -d '{"ethernet_adapters": 0, "initial_config": "hostname test", "iourc_path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2628/test_iou_create_with_params0/iourc", "l1_keepalives": true, "name": "PC TEST 1", "nvram": 512, "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2628/test_iou_create_with_params0/iou.bin", "ram": 1024, "serial_adapters": 4}' POST /projects/{project_id}/iou/vms HTTP/1.1 { "ethernet_adapters": 0, - "iourc_path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2262/test_iou_create_with_params0/iourc", + "initial_config": "hostname test", + "iourc_path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2628/test_iou_create_with_params0/iourc", "l1_keepalives": true, "name": "PC TEST 1", "nvram": 512, - "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2262/test_iou_create_with_params0/iou.bin", + "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2628/test_iou_create_with_params0/iou.bin", "ram": 1024, "serial_adapters": 4 } @@ -27,9 +28,9 @@ X-ROUTE: /v1/projects/{project_id}/iou/vms "l1_keepalives": true, "name": "PC TEST 1", "nvram": 512, - "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2262/test_iou_create_with_params0/iou.bin", + "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2628/test_iou_create_with_params0/iou.bin", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "ram": 1024, "serial_adapters": 4, - "vm_id": "12acb1c1-304e-4c3e-81c4-23c52e6c01ba" + "vm_id": "f1bafbbe-96ba-4088-83e2-391cf9477e89" } diff --git a/docs/api/examples/post_projectsprojectidvirtualboxvms.txt b/docs/api/examples/post_projectsprojectidvirtualboxvms.txt index 2b3c4fb3..751f5867 100644 --- a/docs/api/examples/post_projectsprojectidvirtualboxvms.txt +++ b/docs/api/examples/post_projectsprojectidvirtualboxvms.txt @@ -25,6 +25,6 @@ X-ROUTE: /v1/projects/{project_id}/virtualbox/vms "name": "VM1", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "use_any_adapter": false, - "vm_id": "ad5dba5d-0219-41a0-9bd3-eaca69649f6c", + "vm_id": "be653307-d7d6-4884-932f-0d87c7e2c06b", "vmname": "VM1" } diff --git a/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidadaptersadapteriddportsportiddnio.txt b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidadaptersadapteriddportsportiddnio.txt deleted file mode 100644 index a2be8b22..00000000 --- a/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidadaptersadapteriddportsportiddnio.txt +++ /dev/null @@ -1,25 +0,0 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/ports/{port_id:\d+}/nio' -d '{"lport": 4242, "rhost": "127.0.0.1", "rport": 4343, "type": "nio_udp"}' - -POST /projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/ports/{port_id:\d+}/nio HTTP/1.1 -{ - "lport": 4242, - "rhost": "127.0.0.1", - "rport": 4343, - "type": "nio_udp" -} - - -HTTP/1.1 201 -CONNECTION: keep-alive -CONTENT-LENGTH: 89 -CONTENT-TYPE: application/json -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /v1/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/ports/{port_id:\d+}/nio - -{ - "lport": 4242, - "rhost": "127.0.0.1", - "rport": 4343, - "type": "nio_udp" -} diff --git a/docs/api/examples/post_projectsprojectidvpcsvms.txt b/docs/api/examples/post_projectsprojectidvpcsvms.txt index 60df8000..9ed92782 100644 --- a/docs/api/examples/post_projectsprojectidvpcsvms.txt +++ b/docs/api/examples/post_projectsprojectidvpcsvms.txt @@ -19,5 +19,5 @@ X-ROUTE: /v1/projects/{project_id}/vpcs/vms "name": "PC TEST 1", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "startup_script": null, - "vm_id": "6c343265-dc55-4bfe-8d4e-f3f21eeeca8d" + "vm_id": "154fff9a-bd47-4740-8a00-847ec30dd6e0" } diff --git a/docs/api/examples/put_projectsprojectid.txt b/docs/api/examples/put_projectsprojectid.txt index 878274d0..ebab8846 100644 --- a/docs/api/examples/put_projectsprojectid.txt +++ b/docs/api/examples/put_projectsprojectid.txt @@ -1,8 +1,8 @@ -curl -i -X PUT 'http://localhost:8000/projects/{project_id}' -d '{"path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2262/test_update_path_project_non_l0"}' +curl -i -X PUT 'http://localhost:8000/projects/{project_id}' -d '{"path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2628/test_update_path_project_non_l0"}' PUT /projects/{project_id} HTTP/1.1 { - "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2262/test_update_path_project_non_l0" + "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2628/test_update_path_project_non_l0" } diff --git a/docs/api/v1projectsprojectiddynamipsvms.rst b/docs/api/v1projectsprojectiddynamipsvms.rst index 0479e826..410f5f87 100644 --- a/docs/api/v1projectsprojectiddynamipsvms.rst +++ b/docs/api/v1projectsprojectiddynamipsvms.rst @@ -24,6 +24,7 @@ Input + @@ -46,6 +47,7 @@ Input + @@ -58,6 +60,7 @@ Input + @@ -72,6 +75,7 @@ Output
Name Mandatory Type Description
aux integer auxiliary console TCP port
chassis string router chassis model
clock_divisor integer clock divisor
confreg string configuration register
console integer console TCP port
power_supplies array Power supplies status
private_config string path to the IOS private configuration file
private_config_base64 string private configuration base64 encoded
private_config_content string Content of IOS private configuration file
ram integer amount of RAM in MB
sensors array Temperature sensors
slot0 Network module slot 0
sparsemem boolean sparse memory feature
startup_config string path to the IOS startup configuration file
startup_config_base64 string startup configuration base64 encoded
startup_config_content string Content of IOS startup configuration file
system_id string system ID
vm_id Dynamips VM instance identifier
wic0 Network module WIC slot 0
+ diff --git a/docs/api/v1projectsprojectiddynamipsvmsvmid.rst b/docs/api/v1projectsprojectiddynamipsvmsvmid.rst index 07b207ac..bce8b927 100644 --- a/docs/api/v1projectsprojectiddynamipsvmsvmid.rst +++ b/docs/api/v1projectsprojectiddynamipsvmsvmid.rst @@ -25,6 +25,7 @@ Output
Name Mandatory Type Description
aux integer auxiliary console TCP port
chassis string router chassis model
clock_divisor integer clock divisor
confreg string configuration register
console integer console TCP port
+ @@ -91,6 +92,7 @@ Input
Name Mandatory Type Description
aux integer auxiliary console TCP port
chassis string router chassis model
clock_divisor integer clock divisor
confreg string configuration register
console integer console TCP port
+ @@ -137,6 +139,7 @@ Output
Name Mandatory Type Description
aux integer auxiliary console TCP port
chassis string router chassis model
clock_divisor integer clock divisor
confreg string configuration register
console integer console TCP port
+ diff --git a/docs/api/v1projectsprojectidiouvms.rst b/docs/api/v1projectsprojectidiouvms.rst index a462804a..978cf3ff 100644 --- a/docs/api/v1projectsprojectidiouvms.rst +++ b/docs/api/v1projectsprojectidiouvms.rst @@ -25,6 +25,7 @@ Input + diff --git a/docs/api/v1projectsprojectidiouvmsvmid.rst b/docs/api/v1projectsprojectidiouvmsvmid.rst index 30935bbd..e3dd7611 100644 --- a/docs/api/v1projectsprojectidiouvmsvmid.rst +++ b/docs/api/v1projectsprojectidiouvmsvmid.rst @@ -61,7 +61,7 @@ Input - + diff --git a/docs/api/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst index cd0e5f27..56437eec 100644 --- a/docs/api/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -10,9 +10,9 @@ Add a NIO to a IOU instance Parameters ********** - **adapter_number**: Network adapter where the nio is located -- **port_number**: Port where the nio should be added - **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **port_number**: Port where the nio should be added Response status codes ********************** @@ -28,9 +28,9 @@ Remove a NIO from a IOU instance Parameters ********** - **adapter_number**: Network adapter where the nio is located -- **port_number**: Port from where the nio should be removed - **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **port_number**: Port from where the nio should be removed Response status codes ********************** diff --git a/docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddportsportiddnio.rst b/docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddportsportiddnio.rst deleted file mode 100644 index 7a9228c5..00000000 --- a/docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddportsportiddnio.rst +++ /dev/null @@ -1,40 +0,0 @@ -/v1/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/ports/{port_id:\d+}/nio ------------------------------------------------------------------------------------------------------------------ - -.. contents:: - -POST /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}**/adapters/**{adapter_id:\d+}**/ports/**{port_id:\d+}**/nio -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Add a NIO to a VirtualBox VM instance - -Parameters -********** -- **port_id**: Port in the adapter (always 0 for virtualbox) -- **vm_id**: UUID for the instance -- **project_id**: UUID for the project -- **adapter_id**: Adapter where the nio should be added - -Response status codes -********************** -- **400**: Invalid request -- **201**: NIO created -- **404**: Instance doesn't exist - - -DELETE /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}**/adapters/**{adapter_id:\d+}**/ports/**{port_id:\d+}**/nio -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Remove a NIO from a VirtualBox VM instance - -Parameters -********** -- **port_id**: Port in the adapter (always 0 for virtualbox) -- **vm_id**: UUID for the instance -- **project_id**: UUID for the project -- **adapter_id**: Adapter from where the nio should be removed - -Response status codes -********************** -- **400**: Invalid request -- **404**: Instance doesn't exist -- **204**: NIO deleted - diff --git a/docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddstartcapture.rst b/docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddstartcapture.rst deleted file mode 100644 index 6a3320de..00000000 --- a/docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddstartcapture.rst +++ /dev/null @@ -1,30 +0,0 @@ -/v1/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/start_capture ------------------------------------------------------------------------------------------------------------------ - -.. contents:: - -POST /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}**/adapters/**{adapter_id:\d+}**/start_capture -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Start a packet capture on a VirtualBox VM instance - -Parameters -********** -- **vm_id**: UUID for the instance -- **project_id**: UUID for the project -- **adapter_id**: Adapter to start a packet capture - -Response status codes -********************** -- **200**: Capture started -- **400**: Invalid request -- **404**: Instance doesn't exist - -Input -******* -.. raw:: html - -
Name Mandatory Type Description
aux integer auxiliary console TCP port
chassis string router chassis model
clock_divisor integer clock divisor
confreg string configuration register
console integer console TCP port
Name Mandatory Type Description
console ['integer', 'null'] console TCP port
ethernet_adapters integer How many ethernet adapters are connected to the IOU
initial_config ['string', 'null'] Initial configuration of the IOU
iourc_path string Path of iourc
l1_keepalives ['boolean', 'null'] Always up ethernet interface
name string IOU VM name
Name Mandatory Type Description
console ['integer', 'null'] console TCP port
ethernet_adapters ['integer', 'null'] How many ethernet adapters are connected to the IOU
initial_config ['string', 'null'] Initial configuration path
initial_config ['string', 'null'] Initial configuration of the IOU
iourc_path ['string', 'null'] Path of iourc
l1_keepalives ['boolean', 'null'] Always up ethernet interface
name ['string', 'null'] IOU VM name
- - -
Name Mandatory Type Description
capture_file_name string Capture file name
- diff --git a/docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddstopcapture.rst b/docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddstopcapture.rst deleted file mode 100644 index 9ceeef26..00000000 --- a/docs/api/v1projectsprojectidvirtualboxvmsvmidadaptersadapteriddstopcapture.rst +++ /dev/null @@ -1,21 +0,0 @@ -/v1/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/stop_capture ------------------------------------------------------------------------------------------------------------------ - -.. contents:: - -POST /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}**/adapters/**{adapter_id:\d+}**/stop_capture -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Stop a packet capture on a VirtualBox VM instance - -Parameters -********** -- **vm_id**: UUID for the instance -- **project_id**: UUID for the project -- **adapter_id**: Adapter to stop a packet capture - -Response status codes -********************** -- **400**: Invalid request -- **404**: Instance doesn't exist -- **204**: Capture stopped - diff --git a/docs/api/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst index 1c756021..f1183c0a 100644 --- a/docs/api/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -10,9 +10,9 @@ Add a NIO to a VPCS instance Parameters ********** - **adapter_number**: Network adapter where the nio is located -- **port_number**: Port where the nio should be added - **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **port_number**: Port where the nio should be added Response status codes ********************** @@ -28,9 +28,9 @@ Remove a NIO from a VPCS instance Parameters ********** - **adapter_number**: Network adapter where the nio is located -- **port_number**: Port from where the nio should be removed - **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **port_number**: Port from where the nio should be removed Response status codes ********************** From 517042891360e07f7812daf50c1ef3353f76b13e Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 17 Feb 2015 10:01:15 +0100 Subject: [PATCH 262/485] Fix a capture crash --- gns3server/modules/iou/iou_vm.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 73a919c8..a67a0e13 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -343,7 +343,6 @@ class IOUVM(BaseVM): log.warn("Could not determine the shared library dependencies for {}: {}".format(self._path, e)) return - print(output) p = re.compile("([\.\w]+)\s=>\s+not found") missing_libs = p.findall(output) if missing_libs: @@ -890,6 +889,7 @@ class IOUVM(BaseVM): else: return None + @asyncio.coroutine def start_capture(self, adapter_number, port_number, output_file, data_link_type="DLT_EN10MB"): """ Starts a packet capture. @@ -933,6 +933,7 @@ class IOUVM(BaseVM): self._update_iouyap_config() os.kill(self._iouyap_process.pid, signal.SIGHUP) + @asyncio.coroutine def stop_capture(self, adapter_number, port_number): """ Stops a packet capture. From 57348d05082faaa5c7beb7eaa323c1a63e2ae43b Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 17 Feb 2015 10:37:09 +0100 Subject: [PATCH 263/485] Better organisation of the API documentation --- docs/api/dynamips_device.rst | 8 + .../v1projectsprojectiddynamipsdevices.rst | 43 ++++++ ...ojectsprojectiddynamipsdevicesdeviceid.rst | 106 +++++++++++++ ...mipsdevicesdeviceidportsportnumberdnio.rst | 140 ++++++++++++++++++ ...esdeviceidportsportnumberdstartcapture.rst | 31 ++++ ...cesdeviceidportsportnumberdstopcapture.rst | 21 +++ docs/api/dynamips_vm.rst | 8 + .../v1projectsprojectiddynamipsvms.rst | 4 +- .../v1projectsprojectiddynamipsvmsvmid.rst | 8 +- ...ptersadapternumberdportsportnumberdnio.rst | 40 +++++ ...ternumberdportsportnumberdstartcapture.rst | 32 ++++ ...pternumberdportsportnumberdstopcapture.rst | 22 +++ ...projectsprojectiddynamipsvmsvmidreload.rst | 4 +- ...projectsprojectiddynamipsvmsvmidresume.rst | 4 +- ...1projectsprojectiddynamipsvmsvmidstart.rst | 4 +- ...v1projectsprojectiddynamipsvmsvmidstop.rst | 4 +- ...rojectsprojectiddynamipsvmsvmidsuspend.rst | 4 +- .../api/examples/delete_projectsprojectid.txt | 13 -- ...ptersadapternumberdportsportnumberdnio.txt | 13 -- ...ptersadapternumberdportsportnumberdnio.txt | 13 -- docs/api/examples/get_interfaces.txt | 60 -------- docs/api/examples/get_projectsprojectid.txt | 20 --- .../get_projectsprojectidiouvmsvmid.txt | 26 ---- ...get_projectsprojectidvirtualboxvmsvmid.txt | 26 ---- .../get_projectsprojectidvpcsvmsvmid.txt | 21 --- docs/api/examples/get_version.txt | 17 --- docs/api/examples/post_portsudp.txt | 17 --- .../examples/post_projectsprojectidclose.txt | 13 -- .../examples/post_projectsprojectidcommit.txt | 13 -- .../examples/post_projectsprojectidiouvms.txt | 36 ----- ...ptersadapternumberdportsportnumberdnio.txt | 21 --- .../post_projectsprojectidvirtualboxvms.txt | 30 ---- .../post_projectsprojectidvpcsvms.txt | 23 --- ...ptersadapternumberdportsportnumberdnio.txt | 25 ---- docs/api/examples/post_version.txt | 19 --- docs/api/examples/put_projectsprojectid.txt | 20 --- docs/api/iou.rst | 8 + .../{ => iou}/v1projectsprojectidiouvms.rst | 4 +- .../v1projectsprojectidiouvmsvmid.rst | 8 +- ...ptersadapternumberdportsportnumberdnio.rst | 10 +- ...ternumberdportsportnumberdstartcapture.rst | 32 ++++ ...pternumberdportsportnumberdstopcapture.rst | 22 +++ .../v1projectsprojectidiouvmsvmidreload.rst | 4 +- .../v1projectsprojectidiouvmsvmidstart.rst | 4 +- .../v1projectsprojectidiouvmsvmidstop.rst | 4 +- docs/api/network.rst | 8 + docs/api/{ => network}/v1interfaces.rst | 4 +- docs/api/{ => network}/v1portsudp.rst | 4 +- docs/api/project.rst | 8 + docs/api/{ => project}/v1projects.rst | 4 +- .../api/{ => project}/v1projectsprojectid.rst | 8 +- .../v1projectsprojectidclose.rst | 4 +- .../v1projectsprojectidcommit.rst | 4 +- docs/api/version.rst | 8 + docs/api/{ => version}/v1version.rst | 6 +- docs/api/virtualbox.rst | 8 + .../v1projectsprojectidvirtualboxvms.rst | 4 +- .../v1projectsprojectidvirtualboxvmsvmid.rst | 8 +- ...ptersadapternumberdportsportnumberdnio.rst | 40 +++++ ...ternumberdportsportnumberdstartcapture.rst | 31 ++++ ...pternumberdportsportnumberdstopcapture.rst | 22 +++ ...ojectsprojectidvirtualboxvmsvmidreload.rst | 4 +- ...ojectsprojectidvirtualboxvmsvmidresume.rst | 4 +- ...rojectsprojectidvirtualboxvmsvmidstart.rst | 4 +- ...projectsprojectidvirtualboxvmsvmidstop.rst | 4 +- ...jectsprojectidvirtualboxvmsvmidsuspend.rst | 4 +- docs/api/{ => virtualbox}/v1virtualboxvms.rst | 4 +- docs/api/vpcs.rst | 8 + .../{ => vpcs}/v1projectsprojectidvpcsvms.rst | 4 +- .../v1projectsprojectidvpcsvmsvmid.rst | 8 +- ...ptersadapternumberdportsportnumberdnio.rst | 10 +- .../v1projectsprojectidvpcsvmsvmidreload.rst | 4 +- .../v1projectsprojectidvpcsvmsvmidstart.rst | 4 +- .../v1projectsprojectidvpcsvmsvmidstop.rst | 4 +- gns3server/web/documentation.py | 76 ++++++---- gns3server/web/route.py | 9 +- scripts/documentation.sh | 2 +- tests/modules/iou/test_iou_vm.py | 10 +- 78 files changed, 789 insertions(+), 550 deletions(-) create mode 100644 docs/api/dynamips_device.rst create mode 100644 docs/api/dynamips_device/v1projectsprojectiddynamipsdevices.rst create mode 100644 docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceid.rst create mode 100644 docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst create mode 100644 docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst create mode 100644 docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst create mode 100644 docs/api/dynamips_vm.rst rename docs/api/{ => dynamips_vm}/v1projectsprojectiddynamipsvms.rst (99%) rename docs/api/{ => dynamips_vm}/v1projectsprojectiddynamipsvmsvmid.rst (99%) create mode 100644 docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst create mode 100644 docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst create mode 100644 docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst rename docs/api/{ => dynamips_vm}/v1projectsprojectiddynamipsvmsvmidreload.rst (89%) rename docs/api/{ => dynamips_vm}/v1projectsprojectiddynamipsvmsvmidresume.rst (89%) rename docs/api/{ => dynamips_vm}/v1projectsprojectiddynamipsvmsvmidstart.rst (89%) rename docs/api/{ => dynamips_vm}/v1projectsprojectiddynamipsvmsvmidstop.rst (89%) rename docs/api/{ => dynamips_vm}/v1projectsprojectiddynamipsvmsvmidsuspend.rst (89%) delete mode 100644 docs/api/examples/delete_projectsprojectid.txt delete mode 100644 docs/api/examples/delete_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt delete mode 100644 docs/api/examples/delete_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt delete mode 100644 docs/api/examples/get_interfaces.txt delete mode 100644 docs/api/examples/get_projectsprojectid.txt delete mode 100644 docs/api/examples/get_projectsprojectidiouvmsvmid.txt delete mode 100644 docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt delete mode 100644 docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt delete mode 100644 docs/api/examples/get_version.txt delete mode 100644 docs/api/examples/post_portsudp.txt delete mode 100644 docs/api/examples/post_projectsprojectidclose.txt delete mode 100644 docs/api/examples/post_projectsprojectidcommit.txt delete mode 100644 docs/api/examples/post_projectsprojectidiouvms.txt delete mode 100644 docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt delete mode 100644 docs/api/examples/post_projectsprojectidvirtualboxvms.txt delete mode 100644 docs/api/examples/post_projectsprojectidvpcsvms.txt delete mode 100644 docs/api/examples/post_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt delete mode 100644 docs/api/examples/post_version.txt delete mode 100644 docs/api/examples/put_projectsprojectid.txt create mode 100644 docs/api/iou.rst rename docs/api/{ => iou}/v1projectsprojectidiouvms.rst (98%) rename docs/api/{ => iou}/v1projectsprojectidiouvmsvmid.rst (98%) rename docs/api/{ => iou}/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst (94%) create mode 100644 docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst create mode 100644 docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst rename docs/api/{ => iou}/v1projectsprojectidiouvmsvmidreload.rst (89%) rename docs/api/{ => iou}/v1projectsprojectidiouvmsvmidstart.rst (89%) rename docs/api/{ => iou}/v1projectsprojectidiouvmsvmidstop.rst (89%) create mode 100644 docs/api/network.rst rename docs/api/{ => network}/v1interfaces.rst (83%) rename docs/api/{ => network}/v1portsudp.rst (83%) create mode 100644 docs/api/project.rst rename docs/api/{ => project}/v1projects.rst (96%) rename docs/api/{ => project}/v1projectsprojectid.rst (97%) rename docs/api/{ => project}/v1projectsprojectidclose.rst (87%) rename docs/api/{ => project}/v1projectsprojectidcommit.rst (87%) create mode 100644 docs/api/version.rst rename docs/api/{ => version}/v1version.rst (95%) create mode 100644 docs/api/virtualbox.rst rename docs/api/{ => virtualbox}/v1projectsprojectidvirtualboxvms.rst (98%) rename docs/api/{ => virtualbox}/v1projectsprojectidvirtualboxvmsvmid.rst (98%) create mode 100644 docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst create mode 100644 docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst create mode 100644 docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst rename docs/api/{ => virtualbox}/v1projectsprojectidvirtualboxvmsvmidreload.rst (89%) rename docs/api/{ => virtualbox}/v1projectsprojectidvirtualboxvmsvmidresume.rst (89%) rename docs/api/{ => virtualbox}/v1projectsprojectidvirtualboxvmsvmidstart.rst (89%) rename docs/api/{ => virtualbox}/v1projectsprojectidvirtualboxvmsvmidstop.rst (89%) rename docs/api/{ => virtualbox}/v1projectsprojectidvirtualboxvmsvmidsuspend.rst (89%) rename docs/api/{ => virtualbox}/v1virtualboxvms.rst (83%) create mode 100644 docs/api/vpcs.rst rename docs/api/{ => vpcs}/v1projectsprojectidvpcsvms.rst (97%) rename docs/api/{ => vpcs}/v1projectsprojectidvpcsvmsvmid.rst (97%) rename docs/api/{ => vpcs}/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst (94%) rename docs/api/{ => vpcs}/v1projectsprojectidvpcsvmsvmidreload.rst (89%) rename docs/api/{ => vpcs}/v1projectsprojectidvpcsvmsvmidstart.rst (89%) rename docs/api/{ => vpcs}/v1projectsprojectidvpcsvmsvmidstop.rst (89%) diff --git a/docs/api/dynamips_device.rst b/docs/api/dynamips_device.rst new file mode 100644 index 00000000..83c17b94 --- /dev/null +++ b/docs/api/dynamips_device.rst @@ -0,0 +1,8 @@ +Dynamips device +--------------------- + +.. toctree:: + :glob: + :maxdepth: 2 + + dynamips_device/* diff --git a/docs/api/dynamips_device/v1projectsprojectiddynamipsdevices.rst b/docs/api/dynamips_device/v1projectsprojectiddynamipsdevices.rst new file mode 100644 index 00000000..d43ed5bf --- /dev/null +++ b/docs/api/dynamips_device/v1projectsprojectiddynamipsdevices.rst @@ -0,0 +1,43 @@ +/v1/projects/{project_id}/dynamips/devices +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/dynamips/devices +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Create a new Dynamips device instance + +Parameters +********** +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **201**: Instance created +- **409**: Conflict + +Input +******* +.. raw:: html + + + + + + +
Name Mandatory Type Description
device_id string Dynamips device instance identifier
device_type string Dynamips device type
name string Dynamips device name
+ +Output +******* +.. raw:: html + + + + + + + + +
Name Mandatory Type Description
device_id string Dynamips router instance UUID
mappings object
name string Dynamips device instance name
ports array
project_id string Project UUID
+ diff --git a/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceid.rst b/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceid.rst new file mode 100644 index 00000000..1ff726ba --- /dev/null +++ b/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceid.rst @@ -0,0 +1,106 @@ +/v1/projects/{project_id}/dynamips/devices/{device_id} +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +GET /v1/projects/**{project_id}**/dynamips/devices/**{device_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Get a Dynamips device instance + +Parameters +********** +- **device_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **200**: Success +- **400**: Invalid request +- **404**: Instance doesn't exist + +Output +******* +.. raw:: html + + + + + + + + +
Name Mandatory Type Description
device_id string Dynamips router instance UUID
mappings object
name string Dynamips device instance name
ports array
project_id string Project UUID
+ + +PUT /v1/projects/**{project_id}**/dynamips/devices/**{device_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Update a Dynamips device instance + +Parameters +********** +- **device_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **200**: Instance updated +- **400**: Invalid request +- **404**: Instance doesn't exist +- **409**: Conflict + +Input +******* +Types ++++++++++ +EthernetSwitchPort +^^^^^^^^^^^^^^^^^^^^^^ +Ethernet switch port + +.. raw:: html + + + + + + +
Name Mandatory Type Description
port integer Port number
type enum Possible values: access, dot1q, qinq
vlan integer VLAN number
+ +Body ++++++++++ +.. raw:: html + + + + + +
Name Mandatory Type Description
name string Dynamips device instance name
ports array
+ +Output +******* +.. raw:: html + + + + + + + + +
Name Mandatory Type Description
device_id string Dynamips router instance UUID
mappings object
name string Dynamips device instance name
ports array
project_id string Project UUID
+ + +DELETE /v1/projects/**{project_id}**/dynamips/devices/**{device_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Delete a Dynamips device instance + +Parameters +********** +- **device_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance deleted + diff --git a/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst b/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst new file mode 100644 index 00000000..bb237012 --- /dev/null +++ b/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst @@ -0,0 +1,140 @@ +/v1/projects/{project_id}/dynamips/devices/{device_id}/ports/{port_number:\d+}/nio +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/dynamips/devices/**{device_id}**/ports/**{port_number:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Add a NIO to a Dynamips device instance + +Parameters +********** +- **port_number**: Port on the device +- **device_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **201**: NIO created +- **404**: Instance doesn't exist + +Input +******* +Types ++++++++++ +Ethernet +^^^^^^^^^^^^^^^^^^^^^^ +Generic Ethernet Network Input/Output + +.. raw:: html + + + + + +
Name Mandatory Type Description
ethernet_device string Ethernet device name e.g. eth0
type enum Possible values: nio_generic_ethernet
+ +LinuxEthernet +^^^^^^^^^^^^^^^^^^^^^^ +Linux Ethernet Network Input/Output + +.. raw:: html + + + + + +
Name Mandatory Type Description
ethernet_device string Ethernet device name e.g. eth0
type enum Possible values: nio_linux_ethernet
+ +NULL +^^^^^^^^^^^^^^^^^^^^^^ +NULL Network Input/Output + +.. raw:: html + + + + +
Name Mandatory Type Description
type enum Possible values: nio_null
+ +TAP +^^^^^^^^^^^^^^^^^^^^^^ +TAP Network Input/Output + +.. raw:: html + + + + + +
Name Mandatory Type Description
tap_device string TAP device name e.g. tap0
type enum Possible values: nio_tap
+ +UDP +^^^^^^^^^^^^^^^^^^^^^^ +UDP Network Input/Output + +.. raw:: html + + + + + + + +
Name Mandatory Type Description
lport integer Local port
rhost string Remote host
rport integer Remote port
type enum Possible values: nio_udp
+ +UNIX +^^^^^^^^^^^^^^^^^^^^^^ +UNIX Network Input/Output + +.. raw:: html + + + + + + +
Name Mandatory Type Description
local_file string path to the UNIX socket file (local)
remote_file string path to the UNIX socket file (remote)
type enum Possible values: nio_unix
+ +VDE +^^^^^^^^^^^^^^^^^^^^^^ +VDE Network Input/Output + +.. raw:: html + + + + + + +
Name Mandatory Type Description
control_file string path to the VDE control file
local_file string path to the VDE control file
type enum Possible values: nio_vde
+ +Body ++++++++++ +.. raw:: html + + + + + + +
Name Mandatory Type Description
mappings object
nio UDP, Ethernet, LinuxEthernet, TAP, UNIX, VDE, NULL
port_settings object Ethernet switch
+ + +DELETE /v1/projects/**{project_id}**/dynamips/devices/**{device_id}**/ports/**{port_number:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Remove a NIO from a Dynamips device instance + +Parameters +********** +- **port_number**: Port on the device +- **device_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: NIO deleted + diff --git a/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst b/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst new file mode 100644 index 00000000..41cc168f --- /dev/null +++ b/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst @@ -0,0 +1,31 @@ +/v1/projects/{project_id}/dynamips/devices/{device_id}/ports/{port_number:\d+}/start_capture +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/dynamips/devices/**{device_id}**/ports/**{port_number:\d+}**/start_capture +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Start a packet capture on a Dynamips device instance + +Parameters +********** +- **port_number**: Port on the device +- **device_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **200**: Capture started +- **400**: Invalid request +- **404**: Instance doesn't exist + +Input +******* +.. raw:: html + + + + + +
Name Mandatory Type Description
capture_file_name string Capture file name
data_link_type string PCAP data link type
+ diff --git a/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst b/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst new file mode 100644 index 00000000..9318bdd1 --- /dev/null +++ b/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst @@ -0,0 +1,21 @@ +/v1/projects/{project_id}/dynamips/devices/{device_id}/ports/{port_number:\d+}/stop_capture +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/dynamips/devices/**{device_id}**/ports/**{port_number:\d+}**/stop_capture +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Stop a packet capture on a Dynamips device instance + +Parameters +********** +- **port_number**: Port on the device +- **device_id**: UUID for the instance +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Capture stopped + diff --git a/docs/api/dynamips_vm.rst b/docs/api/dynamips_vm.rst new file mode 100644 index 00000000..f32d26b7 --- /dev/null +++ b/docs/api/dynamips_vm.rst @@ -0,0 +1,8 @@ +Dynamips vm +--------------------- + +.. toctree:: + :glob: + :maxdepth: 2 + + dynamips_vm/* diff --git a/docs/api/v1projectsprojectiddynamipsvms.rst b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvms.rst similarity index 99% rename from docs/api/v1projectsprojectiddynamipsvms.rst rename to docs/api/dynamips_vm/v1projectsprojectiddynamipsvms.rst index 410f5f87..b50dfdad 100644 --- a/docs/api/v1projectsprojectiddynamipsvms.rst +++ b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvms.rst @@ -1,10 +1,10 @@ /v1/projects/{project_id}/dynamips/vms ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: POST /v1/projects/**{project_id}**/dynamips/vms -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Create a new Dynamips VM instance Parameters diff --git a/docs/api/v1projectsprojectiddynamipsvmsvmid.rst b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmid.rst similarity index 99% rename from docs/api/v1projectsprojectiddynamipsvmsvmid.rst rename to docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmid.rst index bce8b927..6eb63363 100644 --- a/docs/api/v1projectsprojectiddynamipsvmsvmid.rst +++ b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmid.rst @@ -1,10 +1,10 @@ /v1/projects/{project_id}/dynamips/vms/{vm_id} ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: GET /v1/projects/**{project_id}**/dynamips/vms/**{vm_id}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Get a Dynamips VM instance Parameters @@ -70,7 +70,7 @@ Output PUT /v1/projects/**{project_id}**/dynamips/vms/**{vm_id}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Update a Dynamips VM instance Parameters @@ -184,7 +184,7 @@ Output DELETE /v1/projects/**{project_id}**/dynamips/vms/**{vm_id}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Delete a Dynamips VM instance Parameters diff --git a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst new file mode 100644 index 00000000..ddff9476 --- /dev/null +++ b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -0,0 +1,40 @@ +/v1/projects/{project_id}/dynamips/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/dynamips/vms/**{vm_id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Add a NIO to a Dynamips VM instance + +Parameters +********** +- **adapter_number**: Adapter where the nio should be added +- **vm_id**: UUID for the instance +- **port_number**: Port on the adapter +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **201**: NIO created +- **404**: Instance doesn't exist + + +DELETE /v1/projects/**{project_id}**/dynamips/vms/**{vm_id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Remove a NIO from a Dynamips VM instance + +Parameters +********** +- **adapter_number**: Adapter from where the nio should be removed +- **vm_id**: UUID for the instance +- **port_number**: Port on the adapter +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: NIO deleted + diff --git a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst new file mode 100644 index 00000000..43c9c0fe --- /dev/null +++ b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst @@ -0,0 +1,32 @@ +/v1/projects/{project_id}/dynamips/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/start_capture +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/dynamips/vms/**{vm_id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/start_capture +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Start a packet capture on a Dynamips VM instance + +Parameters +********** +- **adapter_number**: Adapter to start a packet capture +- **vm_id**: UUID for the instance +- **port_number**: Port on the adapter +- **project_id**: UUID for the project + +Response status codes +********************** +- **200**: Capture started +- **400**: Invalid request +- **404**: Instance doesn't exist + +Input +******* +.. raw:: html + + + + + +
Name Mandatory Type Description
capture_file_name string Capture file name
data_link_type string PCAP data link type
+ diff --git a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst new file mode 100644 index 00000000..1114fae5 --- /dev/null +++ b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst @@ -0,0 +1,22 @@ +/v1/projects/{project_id}/dynamips/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/stop_capture +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/dynamips/vms/**{vm_id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/stop_capture +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Stop a packet capture on a Dynamips VM instance + +Parameters +********** +- **adapter_number**: Adapter to stop a packet capture +- **vm_id**: UUID for the instance +- **port_number**: Port on the adapter (always 0) +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Capture stopped + diff --git a/docs/api/v1projectsprojectiddynamipsvmsvmidreload.rst b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidreload.rst similarity index 89% rename from docs/api/v1projectsprojectiddynamipsvmsvmidreload.rst rename to docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidreload.rst index a1544bf0..d7d7df55 100644 --- a/docs/api/v1projectsprojectiddynamipsvmsvmidreload.rst +++ b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidreload.rst @@ -1,10 +1,10 @@ /v1/projects/{project_id}/dynamips/vms/{vm_id}/reload ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: POST /v1/projects/**{project_id}**/dynamips/vms/**{vm_id}**/reload -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Reload a Dynamips VM instance Parameters diff --git a/docs/api/v1projectsprojectiddynamipsvmsvmidresume.rst b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidresume.rst similarity index 89% rename from docs/api/v1projectsprojectiddynamipsvmsvmidresume.rst rename to docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidresume.rst index 9e5ca67c..fcd48ab5 100644 --- a/docs/api/v1projectsprojectiddynamipsvmsvmidresume.rst +++ b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidresume.rst @@ -1,10 +1,10 @@ /v1/projects/{project_id}/dynamips/vms/{vm_id}/resume ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: POST /v1/projects/**{project_id}**/dynamips/vms/**{vm_id}**/resume -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Resume a suspended Dynamips VM instance Parameters diff --git a/docs/api/v1projectsprojectiddynamipsvmsvmidstart.rst b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidstart.rst similarity index 89% rename from docs/api/v1projectsprojectiddynamipsvmsvmidstart.rst rename to docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidstart.rst index 446d6bca..2dbd25b8 100644 --- a/docs/api/v1projectsprojectiddynamipsvmsvmidstart.rst +++ b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidstart.rst @@ -1,10 +1,10 @@ /v1/projects/{project_id}/dynamips/vms/{vm_id}/start ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: POST /v1/projects/**{project_id}**/dynamips/vms/**{vm_id}**/start -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Start a Dynamips VM instance Parameters diff --git a/docs/api/v1projectsprojectiddynamipsvmsvmidstop.rst b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidstop.rst similarity index 89% rename from docs/api/v1projectsprojectiddynamipsvmsvmidstop.rst rename to docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidstop.rst index 9663fc78..ff62c2c9 100644 --- a/docs/api/v1projectsprojectiddynamipsvmsvmidstop.rst +++ b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidstop.rst @@ -1,10 +1,10 @@ /v1/projects/{project_id}/dynamips/vms/{vm_id}/stop ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: POST /v1/projects/**{project_id}**/dynamips/vms/**{vm_id}**/stop -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Stop a Dynamips VM instance Parameters diff --git a/docs/api/v1projectsprojectiddynamipsvmsvmidsuspend.rst b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidsuspend.rst similarity index 89% rename from docs/api/v1projectsprojectiddynamipsvmsvmidsuspend.rst rename to docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidsuspend.rst index d53b213f..b6fb8a13 100644 --- a/docs/api/v1projectsprojectiddynamipsvmsvmidsuspend.rst +++ b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidsuspend.rst @@ -1,10 +1,10 @@ /v1/projects/{project_id}/dynamips/vms/{vm_id}/suspend ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: POST /v1/projects/**{project_id}**/dynamips/vms/**{vm_id}**/suspend -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Suspend a Dynamips VM instance Parameters diff --git a/docs/api/examples/delete_projectsprojectid.txt b/docs/api/examples/delete_projectsprojectid.txt deleted file mode 100644 index 45efff6c..00000000 --- a/docs/api/examples/delete_projectsprojectid.txt +++ /dev/null @@ -1,13 +0,0 @@ -curl -i -X DELETE 'http://localhost:8000/projects/{project_id}' - -DELETE /projects/{project_id} HTTP/1.1 - - - -HTTP/1.1 204 -CONNECTION: keep-alive -CONTENT-LENGTH: 0 -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /v1/projects/{project_id} - diff --git a/docs/api/examples/delete_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt b/docs/api/examples/delete_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt deleted file mode 100644 index f8aa407f..00000000 --- a/docs/api/examples/delete_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt +++ /dev/null @@ -1,13 +0,0 @@ -curl -i -X DELETE 'http://localhost:8000/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio' - -DELETE /projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio HTTP/1.1 - - - -HTTP/1.1 204 -CONNECTION: keep-alive -CONTENT-LENGTH: 0 -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /v1/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio - diff --git a/docs/api/examples/delete_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt b/docs/api/examples/delete_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt deleted file mode 100644 index 6842905a..00000000 --- a/docs/api/examples/delete_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt +++ /dev/null @@ -1,13 +0,0 @@ -curl -i -X DELETE 'http://localhost:8000/projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio' - -DELETE /projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio HTTP/1.1 - - - -HTTP/1.1 204 -CONNECTION: keep-alive -CONTENT-LENGTH: 0 -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /v1/projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio - diff --git a/docs/api/examples/get_interfaces.txt b/docs/api/examples/get_interfaces.txt deleted file mode 100644 index 7ac8c74d..00000000 --- a/docs/api/examples/get_interfaces.txt +++ /dev/null @@ -1,60 +0,0 @@ -curl -i -X GET 'http://localhost:8000/interfaces' - -GET /interfaces HTTP/1.1 - - - -HTTP/1.1 200 -CONNECTION: keep-alive -CONTENT-LENGTH: 652 -CONTENT-TYPE: application/json -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /v1/interfaces - -[ - { - "id": "lo0", - "name": "lo0" - }, - { - "id": "gif0", - "name": "gif0" - }, - { - "id": "stf0", - "name": "stf0" - }, - { - "id": "en0", - "name": "en0" - }, - { - "id": "en1", - "name": "en1" - }, - { - "id": "fw0", - "name": "fw0" - }, - { - "id": "en2", - "name": "en2" - }, - { - "id": "p2p0", - "name": "p2p0" - }, - { - "id": "bridge0", - "name": "bridge0" - }, - { - "id": "vboxnet0", - "name": "vboxnet0" - }, - { - "id": "vboxnet1", - "name": "vboxnet1" - } -] diff --git a/docs/api/examples/get_projectsprojectid.txt b/docs/api/examples/get_projectsprojectid.txt deleted file mode 100644 index 0823bf08..00000000 --- a/docs/api/examples/get_projectsprojectid.txt +++ /dev/null @@ -1,20 +0,0 @@ -curl -i -X GET 'http://localhost:8000/projects/{project_id}' - -GET /projects/{project_id} HTTP/1.1 - - - -HTTP/1.1 200 -CONNECTION: keep-alive -CONTENT-LENGTH: 277 -CONTENT-TYPE: application/json -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /v1/projects/{project_id} - -{ - "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpd4_jlup7", - "path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpd4_jlup7/00010203-0405-0607-0809-0a0b0c0d0e0f", - "project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f", - "temporary": false -} diff --git a/docs/api/examples/get_projectsprojectidiouvmsvmid.txt b/docs/api/examples/get_projectsprojectidiouvmsvmid.txt deleted file mode 100644 index e702d104..00000000 --- a/docs/api/examples/get_projectsprojectidiouvmsvmid.txt +++ /dev/null @@ -1,26 +0,0 @@ -curl -i -X GET 'http://localhost:8000/projects/{project_id}/iou/vms/{vm_id}' - -GET /projects/{project_id}/iou/vms/{vm_id} HTTP/1.1 - - - -HTTP/1.1 200 -CONNECTION: keep-alive -CONTENT-LENGTH: 381 -CONTENT-TYPE: application/json -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /v1/projects/{project_id}/iou/vms/{vm_id} - -{ - "console": 2000, - "ethernet_adapters": 2, - "l1_keepalives": false, - "name": "PC TEST 1", - "nvram": 128, - "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2628/test_iou_get0/iou.bin", - "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "ram": 256, - "serial_adapters": 2, - "vm_id": "8d4ce7ee-9c5e-4ac0-9106-f8cf28a12b5d" -} diff --git a/docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt b/docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt deleted file mode 100644 index d87a9f2a..00000000 --- a/docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt +++ /dev/null @@ -1,26 +0,0 @@ -curl -i -X GET 'http://localhost:8000/projects/{project_id}/virtualbox/vms/{vm_id}' - -GET /projects/{project_id}/virtualbox/vms/{vm_id} HTTP/1.1 - - - -HTTP/1.1 200 -CONNECTION: keep-alive -CONTENT-LENGTH: 347 -CONTENT-TYPE: application/json -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /v1/projects/{project_id}/virtualbox/vms/{vm_id} - -{ - "adapter_type": "Intel PRO/1000 MT Desktop (82540EM)", - "adapters": 0, - "console": 2001, - "enable_remote_console": false, - "headless": false, - "name": "VMTEST", - "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "use_any_adapter": false, - "vm_id": "591dcdfc-f25f-4a87-bd06-6091551c6f8e", - "vmname": "VMTEST" -} diff --git a/docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt b/docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt deleted file mode 100644 index 83c6f71d..00000000 --- a/docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt +++ /dev/null @@ -1,21 +0,0 @@ -curl -i -X GET 'http://localhost:8000/projects/{project_id}/vpcs/vms/{vm_id}' - -GET /projects/{project_id}/vpcs/vms/{vm_id} HTTP/1.1 - - - -HTTP/1.1 200 -CONNECTION: keep-alive -CONTENT-LENGTH: 187 -CONTENT-TYPE: application/json -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /v1/projects/{project_id}/vpcs/vms/{vm_id} - -{ - "console": 2009, - "name": "PC TEST 1", - "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "startup_script": null, - "vm_id": "37726103-1521-42c3-8925-fd4fabea9caf" -} diff --git a/docs/api/examples/get_version.txt b/docs/api/examples/get_version.txt deleted file mode 100644 index 88017034..00000000 --- a/docs/api/examples/get_version.txt +++ /dev/null @@ -1,17 +0,0 @@ -curl -i -X GET 'http://localhost:8000/version' - -GET /version HTTP/1.1 - - - -HTTP/1.1 200 -CONNECTION: keep-alive -CONTENT-LENGTH: 29 -CONTENT-TYPE: application/json -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /v1/version - -{ - "version": "1.3.dev1" -} diff --git a/docs/api/examples/post_portsudp.txt b/docs/api/examples/post_portsudp.txt deleted file mode 100644 index 3be4b74c..00000000 --- a/docs/api/examples/post_portsudp.txt +++ /dev/null @@ -1,17 +0,0 @@ -curl -i -X POST 'http://localhost:8000/ports/udp' -d '{}' - -POST /ports/udp HTTP/1.1 -{} - - -HTTP/1.1 201 -CONNECTION: keep-alive -CONTENT-LENGTH: 25 -CONTENT-TYPE: application/json -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /v1/ports/udp - -{ - "udp_port": 10000 -} diff --git a/docs/api/examples/post_projectsprojectidclose.txt b/docs/api/examples/post_projectsprojectidclose.txt deleted file mode 100644 index bcc429c9..00000000 --- a/docs/api/examples/post_projectsprojectidclose.txt +++ /dev/null @@ -1,13 +0,0 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/close' -d '{}' - -POST /projects/{project_id}/close HTTP/1.1 -{} - - -HTTP/1.1 204 -CONNECTION: keep-alive -CONTENT-LENGTH: 0 -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /v1/projects/{project_id}/close - diff --git a/docs/api/examples/post_projectsprojectidcommit.txt b/docs/api/examples/post_projectsprojectidcommit.txt deleted file mode 100644 index 0b36f05d..00000000 --- a/docs/api/examples/post_projectsprojectidcommit.txt +++ /dev/null @@ -1,13 +0,0 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/commit' -d '{}' - -POST /projects/{project_id}/commit HTTP/1.1 -{} - - -HTTP/1.1 204 -CONNECTION: keep-alive -CONTENT-LENGTH: 0 -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /v1/projects/{project_id}/commit - diff --git a/docs/api/examples/post_projectsprojectidiouvms.txt b/docs/api/examples/post_projectsprojectidiouvms.txt deleted file mode 100644 index 9f9a6d91..00000000 --- a/docs/api/examples/post_projectsprojectidiouvms.txt +++ /dev/null @@ -1,36 +0,0 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/iou/vms' -d '{"ethernet_adapters": 0, "initial_config": "hostname test", "iourc_path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2628/test_iou_create_with_params0/iourc", "l1_keepalives": true, "name": "PC TEST 1", "nvram": 512, "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2628/test_iou_create_with_params0/iou.bin", "ram": 1024, "serial_adapters": 4}' - -POST /projects/{project_id}/iou/vms HTTP/1.1 -{ - "ethernet_adapters": 0, - "initial_config": "hostname test", - "iourc_path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2628/test_iou_create_with_params0/iourc", - "l1_keepalives": true, - "name": "PC TEST 1", - "nvram": 512, - "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2628/test_iou_create_with_params0/iou.bin", - "ram": 1024, - "serial_adapters": 4 -} - - -HTTP/1.1 201 -CONNECTION: keep-alive -CONTENT-LENGTH: 396 -CONTENT-TYPE: application/json -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /v1/projects/{project_id}/iou/vms - -{ - "console": 2000, - "ethernet_adapters": 0, - "l1_keepalives": true, - "name": "PC TEST 1", - "nvram": 512, - "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2628/test_iou_create_with_params0/iou.bin", - "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "ram": 1024, - "serial_adapters": 4, - "vm_id": "f1bafbbe-96ba-4088-83e2-391cf9477e89" -} diff --git a/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt b/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt deleted file mode 100644 index 5940e201..00000000 --- a/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt +++ /dev/null @@ -1,21 +0,0 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio' -d '{"ethernet_device": "eth0", "type": "nio_generic_ethernet"}' - -POST /projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio HTTP/1.1 -{ - "ethernet_device": "eth0", - "type": "nio_generic_ethernet" -} - - -HTTP/1.1 201 -CONNECTION: keep-alive -CONTENT-LENGTH: 69 -CONTENT-TYPE: application/json -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /v1/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio - -{ - "ethernet_device": "eth0", - "type": "nio_generic_ethernet" -} diff --git a/docs/api/examples/post_projectsprojectidvirtualboxvms.txt b/docs/api/examples/post_projectsprojectidvirtualboxvms.txt deleted file mode 100644 index 751f5867..00000000 --- a/docs/api/examples/post_projectsprojectidvirtualboxvms.txt +++ /dev/null @@ -1,30 +0,0 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/virtualbox/vms' -d '{"linked_clone": false, "name": "VM1", "vmname": "VM1"}' - -POST /projects/{project_id}/virtualbox/vms HTTP/1.1 -{ - "linked_clone": false, - "name": "VM1", - "vmname": "VM1" -} - - -HTTP/1.1 201 -CONNECTION: keep-alive -CONTENT-LENGTH: 341 -CONTENT-TYPE: application/json -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /v1/projects/{project_id}/virtualbox/vms - -{ - "adapter_type": "Intel PRO/1000 MT Desktop (82540EM)", - "adapters": 0, - "console": 2000, - "enable_remote_console": false, - "headless": false, - "name": "VM1", - "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "use_any_adapter": false, - "vm_id": "be653307-d7d6-4884-932f-0d87c7e2c06b", - "vmname": "VM1" -} diff --git a/docs/api/examples/post_projectsprojectidvpcsvms.txt b/docs/api/examples/post_projectsprojectidvpcsvms.txt deleted file mode 100644 index 9ed92782..00000000 --- a/docs/api/examples/post_projectsprojectidvpcsvms.txt +++ /dev/null @@ -1,23 +0,0 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/vpcs/vms' -d '{"name": "PC TEST 1"}' - -POST /projects/{project_id}/vpcs/vms HTTP/1.1 -{ - "name": "PC TEST 1" -} - - -HTTP/1.1 201 -CONNECTION: keep-alive -CONTENT-LENGTH: 187 -CONTENT-TYPE: application/json -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /v1/projects/{project_id}/vpcs/vms - -{ - "console": 2009, - "name": "PC TEST 1", - "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "startup_script": null, - "vm_id": "154fff9a-bd47-4740-8a00-847ec30dd6e0" -} diff --git a/docs/api/examples/post_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt b/docs/api/examples/post_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt deleted file mode 100644 index e55c4ace..00000000 --- a/docs/api/examples/post_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt +++ /dev/null @@ -1,25 +0,0 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio' -d '{"lport": 4242, "rhost": "127.0.0.1", "rport": 4343, "type": "nio_udp"}' - -POST /projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio HTTP/1.1 -{ - "lport": 4242, - "rhost": "127.0.0.1", - "rport": 4343, - "type": "nio_udp" -} - - -HTTP/1.1 201 -CONNECTION: keep-alive -CONTENT-LENGTH: 89 -CONTENT-TYPE: application/json -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /v1/projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio - -{ - "lport": 4242, - "rhost": "127.0.0.1", - "rport": 4343, - "type": "nio_udp" -} diff --git a/docs/api/examples/post_version.txt b/docs/api/examples/post_version.txt deleted file mode 100644 index 2f6c1452..00000000 --- a/docs/api/examples/post_version.txt +++ /dev/null @@ -1,19 +0,0 @@ -curl -i -X POST 'http://localhost:8000/version' -d '{"version": "1.3.dev1"}' - -POST /version HTTP/1.1 -{ - "version": "1.3.dev1" -} - - -HTTP/1.1 200 -CONNECTION: keep-alive -CONTENT-LENGTH: 29 -CONTENT-TYPE: application/json -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /v1/version - -{ - "version": "1.3.dev1" -} diff --git a/docs/api/examples/put_projectsprojectid.txt b/docs/api/examples/put_projectsprojectid.txt deleted file mode 100644 index ebab8846..00000000 --- a/docs/api/examples/put_projectsprojectid.txt +++ /dev/null @@ -1,20 +0,0 @@ -curl -i -X PUT 'http://localhost:8000/projects/{project_id}' -d '{"path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2628/test_update_path_project_non_l0"}' - -PUT /projects/{project_id} HTTP/1.1 -{ - "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-2628/test_update_path_project_non_l0" -} - - -HTTP/1.1 403 -CONNECTION: keep-alive -CONTENT-LENGTH: 100 -CONTENT-TYPE: application/json -DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.dev1 -X-ROUTE: /v1/projects/{project_id} - -{ - "message": "You are not allowed to modify the project directory location", - "status": 403 -} diff --git a/docs/api/iou.rst b/docs/api/iou.rst new file mode 100644 index 00000000..c2188031 --- /dev/null +++ b/docs/api/iou.rst @@ -0,0 +1,8 @@ +Iou +--------------------- + +.. toctree:: + :glob: + :maxdepth: 2 + + iou/* diff --git a/docs/api/v1projectsprojectidiouvms.rst b/docs/api/iou/v1projectsprojectidiouvms.rst similarity index 98% rename from docs/api/v1projectsprojectidiouvms.rst rename to docs/api/iou/v1projectsprojectidiouvms.rst index 978cf3ff..92cebf36 100644 --- a/docs/api/v1projectsprojectidiouvms.rst +++ b/docs/api/iou/v1projectsprojectidiouvms.rst @@ -1,10 +1,10 @@ /v1/projects/{project_id}/iou/vms ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: POST /v1/projects/**{project_id}**/iou/vms -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Create a new IOU instance Parameters diff --git a/docs/api/v1projectsprojectidiouvmsvmid.rst b/docs/api/iou/v1projectsprojectidiouvmsvmid.rst similarity index 98% rename from docs/api/v1projectsprojectidiouvmsvmid.rst rename to docs/api/iou/v1projectsprojectidiouvmsvmid.rst index e3dd7611..0fdbe2e2 100644 --- a/docs/api/v1projectsprojectidiouvmsvmid.rst +++ b/docs/api/iou/v1projectsprojectidiouvmsvmid.rst @@ -1,10 +1,10 @@ /v1/projects/{project_id}/iou/vms/{vm_id} ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: GET /v1/projects/**{project_id}**/iou/vms/**{vm_id}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Get a IOU instance Parameters @@ -38,7 +38,7 @@ Output PUT /v1/projects/**{project_id}**/iou/vms/**{vm_id}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Update a IOU instance Parameters @@ -91,7 +91,7 @@ Output DELETE /v1/projects/**{project_id}**/iou/vms/**{vm_id}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Delete a IOU instance Parameters diff --git a/docs/api/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst similarity index 94% rename from docs/api/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst rename to docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst index 56437eec..9e2ce5a8 100644 --- a/docs/api/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -1,18 +1,18 @@ /v1/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: POST /v1/projects/**{project_id}**/iou/vms/**{vm_id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/nio -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Add a NIO to a IOU instance Parameters ********** - **adapter_number**: Network adapter where the nio is located - **vm_id**: UUID for the instance -- **project_id**: UUID for the project - **port_number**: Port where the nio should be added +- **project_id**: UUID for the project Response status codes ********************** @@ -22,15 +22,15 @@ Response status codes DELETE /v1/projects/**{project_id}**/iou/vms/**{vm_id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/nio -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Remove a NIO from a IOU instance Parameters ********** - **adapter_number**: Network adapter where the nio is located - **vm_id**: UUID for the instance -- **project_id**: UUID for the project - **port_number**: Port from where the nio should be removed +- **project_id**: UUID for the project Response status codes ********************** diff --git a/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst b/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst new file mode 100644 index 00000000..fdd481c9 --- /dev/null +++ b/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst @@ -0,0 +1,32 @@ +/v1/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/start_capture +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/iou/vms/**{vm_id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/start_capture +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Start a packet capture on a IOU VM instance + +Parameters +********** +- **adapter_number**: Adapter to start a packet capture +- **vm_id**: UUID for the instance +- **port_number**: Port on the adapter +- **project_id**: UUID for the project + +Response status codes +********************** +- **200**: Capture started +- **400**: Invalid request +- **404**: Instance doesn't exist + +Input +******* +.. raw:: html + + + + + +
Name Mandatory Type Description
capture_file_name string Capture file name
data_link_type string PCAP data link type
+ diff --git a/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst b/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst new file mode 100644 index 00000000..a2441a3d --- /dev/null +++ b/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst @@ -0,0 +1,22 @@ +/v1/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/stop_capture +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/iou/vms/**{vm_id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/stop_capture +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Stop a packet capture on a IOU VM instance + +Parameters +********** +- **adapter_number**: Adapter to stop a packet capture +- **vm_id**: UUID for the instance +- **port_number**: Port on the adapter (always 0) +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Capture stopped + diff --git a/docs/api/v1projectsprojectidiouvmsvmidreload.rst b/docs/api/iou/v1projectsprojectidiouvmsvmidreload.rst similarity index 89% rename from docs/api/v1projectsprojectidiouvmsvmidreload.rst rename to docs/api/iou/v1projectsprojectidiouvmsvmidreload.rst index 33c1d767..90f3c56b 100644 --- a/docs/api/v1projectsprojectidiouvmsvmidreload.rst +++ b/docs/api/iou/v1projectsprojectidiouvmsvmidreload.rst @@ -1,10 +1,10 @@ /v1/projects/{project_id}/iou/vms/{vm_id}/reload ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: POST /v1/projects/**{project_id}**/iou/vms/**{vm_id}**/reload -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Reload a IOU instance Parameters diff --git a/docs/api/v1projectsprojectidiouvmsvmidstart.rst b/docs/api/iou/v1projectsprojectidiouvmsvmidstart.rst similarity index 89% rename from docs/api/v1projectsprojectidiouvmsvmidstart.rst rename to docs/api/iou/v1projectsprojectidiouvmsvmidstart.rst index 3956f666..c781dfc3 100644 --- a/docs/api/v1projectsprojectidiouvmsvmidstart.rst +++ b/docs/api/iou/v1projectsprojectidiouvmsvmidstart.rst @@ -1,10 +1,10 @@ /v1/projects/{project_id}/iou/vms/{vm_id}/start ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: POST /v1/projects/**{project_id}**/iou/vms/**{vm_id}**/start -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Start a IOU instance Parameters diff --git a/docs/api/v1projectsprojectidiouvmsvmidstop.rst b/docs/api/iou/v1projectsprojectidiouvmsvmidstop.rst similarity index 89% rename from docs/api/v1projectsprojectidiouvmsvmidstop.rst rename to docs/api/iou/v1projectsprojectidiouvmsvmidstop.rst index 860b566d..4f60ad43 100644 --- a/docs/api/v1projectsprojectidiouvmsvmidstop.rst +++ b/docs/api/iou/v1projectsprojectidiouvmsvmidstop.rst @@ -1,10 +1,10 @@ /v1/projects/{project_id}/iou/vms/{vm_id}/stop ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: POST /v1/projects/**{project_id}**/iou/vms/**{vm_id}**/stop -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Stop a IOU instance Parameters diff --git a/docs/api/network.rst b/docs/api/network.rst new file mode 100644 index 00000000..38366abe --- /dev/null +++ b/docs/api/network.rst @@ -0,0 +1,8 @@ +Network +--------------------- + +.. toctree:: + :glob: + :maxdepth: 2 + + network/* diff --git a/docs/api/v1interfaces.rst b/docs/api/network/v1interfaces.rst similarity index 83% rename from docs/api/v1interfaces.rst rename to docs/api/network/v1interfaces.rst index 73042f7d..2a1071f3 100644 --- a/docs/api/v1interfaces.rst +++ b/docs/api/network/v1interfaces.rst @@ -1,10 +1,10 @@ /v1/interfaces ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: GET /v1/interfaces -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ List all the network interfaces available on the server Response status codes diff --git a/docs/api/v1portsudp.rst b/docs/api/network/v1portsudp.rst similarity index 83% rename from docs/api/v1portsudp.rst rename to docs/api/network/v1portsudp.rst index 69d90882..0d6f7975 100644 --- a/docs/api/v1portsudp.rst +++ b/docs/api/network/v1portsudp.rst @@ -1,10 +1,10 @@ /v1/ports/udp ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: POST /v1/ports/udp -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Allocate an UDP port on the server Response status codes diff --git a/docs/api/project.rst b/docs/api/project.rst new file mode 100644 index 00000000..95453d81 --- /dev/null +++ b/docs/api/project.rst @@ -0,0 +1,8 @@ +Project +--------------------- + +.. toctree:: + :glob: + :maxdepth: 2 + + project/* diff --git a/docs/api/v1projects.rst b/docs/api/project/v1projects.rst similarity index 96% rename from docs/api/v1projects.rst rename to docs/api/project/v1projects.rst index fadee814..9ace36fe 100644 --- a/docs/api/v1projects.rst +++ b/docs/api/project/v1projects.rst @@ -1,10 +1,10 @@ /v1/projects ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: POST /v1/projects -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Create a new project on the server Response status codes diff --git a/docs/api/v1projectsprojectid.rst b/docs/api/project/v1projectsprojectid.rst similarity index 97% rename from docs/api/v1projectsprojectid.rst rename to docs/api/project/v1projectsprojectid.rst index 4881beae..c1a70376 100644 --- a/docs/api/v1projectsprojectid.rst +++ b/docs/api/project/v1projectsprojectid.rst @@ -1,10 +1,10 @@ /v1/projects/{project_id} ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: GET /v1/projects/**{project_id}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Get project information Parameters @@ -30,7 +30,7 @@ Output PUT /v1/projects/**{project_id}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Update a project Parameters @@ -67,7 +67,7 @@ Output DELETE /v1/projects/**{project_id}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Delete a project from disk Parameters diff --git a/docs/api/v1projectsprojectidclose.rst b/docs/api/project/v1projectsprojectidclose.rst similarity index 87% rename from docs/api/v1projectsprojectidclose.rst rename to docs/api/project/v1projectsprojectidclose.rst index e3d9e87c..c0623c9b 100644 --- a/docs/api/v1projectsprojectidclose.rst +++ b/docs/api/project/v1projectsprojectidclose.rst @@ -1,10 +1,10 @@ /v1/projects/{project_id}/close ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: POST /v1/projects/**{project_id}**/close -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Close a project Parameters diff --git a/docs/api/v1projectsprojectidcommit.rst b/docs/api/project/v1projectsprojectidcommit.rst similarity index 87% rename from docs/api/v1projectsprojectidcommit.rst rename to docs/api/project/v1projectsprojectidcommit.rst index a3e0aac5..49c6fb8a 100644 --- a/docs/api/v1projectsprojectidcommit.rst +++ b/docs/api/project/v1projectsprojectidcommit.rst @@ -1,10 +1,10 @@ /v1/projects/{project_id}/commit ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: POST /v1/projects/**{project_id}**/commit -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Write changes on disk Parameters diff --git a/docs/api/version.rst b/docs/api/version.rst new file mode 100644 index 00000000..adc4c1f0 --- /dev/null +++ b/docs/api/version.rst @@ -0,0 +1,8 @@ +Version +--------------------- + +.. toctree:: + :glob: + :maxdepth: 2 + + version/* diff --git a/docs/api/v1version.rst b/docs/api/version/v1version.rst similarity index 95% rename from docs/api/v1version.rst rename to docs/api/version/v1version.rst index ae53f9c2..2fdf1edb 100644 --- a/docs/api/v1version.rst +++ b/docs/api/version/v1version.rst @@ -1,10 +1,10 @@ /v1/version ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: GET /v1/version -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Retrieve the server version number Response status codes @@ -22,7 +22,7 @@ Output POST /v1/version -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Check if version is the same as the server Response status codes diff --git a/docs/api/virtualbox.rst b/docs/api/virtualbox.rst new file mode 100644 index 00000000..517624b2 --- /dev/null +++ b/docs/api/virtualbox.rst @@ -0,0 +1,8 @@ +Virtualbox +--------------------- + +.. toctree:: + :glob: + :maxdepth: 2 + + virtualbox/* diff --git a/docs/api/v1projectsprojectidvirtualboxvms.rst b/docs/api/virtualbox/v1projectsprojectidvirtualboxvms.rst similarity index 98% rename from docs/api/v1projectsprojectidvirtualboxvms.rst rename to docs/api/virtualbox/v1projectsprojectidvirtualboxvms.rst index 2e0ce096..8f85e7a5 100644 --- a/docs/api/v1projectsprojectidvirtualboxvms.rst +++ b/docs/api/virtualbox/v1projectsprojectidvirtualboxvms.rst @@ -1,10 +1,10 @@ /v1/projects/{project_id}/virtualbox/vms ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: POST /v1/projects/**{project_id}**/virtualbox/vms -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Create a new VirtualBox VM instance Parameters diff --git a/docs/api/v1projectsprojectidvirtualboxvmsvmid.rst b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmid.rst similarity index 98% rename from docs/api/v1projectsprojectidvirtualboxvmsvmid.rst rename to docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmid.rst index 11274204..ffb8670a 100644 --- a/docs/api/v1projectsprojectidvirtualboxvmsvmid.rst +++ b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmid.rst @@ -1,10 +1,10 @@ /v1/projects/{project_id}/virtualbox/vms/{vm_id} ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: GET /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Get a VirtualBox VM instance Parameters @@ -38,7 +38,7 @@ Output PUT /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Update a VirtualBox VM instance Parameters @@ -89,7 +89,7 @@ Output DELETE /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Delete a VirtualBox VM instance Parameters diff --git a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst new file mode 100644 index 00000000..726fa494 --- /dev/null +++ b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -0,0 +1,40 @@ +/v1/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Add a NIO to a VirtualBox VM instance + +Parameters +********** +- **adapter_number**: Adapter where the nio should be added +- **vm_id**: UUID for the instance +- **port_number**: Port on the adapter (always 0) +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **201**: NIO created +- **404**: Instance doesn't exist + + +DELETE /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Remove a NIO from a VirtualBox VM instance + +Parameters +********** +- **adapter_number**: Adapter from where the nio should be removed +- **vm_id**: UUID for the instance +- **port_number**: Port on the adapter (always) +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: NIO deleted + diff --git a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst new file mode 100644 index 00000000..67ff14c4 --- /dev/null +++ b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst @@ -0,0 +1,31 @@ +/v1/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/start_capture +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/start_capture +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Start a packet capture on a VirtualBox VM instance + +Parameters +********** +- **adapter_number**: Adapter to start a packet capture +- **vm_id**: UUID for the instance +- **port_number**: Port on the adapter (always 0) +- **project_id**: UUID for the project + +Response status codes +********************** +- **200**: Capture started +- **400**: Invalid request +- **404**: Instance doesn't exist + +Input +******* +.. raw:: html + + + + +
Name Mandatory Type Description
capture_file_name string Capture file name
+ diff --git a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst new file mode 100644 index 00000000..1377be32 --- /dev/null +++ b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst @@ -0,0 +1,22 @@ +/v1/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/stop_capture +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/stop_capture +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Stop a packet capture on a VirtualBox VM instance + +Parameters +********** +- **adapter_number**: Adapter to stop a packet capture +- **vm_id**: UUID for the instance +- **port_number**: Port on the adapter (always 0) +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Capture stopped + diff --git a/docs/api/v1projectsprojectidvirtualboxvmsvmidreload.rst b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidreload.rst similarity index 89% rename from docs/api/v1projectsprojectidvirtualboxvmsvmidreload.rst rename to docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidreload.rst index d32b9e02..6001b143 100644 --- a/docs/api/v1projectsprojectidvirtualboxvmsvmidreload.rst +++ b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidreload.rst @@ -1,10 +1,10 @@ /v1/projects/{project_id}/virtualbox/vms/{vm_id}/reload ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: POST /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}**/reload -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Reload a VirtualBox VM instance Parameters diff --git a/docs/api/v1projectsprojectidvirtualboxvmsvmidresume.rst b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidresume.rst similarity index 89% rename from docs/api/v1projectsprojectidvirtualboxvmsvmidresume.rst rename to docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidresume.rst index 7d82060f..cfdc6edc 100644 --- a/docs/api/v1projectsprojectidvirtualboxvmsvmidresume.rst +++ b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidresume.rst @@ -1,10 +1,10 @@ /v1/projects/{project_id}/virtualbox/vms/{vm_id}/resume ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: POST /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}**/resume -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Resume a suspended VirtualBox VM instance Parameters diff --git a/docs/api/v1projectsprojectidvirtualboxvmsvmidstart.rst b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidstart.rst similarity index 89% rename from docs/api/v1projectsprojectidvirtualboxvmsvmidstart.rst rename to docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidstart.rst index 20a30a8a..695c2712 100644 --- a/docs/api/v1projectsprojectidvirtualboxvmsvmidstart.rst +++ b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidstart.rst @@ -1,10 +1,10 @@ /v1/projects/{project_id}/virtualbox/vms/{vm_id}/start ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: POST /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}**/start -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Start a VirtualBox VM instance Parameters diff --git a/docs/api/v1projectsprojectidvirtualboxvmsvmidstop.rst b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidstop.rst similarity index 89% rename from docs/api/v1projectsprojectidvirtualboxvmsvmidstop.rst rename to docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidstop.rst index cafcbc1c..838a33c6 100644 --- a/docs/api/v1projectsprojectidvirtualboxvmsvmidstop.rst +++ b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidstop.rst @@ -1,10 +1,10 @@ /v1/projects/{project_id}/virtualbox/vms/{vm_id}/stop ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: POST /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}**/stop -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Stop a VirtualBox VM instance Parameters diff --git a/docs/api/v1projectsprojectidvirtualboxvmsvmidsuspend.rst b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidsuspend.rst similarity index 89% rename from docs/api/v1projectsprojectidvirtualboxvmsvmidsuspend.rst rename to docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidsuspend.rst index 0652378e..a978baa5 100644 --- a/docs/api/v1projectsprojectidvirtualboxvmsvmidsuspend.rst +++ b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidsuspend.rst @@ -1,10 +1,10 @@ /v1/projects/{project_id}/virtualbox/vms/{vm_id}/suspend ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: POST /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}**/suspend -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Suspend a VirtualBox VM instance Parameters diff --git a/docs/api/v1virtualboxvms.rst b/docs/api/virtualbox/v1virtualboxvms.rst similarity index 83% rename from docs/api/v1virtualboxvms.rst rename to docs/api/virtualbox/v1virtualboxvms.rst index a174df6c..6499008b 100644 --- a/docs/api/v1virtualboxvms.rst +++ b/docs/api/virtualbox/v1virtualboxvms.rst @@ -1,10 +1,10 @@ /v1/virtualbox/vms ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: GET /v1/virtualbox/vms -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Get all VirtualBox VMs available Response status codes diff --git a/docs/api/vpcs.rst b/docs/api/vpcs.rst new file mode 100644 index 00000000..ab00c921 --- /dev/null +++ b/docs/api/vpcs.rst @@ -0,0 +1,8 @@ +Vpcs +--------------------- + +.. toctree:: + :glob: + :maxdepth: 2 + + vpcs/* diff --git a/docs/api/v1projectsprojectidvpcsvms.rst b/docs/api/vpcs/v1projectsprojectidvpcsvms.rst similarity index 97% rename from docs/api/v1projectsprojectidvpcsvms.rst rename to docs/api/vpcs/v1projectsprojectidvpcsvms.rst index 7dfb6bc9..c59992e6 100644 --- a/docs/api/v1projectsprojectidvpcsvms.rst +++ b/docs/api/vpcs/v1projectsprojectidvpcsvms.rst @@ -1,10 +1,10 @@ /v1/projects/{project_id}/vpcs/vms ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: POST /v1/projects/**{project_id}**/vpcs/vms -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Create a new VPCS instance Parameters diff --git a/docs/api/v1projectsprojectidvpcsvmsvmid.rst b/docs/api/vpcs/v1projectsprojectidvpcsvmsvmid.rst similarity index 97% rename from docs/api/v1projectsprojectidvpcsvmsvmid.rst rename to docs/api/vpcs/v1projectsprojectidvpcsvmsvmid.rst index 50268c7b..f49c5964 100644 --- a/docs/api/v1projectsprojectidvpcsvmsvmid.rst +++ b/docs/api/vpcs/v1projectsprojectidvpcsvmsvmid.rst @@ -1,10 +1,10 @@ /v1/projects/{project_id}/vpcs/vms/{vm_id} ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: GET /v1/projects/**{project_id}**/vpcs/vms/**{vm_id}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Get a VPCS instance Parameters @@ -33,7 +33,7 @@ Output PUT /v1/projects/**{project_id}**/vpcs/vms/**{vm_id}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Update a VPCS instance Parameters @@ -74,7 +74,7 @@ Output DELETE /v1/projects/**{project_id}**/vpcs/vms/**{vm_id}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Delete a VPCS instance Parameters diff --git a/docs/api/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst similarity index 94% rename from docs/api/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst rename to docs/api/vpcs/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst index f1183c0a..bfb811dd 100644 --- a/docs/api/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -1,18 +1,18 @@ /v1/projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: POST /v1/projects/**{project_id}**/vpcs/vms/**{vm_id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/nio -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Add a NIO to a VPCS instance Parameters ********** - **adapter_number**: Network adapter where the nio is located - **vm_id**: UUID for the instance -- **project_id**: UUID for the project - **port_number**: Port where the nio should be added +- **project_id**: UUID for the project Response status codes ********************** @@ -22,15 +22,15 @@ Response status codes DELETE /v1/projects/**{project_id}**/vpcs/vms/**{vm_id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/nio -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Remove a NIO from a VPCS instance Parameters ********** - **adapter_number**: Network adapter where the nio is located - **vm_id**: UUID for the instance -- **project_id**: UUID for the project - **port_number**: Port from where the nio should be removed +- **project_id**: UUID for the project Response status codes ********************** diff --git a/docs/api/v1projectsprojectidvpcsvmsvmidreload.rst b/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidreload.rst similarity index 89% rename from docs/api/v1projectsprojectidvpcsvmsvmidreload.rst rename to docs/api/vpcs/v1projectsprojectidvpcsvmsvmidreload.rst index 33b4d868..4b5e197e 100644 --- a/docs/api/v1projectsprojectidvpcsvmsvmidreload.rst +++ b/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidreload.rst @@ -1,10 +1,10 @@ /v1/projects/{project_id}/vpcs/vms/{vm_id}/reload ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: POST /v1/projects/**{project_id}**/vpcs/vms/**{vm_id}**/reload -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Reload a VPCS instance Parameters diff --git a/docs/api/v1projectsprojectidvpcsvmsvmidstart.rst b/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidstart.rst similarity index 89% rename from docs/api/v1projectsprojectidvpcsvmsvmidstart.rst rename to docs/api/vpcs/v1projectsprojectidvpcsvmsvmidstart.rst index 46897b88..c8e7a550 100644 --- a/docs/api/v1projectsprojectidvpcsvmsvmidstart.rst +++ b/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidstart.rst @@ -1,10 +1,10 @@ /v1/projects/{project_id}/vpcs/vms/{vm_id}/start ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: POST /v1/projects/**{project_id}**/vpcs/vms/**{vm_id}**/start -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Start a VPCS instance Parameters diff --git a/docs/api/v1projectsprojectidvpcsvmsvmidstop.rst b/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidstop.rst similarity index 89% rename from docs/api/v1projectsprojectidvpcsvmsvmidstop.rst rename to docs/api/vpcs/v1projectsprojectidvpcsvmsvmidstop.rst index 1bc21787..bf3b7fdb 100644 --- a/docs/api/v1projectsprojectidvpcsvmsvmidstop.rst +++ b/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidstop.rst @@ -1,10 +1,10 @@ /v1/projects/{project_id}/vpcs/vms/{vm_id}/stop ------------------------------------------------------------------------------------------------------------------ +---------------------------------------------------------------------------------------------------------------------- .. contents:: POST /v1/projects/**{project_id}**/vpcs/vms/**{vm_id}**/stop -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Stop a VPCS instance Parameters diff --git a/gns3server/web/documentation.py b/gns3server/web/documentation.py index caf73aa4..f2032c9b 100644 --- a/gns3server/web/documentation.py +++ b/gns3server/web/documentation.py @@ -17,6 +17,7 @@ import re import os.path +import os from gns3server.handlers import * from gns3server.web.route import Route @@ -30,40 +31,55 @@ class Documentation(object): 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.replace("{", '**{').replace("}", "}**"))) - 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)) + for handler_name in sorted(self._documentation): + + self._create_handler_directory(handler_name) + + for path in sorted(self._documentation[handler_name]): + filename = self._file_path(path) + handler_doc = self._documentation[handler_name][path] + with open("docs/api/{}/{}.rst".format(handler_name, 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.replace("{", '**{').replace("}", "}**"))) + 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") - 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["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"]) - 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) - self._include_query_example(f, method, path) + def _create_handler_directory(self, handler_name): + """Create a directory for the handler and add an index inside""" + + directory = "docs/api/{}".format(handler_name) + os.makedirs(directory, exist_ok=True) + + with open("docs/api/{}.rst".format(handler_name), "w+") as f: + f.write(handler_name.replace("_", " ", ).capitalize()) + f.write("\n---------------------\n\n") + f.write(".. toctree::\n :glob:\n :maxdepth: 2\n\n {}/*\n".format(handler_name)) def _include_query_example(self, f, method, path): """If a sample session is available we include it in documentation""" @@ -81,7 +97,7 @@ class Documentation(object): 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)) + f.write("{}\n^^^^^^^^^^^^^^^^^^^^^^\n{}\n\n".format(definition, desc)) self._write_json_schema(f, schema['definitions'][definition]) f.write("Body\n+++++++++\n") diff --git a/gns3server/web/route.py b/gns3server/web/route.py index 4de33da0..4bf5679b 100644 --- a/gns3server/web/route.py +++ b/gns3server/web/route.py @@ -83,18 +83,21 @@ class Route(object): input_schema = kw.get("input", {}) api_version = kw.get("version", 1) cls._path = "/v{version}{path}".format(path=path, version=api_version) - cls._documentation.setdefault(cls._path, {"methods": []}) def register(func): route = cls._path - cls._documentation[route]["methods"].append({ + handler = func.__module__.replace("_handler", "").replace("gns3server.handlers.", "") + cls._documentation.setdefault(handler, {}) + cls._documentation[handler].setdefault(route, {"methods": []}) + + cls._documentation[handler][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", "") + "description": kw.get("description", ""), }) func = asyncio.coroutine(func) diff --git a/scripts/documentation.sh b/scripts/documentation.sh index 41111993..fff4ed24 100755 --- a/scripts/documentation.sh +++ b/scripts/documentation.sh @@ -28,7 +28,7 @@ export PYTEST_BUILD_DOCUMENTATION=1 rm -Rf docs/api/ mkdir -p docs/api/examples -py.test -v +#py.test -v python3 gns3server/web/documentation.py cd docs make html diff --git a/tests/modules/iou/test_iou_vm.py b/tests/modules/iou/test_iou_vm.py index 137bd984..1e93acd9 100644 --- a/tests/modules/iou/test_iou_vm.py +++ b/tests/modules/iou/test_iou_vm.py @@ -260,21 +260,21 @@ def test_enable_l1_keepalives(loop, vm): assert command == ["test"] -def test_start_capture(vm, tmpdir, manager, free_console_port): +def test_start_capture(vm, tmpdir, manager, free_console_port, loop): output_file = str(tmpdir / "test.pcap") nio = manager.create_nio(vm.iouyap_path, {"type": "nio_udp", "lport": free_console_port, "rport": free_console_port, "rhost": "192.168.1.2"}) vm.adapter_add_nio_binding(0, 0, nio) - vm.start_capture(0, 0, output_file) + loop.run_until_complete(asyncio.async(vm.start_capture(0, 0, output_file))) assert vm._adapters[0].get_nio(0).capturing -def test_stop_capture(vm, tmpdir, manager, free_console_port): +def test_stop_capture(vm, tmpdir, manager, free_console_port, loop): output_file = str(tmpdir / "test.pcap") nio = manager.create_nio(vm.iouyap_path, {"type": "nio_udp", "lport": free_console_port, "rport": free_console_port, "rhost": "192.168.1.2"}) vm.adapter_add_nio_binding(0, 0, nio) - vm.start_capture(0, 0, output_file) + loop.run_until_complete(vm.start_capture(0, 0, output_file)) assert vm._adapters[0].get_nio(0).capturing - vm.stop_capture(0, 0) + loop.run_until_complete(asyncio.async(vm.stop_capture(0, 0))) assert vm._adapters[0].get_nio(0).capturing == False From 0e98497a994c332a2c8ba63b6525e32169267785 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 17 Feb 2015 14:52:51 +0100 Subject: [PATCH 264/485] Add an endpoint for exporting the initial config file --- gns3server/handlers/iou_handler.py | 19 +++++++++++++++++++ gns3server/modules/iou/iou_vm.py | 15 +++++++++++++++ gns3server/schemas/iou.py | 20 ++++++++++++++++++++ tests/api/test_iou.py | 30 ++++++++++++++++++++++++++---- 4 files changed, 80 insertions(+), 4 deletions(-) diff --git a/gns3server/handlers/iou_handler.py b/gns3server/handlers/iou_handler.py index 356b72ce..dc272d04 100644 --- a/gns3server/handlers/iou_handler.py +++ b/gns3server/handlers/iou_handler.py @@ -25,6 +25,7 @@ from ..schemas.iou import IOU_UPDATE_SCHEMA from ..schemas.iou import IOU_OBJECT_SCHEMA from ..schemas.iou import IOU_NIO_SCHEMA from ..schemas.iou import IOU_CAPTURE_SCHEMA +from ..schemas.iou import IOU_INITIAL_CONFIG_SCHEMA from ..modules.iou import IOU @@ -293,3 +294,21 @@ class IOUHandler: port_number = int(request.match_info["port_number"]) yield from vm.stop_capture(adapter_number, port_number) response.set_status(204) + + @Route.get( + r"/projects/{project_id}/iou/vms/{vm_id}/initial_config", + status_codes={ + 200: "Initial config retrieved", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + output=IOU_INITIAL_CONFIG_SCHEMA, + description="Retrieve the initial config content") + def show_initial_config(request, response): + + iou_manager = IOU.instance() + vm = iou_manager.get_vm(request.match_info["vm_id"], + project_id=request.match_info["project_id"]) + response.set_status(200) + response.json({"content": vm.initial_config, + "path": vm.relative_initial_config_file}) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index a67a0e13..46e62804 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -889,6 +889,21 @@ class IOUVM(BaseVM): else: return None + @property + def relative_initial_config_file(self): + """ + Returns the initial config file relative to the project directory. + It's compatible with pre 1.3 topologies. + + :returns: path to config file. None if the file doesn't exist + """ + + path = os.path.join(self.working_dir, 'initial-config.cfg') + if os.path.exists(path): + return path.replace(self.project.path, "")[1:] + else: + return None + @asyncio.coroutine def start_capture(self, adapter_number, port_number, output_file, data_link_type="DLT_EN10MB"): """ diff --git a/gns3server/schemas/iou.py b/gns3server/schemas/iou.py index 3e7234d2..60ee9a4f 100644 --- a/gns3server/schemas/iou.py +++ b/gns3server/schemas/iou.py @@ -281,3 +281,23 @@ IOU_CAPTURE_SCHEMA = { "additionalProperties": False, "required": ["capture_file_name", "data_link_type"] } + +IOU_INITIAL_CONFIG_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to get the initial configuration file", + "type": "object", + "properties": { + "content": { + "description": "Content of the initial configuration file", + "type": ["string", "null"], + "minLength": 1, + }, + "path": { + "description": "Relative path on the server of the initial configuration file", + "type": ["string", "null"], + "minLength": 1, + }, + }, + "additionalProperties": False, + "required": ["content", "path"] +} diff --git a/tests/api/test_iou.py b/tests/api/test_iou.py index 1c913592..2fb15060 100644 --- a/tests/api/test_iou.py +++ b/tests/api/test_iou.py @@ -47,7 +47,9 @@ def vm(server, project, base_params): def initial_config_file(project, vm): - return os.path.join(project.path, "project-files", "iou", vm["vm_id"], "initial-config.cfg") + directory = os.path.join(project.path, "project-files", "iou", vm["vm_id"]) + os.makedirs(directory, exist_ok=True) + return os.path.join(directory, "initial-config.cfg") def test_iou_create(server, project, base_params): @@ -208,18 +210,38 @@ def test_iou_start_capture(server, vm, tmpdir): with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM.start_capture", return_value=True) as mock: params = {"capture_file_name": "test.pcap", "data_link_type": "DLT_EN10MB"} - response = server.post("/projects/{project_id}/iou/vms/{vm_id}/adapters/0/ports/0/start_capture".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), body=params) + response = server.post("/projects/{project_id}/iou/vms/{vm_id}/adapters/0/ports/0/start_capture".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), body=params, example=True) assert mock.called assert response.status == 200 assert "test.pcap" in response.json["pcap_file_path"] -def test_iou_stop_capture(server, vm, tmpdir): +def test_iou_stop_capture(server, vm): with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM.stop_capture", return_value=True) as mock: - response = server.post("/projects/{project_id}/iou/vms/{vm_id}/adapters/0/ports/0/stop_capture".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + response = server.post("/projects/{project_id}/iou/vms/{vm_id}/adapters/0/ports/0/stop_capture".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert mock.called assert response.status == 204 + + +def test_get_initial_config_without_config_file(server, vm): + + response = server.get("/projects/{project_id}/iou/vms/{vm_id}/initial_config".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) + assert response.status == 200 + assert response.json["content"] == None + assert response.json["path"] == None + + +def test_get_initial_config_with_config_file(server, project, vm): + + path = initial_config_file(project, vm) + with open(path, "w+") as f: + f.write("TEST") + + response = server.get("/projects/{project_id}/iou/vms/{vm_id}/initial_config".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) + assert response.status == 200 + assert response.json["content"] == "TEST" + assert response.json["path"] == "project-files/iou/{vm_id}/initial-config.cfg".format(vm_id=vm["vm_id"]) From dd1833c4f0032e5201a62e254bd60693a3137914 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 17 Feb 2015 16:40:45 +0100 Subject: [PATCH 265/485] iourc_path is set from server settings file --- gns3server/handlers/iou_handler.py | 2 -- gns3server/modules/iou/iou_vm.py | 42 ++++++++++++------------------ gns3server/schemas/iou.py | 8 ------ tests/api/test_iou.py | 2 +- tests/modules/iou/test_iou_vm.py | 22 +++++++++++++--- 5 files changed, 36 insertions(+), 40 deletions(-) diff --git a/gns3server/handlers/iou_handler.py b/gns3server/handlers/iou_handler.py index dc272d04..9b72da8f 100644 --- a/gns3server/handlers/iou_handler.py +++ b/gns3server/handlers/iou_handler.py @@ -65,7 +65,6 @@ class IOUHandler: initial_config=request.json.get("initial_config") ) vm.path = request.json.get("path", vm.path) - vm.iourc_path = request.json.get("iourc_path", vm.iourc_path) response.set_status(201) response.json(vm) @@ -112,7 +111,6 @@ class IOUHandler: vm.name = request.json.get("name", vm.name) vm.console = request.json.get("console", vm.console) vm.path = request.json.get("path", vm.path) - vm.iourc_path = request.json.get("iourc_path", vm.iourc_path) vm.ethernet_adapters = request.json.get("ethernet_adapters", vm.ethernet_adapters) vm.serial_adapters = request.json.get("serial_adapters", vm.serial_adapters) vm.ram = request.json.get("ram", vm.ram) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 46e62804..04c3afdd 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -86,12 +86,10 @@ class IOUVM(BaseVM): self._iou_stdout_file = "" self._started = False self._path = None - self._iourc_path = None self._ioucon_thread = None self._console_host = console_host # IOU settings - self._iourc = None self._ethernet_adapters = [] self._serial_adapters = [] self.ethernet_adapters = 2 if ethernet_adapters is None else ethernet_adapters # one adapter = 4 interfaces @@ -154,26 +152,6 @@ class IOUVM(BaseVM): if not os.access(self._path, os.X_OK): raise IOUError("IOU image '{}' is not executable".format(self._path)) - @property - def iourc_path(self): - """ - Returns the path to the iourc file. - :returns: path to the iourc file - """ - - return self._iourc_path - - @iourc_path.setter - def iourc_path(self, path): - """ - Set path to IOURC file - """ - - self._iourc_path = path - log.info("IOU {name} [id={id}]: iourc file path set to {path}".format(name=self._name, - id=self._id, - path=self._iourc_path)) - @property def use_default_iou_values(self): """ @@ -237,6 +215,16 @@ class IOUVM(BaseVM): path = shutil.which("iouyap") return path + @property + def iourc_path(self): + """ + Returns the IOURC path. + + :returns: path to IOURC + """ + + return self._manager.config.get_section_config("IOU").get("iourc_path") + @property def console(self): """ @@ -362,7 +350,8 @@ class IOUVM(BaseVM): self._rename_nvram_file() - if self._iourc_path and not os.path.isfile(self._iourc_path): + iourc_path = self.iourc_path + if iourc_path and not os.path.isfile(iourc_path): raise IOUError("A valid iourc file is necessary to start IOU") iouyap_path = self.iouyap_path @@ -372,8 +361,9 @@ class IOUVM(BaseVM): self._create_netmap_config() # created a environment variable pointing to the iourc file. env = os.environ.copy() - if self._iourc_path: - env["IOURC"] = self._iourc_path + + if iourc_path: + env["IOURC"] = iourc_path self._command = yield from self._build_command() try: log.info("Starting IOU: {}".format(self._command)) @@ -832,7 +822,7 @@ class IOUVM(BaseVM): """ env = os.environ.copy() - env["IOURC"] = self._iourc + env["IOURC"] = self.iourc_path try: output = yield from gns3server.utils.asyncio.subprocess_check_output(self._path, "-h", cwd=self.working_dir, env=env) if re.search("-l\s+Enable Layer 1 keepalive messages", output): diff --git a/gns3server/schemas/iou.py b/gns3server/schemas/iou.py index 60ee9a4f..9e846f59 100644 --- a/gns3server/schemas/iou.py +++ b/gns3server/schemas/iou.py @@ -46,10 +46,6 @@ IOU_CREATE_SCHEMA = { "description": "Path of iou binary", "type": "string" }, - "iourc_path": { - "description": "Path of iourc", - "type": "string" - }, "serial_adapters": { "description": "How many serial adapters are connected to the IOU", "type": "integer" @@ -99,10 +95,6 @@ IOU_UPDATE_SCHEMA = { "description": "Path of iou binary", "type": ["string", "null"] }, - "iourc_path": { - "description": "Path of iourc", - "type": ["string", "null"] - }, "serial_adapters": { "description": "How many serial adapters are connected to the IOU", "type": ["integer", "null"] diff --git a/tests/api/test_iou.py b/tests/api/test_iou.py index 2fb15060..2fe63861 100644 --- a/tests/api/test_iou.py +++ b/tests/api/test_iou.py @@ -36,7 +36,7 @@ def fake_iou_bin(tmpdir): @pytest.fixture def base_params(tmpdir, fake_iou_bin): """Return standard parameters""" - return {"name": "PC TEST 1", "path": fake_iou_bin, "iourc_path": str(tmpdir / "iourc")} + return {"name": "PC TEST 1", "path": fake_iou_bin} @pytest.fixture diff --git a/tests/modules/iou/test_iou_vm.py b/tests/modules/iou/test_iou_vm.py index 1e93acd9..6b1f976f 100644 --- a/tests/modules/iou/test_iou_vm.py +++ b/tests/modules/iou/test_iou_vm.py @@ -38,7 +38,7 @@ def manager(port_manager): @pytest.fixture(scope="function") def vm(project, manager, tmpdir, fake_iou_bin): - fake_file = str(tmpdir / "iourc") + fake_file = str(tmpdir / "iouyap") with open(fake_file, "w+") as f: f.write("1") @@ -48,7 +48,6 @@ def vm(project, manager, tmpdir, fake_iou_bin): manager.config.set_section_config("IOU", config) vm.path = fake_iou_bin - vm.iourc_path = fake_file return vm @@ -92,6 +91,23 @@ def test_start(loop, vm, monkeypatch): assert vm.is_running() +def test_start_with_iourc(loop, vm, monkeypatch, tmpdir): + + fake_file = str(tmpdir / "iourc") + with open(fake_file, "w+") as f: + f.write("1") + + with patch("gns3server.config.Config.get_section_config", return_value={"iourc_path": fake_file, "iouyap_path": vm.iouyap_path}): + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._check_requirements", return_value=True): + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._start_ioucon", return_value=True): + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._start_iouyap", return_value=True): + with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()) as exec_mock: + loop.run_until_complete(asyncio.async(vm.start())) + assert vm.is_running() + arsgs, kwargs = exec_mock.call_args + assert kwargs["env"]["IOURC"] == fake_file + + def test_rename_nvram_file(loop, vm, monkeypatch): """ It should rename the nvram file to the correct name before launching the VM @@ -277,4 +293,4 @@ def test_stop_capture(vm, tmpdir, manager, free_console_port, loop): loop.run_until_complete(vm.start_capture(0, 0, output_file)) assert vm._adapters[0].get_nio(0).capturing loop.run_until_complete(asyncio.async(vm.stop_capture(0, 0))) - assert vm._adapters[0].get_nio(0).capturing == False + assert vm._adapters[0].get_nio(0).capturing is False From 03b65638643cc5b9e3937afd31011b65e073f89b Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 17 Feb 2015 18:12:43 +0100 Subject: [PATCH 266/485] Initial config path for IOU --- gns3server/handlers/iou_handler.py | 4 ++-- gns3server/modules/iou/iou_vm.py | 3 ++- gns3server/schemas/iou.py | 10 +++++++--- tests/api/test_iou.py | 10 ++++++---- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/gns3server/handlers/iou_handler.py b/gns3server/handlers/iou_handler.py index 9b72da8f..05d34614 100644 --- a/gns3server/handlers/iou_handler.py +++ b/gns3server/handlers/iou_handler.py @@ -62,7 +62,7 @@ class IOUHandler: ram=request.json.get("ram"), nvram=request.json.get("nvram"), l1_keepalives=request.json.get("l1_keepalives"), - initial_config=request.json.get("initial_config") + initial_config=request.json.get("initial_config_content") ) vm.path = request.json.get("path", vm.path) response.set_status(201) @@ -116,7 +116,7 @@ class IOUHandler: vm.ram = request.json.get("ram", vm.ram) vm.nvram = request.json.get("nvram", vm.nvram) vm.l1_keepalives = request.json.get("l1_keepalives", vm.l1_keepalives) - vm.initial_config = request.json.get("initial_config", vm.initial_config) + vm.initial_config = request.json.get("initial_config_content", vm.initial_config) response.json(vm) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 04c3afdd..29b8f21c 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -200,6 +200,7 @@ class IOUVM(BaseVM): "ram": self._ram, "nvram": self._nvram, "l1_keepalives": self._l1_keepalives, + "initial_config": self.relative_initial_config_file } @property @@ -890,7 +891,7 @@ class IOUVM(BaseVM): path = os.path.join(self.working_dir, 'initial-config.cfg') if os.path.exists(path): - return path.replace(self.project.path, "")[1:] + return 'initial-config.cfg' else: return None diff --git a/gns3server/schemas/iou.py b/gns3server/schemas/iou.py index 9e846f59..e495af5e 100644 --- a/gns3server/schemas/iou.py +++ b/gns3server/schemas/iou.py @@ -66,7 +66,7 @@ IOU_CREATE_SCHEMA = { "description": "Always up ethernet interface", "type": ["boolean", "null"] }, - "initial_config": { + "initial_config_content": { "description": "Initial configuration of the IOU", "type": ["string", "null"] } @@ -115,7 +115,7 @@ IOU_UPDATE_SCHEMA = { "description": "Always up ethernet interface", "type": ["boolean", "null"] }, - "initial_config": { + "initial_config_content": { "description": "Initial configuration of the IOU", "type": ["string", "null"] } @@ -177,9 +177,13 @@ IOU_OBJECT_SCHEMA = { "description": "Always up ethernet interface", "type": "boolean" }, + "initial_config": { + "description": "Path of the initial config content relative to project directory", + "type": ["string", "null"] + } }, "additionalProperties": False, - "required": ["name", "vm_id", "console", "project_id", "path", "serial_adapters", "ethernet_adapters", "ram", "nvram", "l1_keepalives"] + "required": ["name", "vm_id", "console", "project_id", "path", "serial_adapters", "ethernet_adapters", "ram", "nvram", "l1_keepalives", "initial_config"] } IOU_NIO_SCHEMA = { diff --git a/tests/api/test_iou.py b/tests/api/test_iou.py index 2fe63861..e112bd94 100644 --- a/tests/api/test_iou.py +++ b/tests/api/test_iou.py @@ -72,7 +72,7 @@ def test_iou_create_with_params(server, project, base_params): params["serial_adapters"] = 4 params["ethernet_adapters"] = 0 params["l1_keepalives"] = True - params["initial_config"] = "hostname test" + params["initial_config_content"] = "hostname test" response = server.post("/projects/{project_id}/iou/vms".format(project_id=project.id), params, example=True) assert response.status == 201 @@ -84,8 +84,9 @@ def test_iou_create_with_params(server, project, base_params): assert response.json["ram"] == 1024 assert response.json["nvram"] == 512 assert response.json["l1_keepalives"] is True + assert "initial-config.cfg" in response.json["initial_config"] with open(initial_config_file(project, response.json)) as f: - assert f.read() == params["initial_config"] + assert f.read() == params["initial_config_content"] def test_iou_get(server, project, vm): @@ -138,7 +139,7 @@ def test_iou_update(server, vm, tmpdir, free_console_port, project): "ethernet_adapters": 4, "serial_adapters": 0, "l1_keepalives": True, - "initial_config": "hostname test" + "initial_config_content": "hostname test" } response = server.put("/projects/{project_id}/iou/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), params) assert response.status == 200 @@ -149,6 +150,7 @@ def test_iou_update(server, vm, tmpdir, free_console_port, project): assert response.json["ram"] == 512 assert response.json["nvram"] == 2048 assert response.json["l1_keepalives"] is True + assert "initial-config.cfg" in response.json["initial_config"] with open(initial_config_file(project, response.json)) as f: assert f.read() == "hostname test" @@ -244,4 +246,4 @@ def test_get_initial_config_with_config_file(server, project, vm): response = server.get("/projects/{project_id}/iou/vms/{vm_id}/initial_config".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert response.status == 200 assert response.json["content"] == "TEST" - assert response.json["path"] == "project-files/iou/{vm_id}/initial-config.cfg".format(vm_id=vm["vm_id"]) + assert response.json["path"] == "initial-config.cfg" From 0977af1c00d1af0a1612708be184b1daeb713d42 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 18 Feb 2015 11:06:13 +0100 Subject: [PATCH 267/485] Add a dedicated glossary page --- ...ptersadapternumberdportsportnumberdnio.rst | 4 +-- ...ternumberdportsportnumberdstartcapture.rst | 2 +- ...pternumberdportsportnumberdstopcapture.rst | 2 +- docs/api/iou/v1projectsprojectidiouvms.rst | 4 +-- .../api/iou/v1projectsprojectidiouvmsvmid.rst | 5 ++-- ...ptersadapternumberdportsportnumberdnio.rst | 4 +-- ...ternumberdportsportnumberdstartcapture.rst | 2 +- ...pternumberdportsportnumberdstopcapture.rst | 2 +- ...ojectsprojectidiouvmsvmidinitialconfig.rst | 25 ++++++++++++++++++ ...ptersadapternumberdportsportnumberdnio.rst | 4 +-- ...ternumberdportsportnumberdstartcapture.rst | 2 +- ...pternumberdportsportnumberdstopcapture.rst | 2 +- ...ptersadapternumberdportsportnumberdnio.rst | 4 +-- docs/general.rst | 26 ++++--------------- docs/glossary.rst | 20 ++++++++++++++ docs/index.rst | 1 + 16 files changed, 70 insertions(+), 39 deletions(-) create mode 100644 docs/api/iou/v1projectsprojectidiouvmsvmidinitialconfig.rst create mode 100644 docs/glossary.rst diff --git a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst index ddff9476..4db67440 100644 --- a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -9,9 +9,9 @@ Add a NIO to a Dynamips VM instance Parameters ********** -- **adapter_number**: Adapter where the nio should be added - **vm_id**: UUID for the instance - **port_number**: Port on the adapter +- **adapter_number**: Adapter where the nio should be added - **project_id**: UUID for the project Response status codes @@ -27,9 +27,9 @@ Remove a NIO from a Dynamips VM instance Parameters ********** -- **adapter_number**: Adapter from where the nio should be removed - **vm_id**: UUID for the instance - **port_number**: Port on the adapter +- **adapter_number**: Adapter from where the nio should be removed - **project_id**: UUID for the project Response status codes diff --git a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst index 43c9c0fe..a00b56bc 100644 --- a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst +++ b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst @@ -9,9 +9,9 @@ Start a packet capture on a Dynamips VM instance Parameters ********** -- **adapter_number**: Adapter to start a packet capture - **vm_id**: UUID for the instance - **port_number**: Port on the adapter +- **adapter_number**: Adapter to start a packet capture - **project_id**: UUID for the project Response status codes diff --git a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst index 1114fae5..12efdbdb 100644 --- a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst +++ b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst @@ -9,9 +9,9 @@ Stop a packet capture on a Dynamips VM instance Parameters ********** -- **adapter_number**: Adapter to stop a packet capture - **vm_id**: UUID for the instance - **port_number**: Port on the adapter (always 0) +- **adapter_number**: Adapter to stop a packet capture - **project_id**: UUID for the project Response status codes diff --git a/docs/api/iou/v1projectsprojectidiouvms.rst b/docs/api/iou/v1projectsprojectidiouvms.rst index 92cebf36..281eccd2 100644 --- a/docs/api/iou/v1projectsprojectidiouvms.rst +++ b/docs/api/iou/v1projectsprojectidiouvms.rst @@ -25,8 +25,7 @@ Input Name Mandatory Type Description console ['integer', 'null'] console TCP port ethernet_adapters integer How many ethernet adapters are connected to the IOU - initial_config ['string', 'null'] Initial configuration of the IOU - iourc_path string Path of iourc + initial_config_content ['string', 'null'] Initial configuration of the IOU l1_keepalives ['boolean', 'null'] Always up ethernet interface name ✔ string IOU VM name nvram ['integer', 'null'] Allocated NVRAM KB @@ -44,6 +43,7 @@ Output Name Mandatory Type Description console ✔ integer console TCP port ethernet_adapters ✔ integer How many ethernet adapters are connected to the IOU + initial_config ✔ ['string', 'null'] Path of the initial config content relative to project directory l1_keepalives ✔ boolean Always up ethernet interface name ✔ string IOU VM name nvram ✔ integer Allocated NVRAM KB diff --git a/docs/api/iou/v1projectsprojectidiouvmsvmid.rst b/docs/api/iou/v1projectsprojectidiouvmsvmid.rst index 0fdbe2e2..a3ea1cbe 100644 --- a/docs/api/iou/v1projectsprojectidiouvmsvmid.rst +++ b/docs/api/iou/v1projectsprojectidiouvmsvmid.rst @@ -26,6 +26,7 @@ Output Name Mandatory Type Description console ✔ integer console TCP port ethernet_adapters ✔ integer How many ethernet adapters are connected to the IOU + initial_config ✔ ['string', 'null'] Path of the initial config content relative to project directory l1_keepalives ✔ boolean Always up ethernet interface name ✔ string IOU VM name nvram ✔ integer Allocated NVRAM KB @@ -61,8 +62,7 @@ Input Name Mandatory Type Description console ['integer', 'null'] console TCP port ethernet_adapters ['integer', 'null'] How many ethernet adapters are connected to the IOU - initial_config ['string', 'null'] Initial configuration of the IOU - iourc_path ['string', 'null'] Path of iourc + initial_config_content ['string', 'null'] Initial configuration of the IOU l1_keepalives ['boolean', 'null'] Always up ethernet interface name ['string', 'null'] IOU VM name nvram ['integer', 'null'] Allocated NVRAM KB @@ -79,6 +79,7 @@ Output Name Mandatory Type Description console ✔ integer console TCP port ethernet_adapters ✔ integer How many ethernet adapters are connected to the IOU + initial_config ✔ ['string', 'null'] Path of the initial config content relative to project directory l1_keepalives ✔ boolean Always up ethernet interface name ✔ string IOU VM name nvram ✔ integer Allocated NVRAM KB diff --git a/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst index 9e2ce5a8..59088dce 100644 --- a/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -9,9 +9,9 @@ Add a NIO to a IOU instance Parameters ********** -- **adapter_number**: Network adapter where the nio is located - **vm_id**: UUID for the instance - **port_number**: Port where the nio should be added +- **adapter_number**: Network adapter where the nio is located - **project_id**: UUID for the project Response status codes @@ -27,9 +27,9 @@ Remove a NIO from a IOU instance Parameters ********** -- **adapter_number**: Network adapter where the nio is located - **vm_id**: UUID for the instance - **port_number**: Port from where the nio should be removed +- **adapter_number**: Network adapter where the nio is located - **project_id**: UUID for the project Response status codes diff --git a/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst b/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst index fdd481c9..ff553a9c 100644 --- a/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst +++ b/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst @@ -9,9 +9,9 @@ Start a packet capture on a IOU VM instance Parameters ********** -- **adapter_number**: Adapter to start a packet capture - **vm_id**: UUID for the instance - **port_number**: Port on the adapter +- **adapter_number**: Adapter to start a packet capture - **project_id**: UUID for the project Response status codes diff --git a/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst b/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst index a2441a3d..dee6e612 100644 --- a/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst +++ b/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst @@ -9,9 +9,9 @@ Stop a packet capture on a IOU VM instance Parameters ********** -- **adapter_number**: Adapter to stop a packet capture - **vm_id**: UUID for the instance - **port_number**: Port on the adapter (always 0) +- **adapter_number**: Adapter to stop a packet capture - **project_id**: UUID for the project Response status codes diff --git a/docs/api/iou/v1projectsprojectidiouvmsvmidinitialconfig.rst b/docs/api/iou/v1projectsprojectidiouvmsvmidinitialconfig.rst new file mode 100644 index 00000000..f2cfdbfd --- /dev/null +++ b/docs/api/iou/v1projectsprojectidiouvmsvmidinitialconfig.rst @@ -0,0 +1,25 @@ +/v1/projects/{project_id}/iou/vms/{vm_id}/initial_config +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +GET /v1/projects/**{project_id}**/iou/vms/**{vm_id}**/initial_config +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Retrieve the initial config content + +Response status codes +********************** +- **200**: Initial config retrieved +- **400**: Invalid request +- **404**: Instance doesn't exist + +Output +******* +.. raw:: html + + + + + +
Name Mandatory Type Description
content ['string', 'null'] Content of the initial configuration file
path ['string', 'null'] Relative path on the server of the initial configuration file
+ diff --git a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst index 726fa494..9d22751c 100644 --- a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -9,9 +9,9 @@ Add a NIO to a VirtualBox VM instance Parameters ********** -- **adapter_number**: Adapter where the nio should be added - **vm_id**: UUID for the instance - **port_number**: Port on the adapter (always 0) +- **adapter_number**: Adapter where the nio should be added - **project_id**: UUID for the project Response status codes @@ -27,9 +27,9 @@ Remove a NIO from a VirtualBox VM instance Parameters ********** -- **adapter_number**: Adapter from where the nio should be removed - **vm_id**: UUID for the instance - **port_number**: Port on the adapter (always) +- **adapter_number**: Adapter from where the nio should be removed - **project_id**: UUID for the project Response status codes diff --git a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst index 67ff14c4..67bba0ab 100644 --- a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst +++ b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst @@ -9,9 +9,9 @@ Start a packet capture on a VirtualBox VM instance Parameters ********** -- **adapter_number**: Adapter to start a packet capture - **vm_id**: UUID for the instance - **port_number**: Port on the adapter (always 0) +- **adapter_number**: Adapter to start a packet capture - **project_id**: UUID for the project Response status codes diff --git a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst index 1377be32..ea6fea53 100644 --- a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst +++ b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst @@ -9,9 +9,9 @@ Stop a packet capture on a VirtualBox VM instance Parameters ********** -- **adapter_number**: Adapter to stop a packet capture - **vm_id**: UUID for the instance - **port_number**: Port on the adapter (always 0) +- **adapter_number**: Adapter to stop a packet capture - **project_id**: UUID for the project Response status codes diff --git a/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst index bfb811dd..8be4efb3 100644 --- a/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -9,9 +9,9 @@ Add a NIO to a VPCS instance Parameters ********** -- **adapter_number**: Network adapter where the nio is located - **vm_id**: UUID for the instance - **port_number**: Port where the nio should be added +- **adapter_number**: Network adapter where the nio is located - **project_id**: UUID for the project Response status codes @@ -27,9 +27,9 @@ Remove a NIO from a VPCS instance Parameters ********** -- **adapter_number**: Network adapter where the nio is located - **vm_id**: UUID for the instance - **port_number**: Port from where the nio should be removed +- **adapter_number**: Network adapter where the nio is located - **project_id**: UUID for the project Response status codes diff --git a/docs/general.rst b/docs/general.rst index 5e01f896..9c0855f7 100644 --- a/docs/general.rst +++ b/docs/general.rst @@ -1,3 +1,8 @@ +Communications +=============== + +All the communication are done over HTTP using JSON. + Errors ====== @@ -10,24 +15,3 @@ JSON like that "status": 409, "message": "Conflict" } - -Glossary -======== - -VM ---- - -A Virtual Machine (Dynamips, IOU, Qemu, VPCS...) - -Adapter -------- - -The physical network interface. The adapter can contain multiple ports. - -Port ----- - -A port is an opening on network adapter that cable plug into. - -For example a VM can have a serial and an ethernet adapter plugged in. -The ethernet adapter can have 4 ports. diff --git a/docs/glossary.rst b/docs/glossary.rst new file mode 100644 index 00000000..9192b9b5 --- /dev/null +++ b/docs/glossary.rst @@ -0,0 +1,20 @@ +Glossary +======== + +VM +--- + +A Virtual Machine (Dynamips, IOU, Qemu, VPCS...) + +Adapter +------- + +The physical network interface. The adapter can contain multiple ports. + +Port +---- + +A port is an opening on network adapter that cable plug into. + +For example a VM can have a serial and an ethernet adapter plugged in. +The ethernet adapter can have 4 ports. diff --git a/docs/index.rst b/docs/index.rst index a8a1fc27..74292e8a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -4,6 +4,7 @@ Welcome to API documentation! .. toctree:: general + glossary development From 3cb721342708f5d286bd0bae58fbc1497525a1d3 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 18 Feb 2015 15:18:18 +0100 Subject: [PATCH 268/485] Fix crash in VPCS --- gns3server/utils/asyncio.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gns3server/utils/asyncio.py b/gns3server/utils/asyncio.py index a627cb3e..16dc8cd2 100644 --- a/gns3server/utils/asyncio.py +++ b/gns3server/utils/asyncio.py @@ -38,17 +38,17 @@ def wait_run_in_executor(func, *args): @asyncio.coroutine -def subprocess_check_output(*args, working_dir=None, env=None): +def subprocess_check_output(*args, cwd=None, env=None): """ Run a command and capture output :param *args: List of command arguments - :param working_dir: Working directory + :param cwd: Current working directory :param env: Command environment :returns: Command output """ - proc = yield from asyncio.create_subprocess_exec(*args, stdout=asyncio.subprocess.PIPE, cwd=working_dir, env=env) + proc = yield from asyncio.create_subprocess_exec(*args, stdout=asyncio.subprocess.PIPE, cwd=cwd, env=env) output = yield from proc.stdout.read() if output is None: return "" From d65617657c03dc6243fd793906f80fc31907b246 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 18 Feb 2015 16:13:09 +0100 Subject: [PATCH 269/485] Fix old project directories renames --- gns3server/modules/base_manager.py | 19 ++++----- tests/modules/test_manager.py | 67 ++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 tests/modules/test_manager.py diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 4013dad3..d379a59b 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -158,23 +158,22 @@ class BaseManager: project = ProjectManager.instance().get_project(project_id) - try: - if vm_id and hasattr(self, "get_legacy_vm_workdir_name"): + # If it's not an UUID + if vm_id and (isinstance(vm_id, int) or len(vm_id) != 36): + legacy_id = int(vm_id) + vm_id = str(uuid4()) + if hasattr(self, "get_legacy_vm_workdir_name"): # move old project VM files to a new location - legacy_id = int(vm_id) - project_dir = os.path.dirname(project.path) - project_name = os.path.basename(project_dir) - project_files_dir = os.path.join(project_dir, "{}-files".format(project_name)) + + project_name = os.path.basename(project.path) + project_files_dir = os.path.join(project.path, "{}-files".format(project_name)) module_path = os.path.join(project_files_dir, self.module_name.lower()) vm_working_dir = os.path.join(module_path, self.get_legacy_vm_workdir_name(legacy_id)) - vm_id = str(uuid4()) - new_vm_working_dir = os.path.join(project.path, self.module_name.lower(), vm_id) + new_vm_working_dir = os.path.join(project.path, "project-files", self.module_name.lower(), vm_id) try: yield from wait_run_in_executor(shutil.move, vm_working_dir, new_vm_working_dir) except OSError as e: raise aiohttp.web.HTTPInternalServerError(text="Could not move VM working directory: {}".format(e)) - except ValueError: - pass if not vm_id: vm_id = str(uuid4()) diff --git a/tests/modules/test_manager.py b/tests/modules/test_manager.py new file mode 100644 index 00000000..b38eb804 --- /dev/null +++ b/tests/modules/test_manager.py @@ -0,0 +1,67 @@ +# -*- 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 uuid +import os +import pytest +from unittest.mock import patch + + +from gns3server.modules.vpcs import VPCS + + +def test_create_vm_new_topology(loop, project, port_manager): + + VPCS._instance = None + vpcs = VPCS.instance() + vpcs.port_manager = port_manager + vm_id = str(uuid.uuid4()) + vm = loop.run_until_complete(vpcs.create_vm("PC 1", project.id, vm_id)) + assert vm in project.vms + + +def test_create_vm_new_topology_without_uuid(loop, project, port_manager): + + VPCS._instance = None + vpcs = VPCS.instance() + vpcs.port_manager = port_manager + vm = loop.run_until_complete(vpcs.create_vm("PC 1", project.id, None)) + assert vm in project.vms + assert len(vm.id) == 36 + + +def test_create_vm_old_topology(loop, project, tmpdir, port_manager): + + with patch("gns3server.config.Config.get_section_config", return_value={"local": True}): + # Create an old topology directory + project_dir = str(tmpdir / "testold") + vm_dir = os.path.join(project_dir, "testold-files", "vpcs", "pc-1") + project.path = project_dir + os.makedirs(vm_dir, exist_ok=True) + with open(os.path.join(vm_dir, "startup.vpc"), "w+") as f: + f.write("1") + + VPCS._instance = None + vpcs = VPCS.instance() + vpcs.port_manager = port_manager + vm_id = 1 + vm = loop.run_until_complete(vpcs.create_vm("PC 1", project.id, vm_id)) + assert len(vm.id) == 36 + + vm_dir = os.path.join(project_dir, "project-files", "vpcs", vm.id) + with open(os.path.join(vm_dir, "startup.vpc")) as f: + assert f.read() == "1" From 610dee957d0d25ba972e1ed93aa574ad508fdfaa Mon Sep 17 00:00:00 2001 From: grossmj Date: Wed, 18 Feb 2015 17:48:02 -0700 Subject: [PATCH 270/485] Use HTTP error 409 instead of 500 for VMError. --- gns3server/modules/dynamips/nodes/router.py | 4 ++-- gns3server/web/route.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index 910430a9..dcfaac80 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -1495,10 +1495,10 @@ class Router(BaseVM): try: reply = yield from self._hypervisor.send("vm extract_config {}".format(self._name)) - reply = reply[0].rsplit(' ', 2)[-2:] - except IOError: + except DynamipsError: #for some reason Dynamips gets frozen when it does not find the magic number in the NVRAM file. return None, None + reply = reply[0].rsplit(' ', 2)[-2:] startup_config = reply[0][1:-1] # get statup-config and remove single quotes private_config = reply[1][1:-1] # get private-config and remove single quotes return startup_config, private_config diff --git a/gns3server/web/route.py b/gns3server/web/route.py index 4bf5679b..1fee37c7 100644 --- a/gns3server/web/route.py +++ b/gns3server/web/route.py @@ -115,8 +115,8 @@ class Route(object): except VMError as e: log.error("VM error detected: {type}".format(type=type(e)), exc_info=1) response = Response(route=route) - response.set_status(500) - response.json({"message": str(e), "status": 500}) + response.set_status(409) + response.json({"message": str(e), "status": 409}) except Exception as e: log.error("Uncaught exception detected: {type}".format(type=type(e)), exc_info=1) response = Response(route=route) From 3d1363150ebf5af4e1ce70d02681780aacf9672f Mon Sep 17 00:00:00 2001 From: grossmj Date: Wed, 18 Feb 2015 18:24:35 -0700 Subject: [PATCH 271/485] Fixes ghost file path. --- gns3server/modules/dynamips/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index c10fce52..03eb326c 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -435,7 +435,7 @@ class Dynamips(BaseManager): except DynamipsError as e: log.warn("Could not create ghost instance: {}".format(e)) - if vm.ghost_file != ghost_file and os.path.isfile(ghost_file): + if vm.ghost_file != ghost_file and os.path.isfile(ghost_file_path): # set the ghost file to the router yield from vm.set_ghost_status(2) yield from vm.set_ghost_file(ghost_file) From 380c4d82112a16e80ea585e5a4cc4f367638cc4d Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 18 Feb 2015 18:40:01 -0700 Subject: [PATCH 272/485] Fixes capture directory path. --- gns3server/modules/project.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index 973ce58e..6aa4bf6f 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -178,7 +178,7 @@ class Project: :returns: working directory """ - workdir = os.path.join(self._path, 'project-files', module_name) + workdir = os.path.join(self._path, "project-files", module_name) try: os.makedirs(workdir, exist_ok=True) except OSError as e: @@ -194,7 +194,7 @@ class Project: :returns: VM working directory """ - workdir = os.path.join(self._path, 'project-files', vm.manager.module_name.lower(), vm.id) + workdir = os.path.join(self._path, "project-files", vm.manager.module_name.lower(), vm.id) try: os.makedirs(workdir, exist_ok=True) except OSError as e: @@ -208,7 +208,7 @@ class Project: :returns: path to the directory """ - workdir = os.path.join(self._path, "captures") + workdir = os.path.join(self._path, "project-files", "captures") try: os.makedirs(workdir, exist_ok=True) except OSError as e: From 25bcbfb0730a94f2f6c035511c2c6033a44c3d95 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 19 Feb 2015 11:33:25 +0100 Subject: [PATCH 273/485] Allocation of console port in base vm --- gns3server/modules/base_vm.py | 47 +- gns3server/modules/dynamips/nodes/router.py | 44 +- gns3server/modules/iou/iou_vm.py | 32 +- gns3server/modules/qemu/qemu_vm.py | 1190 +++++++++++++++++ .../modules/virtualbox/virtualbox_vm.py | 32 - gns3server/modules/vpcs/vpcs_vm.py | 32 +- 6 files changed, 1252 insertions(+), 125 deletions(-) create mode 100644 gns3server/modules/qemu/qemu_vm.py diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index c6a49069..c5b39405 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -28,12 +28,28 @@ log = logging.getLogger(__name__) class BaseVM: - def __init__(self, name, vm_id, project, manager): + """ + Base vm implementation. + + :param name: name of this IOU vm + :param vm_id: IOU instance identifier + :param project: Project instance + :param manager: parent VM Manager + :param console: TCP console port + """ + + def __init__(self, name, vm_id, project, manager, console=None): self._name = name self._id = vm_id self._project = project self._manager = manager + self._console = console + + if self._console is not None: + self._console = self._manager.port_manager.reserve_console_port(self._console) + else: + self._console = self._manager.port_manager.get_free_console_port() log.debug("{module}: {name} [{id}] initialized".format(module=self.manager.module_name, name=self.name, @@ -147,3 +163,32 @@ class BaseVM: """ raise NotImplementedError + + @property + def console(self): + """ + Returns the console port of this VPCS vm. + + :returns: console port + """ + + return self._console + + @console.setter + def console(self, console): + """ + Change console port + + :params console: Console port (integer) + """ + + if console == self._console: + return + if self._console: + self._manager.port_manager.release_console_port(self._console) + self._console = self._manager.port_manager.reserve_console_port(console) + log.info("{module}: '{name}' [{id}]: console port set to {port}".format( + module=self.manager.module_name, + name=self.name, + id=self.id, + port=console)) diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index dcfaac80..36db166d 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -83,7 +83,6 @@ class Router(BaseVM): self._disk0 = 0 # Megabytes self._disk1 = 0 # Megabytes self._confreg = "0x2102" - self._console = None self._aux = None self._mac_addr = "" self._system_id = "FTX0945W0MY" # processor board ID in IOS @@ -106,11 +105,6 @@ class Router(BaseVM): raise DynamipsError("Dynamips identifier {} is already used by another router".format(dynamips_id)) self._dynamips_ids[project.id].append(self._dynamips_id) - if self._console is not None: - self._console = self._manager.port_manager.reserve_console_port(self._console) - else: - self._console = self._manager.port_manager.get_free_console_port() - if self._aux is not None: self._aux = self._manager.port_manager.reserve_console_port(self._aux) else: @@ -863,16 +857,6 @@ class Router(BaseVM): new_confreg=confreg)) self._confreg = confreg - @property - def console(self): - """ - Returns the TCP console port. - - :returns: console port (integer) - """ - - return self._console - @asyncio.coroutine def set_console(self, console): """ @@ -1014,8 +998,8 @@ class Router(BaseVM): if slot is not None: current_adapter = slot raise DynamipsError('Slot {slot_number} is already occupied by adapter {adapter} on router "{name}"'.format(name=self._name, - slot_number=slot_number, - adapter=current_adapter)) + slot_number=slot_number, + adapter=current_adapter)) is_running = yield from self.is_running() @@ -1081,16 +1065,16 @@ class Router(BaseVM): slot_number=slot_number)) log.info('Router "{name}" [{id}]: OIR stop event sent to slot {slot_number}'.format(name=self._name, - id=self._id, - slot_number=slot_number)) + id=self._id, + slot_number=slot_number)) yield from self._hypervisor.send('vm slot_remove_binding "{name}" {slot_number} 0'.format(name=self._name, slot_number=slot_number)) log.info('Router "{name}" [{id}]: adapter {adapter} removed from slot {slot_number}'.format(name=self._name, - id=self._id, - adapter=adapter, - slot_number=slot_number)) + id=self._id, + adapter=adapter, + slot_number=slot_number)) self._slots[slot_number] = None @asyncio.coroutine @@ -1119,14 +1103,14 @@ class Router(BaseVM): # WIC1 = 16, WIC2 = 32 and WIC3 = 48 internal_wic_slot_number = 16 * (wic_slot_number + 1) yield from self._hypervisor.send('vm slot_add_binding "{name}" {slot_number} {wic_slot_number} {wic}'.format(name=self._name, - slot_number=slot_number, - wic_slot_number=internal_wic_slot_number, - wic=wic)) + slot_number=slot_number, + wic_slot_number=internal_wic_slot_number, + wic=wic)) log.info('Router "{name}" [{id}]: {wic} inserted into WIC slot {wic_slot_number}'.format(name=self._name, - id=self._id, - wic=wic, - wic_slot_number=wic_slot_number)) + id=self._id, + wic=wic, + wic_slot_number=wic_slot_number)) adapter.install_wic(wic_slot_number, wic) @@ -1496,7 +1480,7 @@ class Router(BaseVM): try: reply = yield from self._hypervisor.send("vm extract_config {}".format(self._name)) except DynamipsError: - #for some reason Dynamips gets frozen when it does not find the magic number in the NVRAM file. + # for some reason Dynamips gets frozen when it does not find the magic number in the NVRAM file. return None, None reply = reply[0].rsplit(' ', 2)[-2:] startup_config = reply[0][1:-1] # get statup-config and remove single quotes diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 29b8f21c..8fef72a9 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -77,9 +77,8 @@ class IOUVM(BaseVM): l1_keepalives=None, initial_config=None): - super().__init__(name, vm_id, project, manager) + super().__init__(name, vm_id, project, manager, console=console) - self._console = console self._command = [] self._iouyap_process = None self._iou_process = None @@ -103,11 +102,6 @@ class IOUVM(BaseVM): if initial_config is not None: self.initial_config = initial_config - if self._console is not None: - self._console = self._manager.port_manager.reserve_console_port(self._console) - else: - self._console = self._manager.port_manager.get_free_console_port() - @asyncio.coroutine def close(self): @@ -226,30 +220,6 @@ class IOUVM(BaseVM): return self._manager.config.get_section_config("IOU").get("iourc_path") - @property - def console(self): - """ - Returns the console port of this IOU vm. - - :returns: console port - """ - - return self._console - - @console.setter - def console(self, console): - """ - Change console port - - :params console: Console port (integer) - """ - - if console == self._console: - return - if self._console: - self._manager.port_manager.release_console_port(self._console) - self._console = self._manager.port_manager.reserve_console_port(console) - @property def ram(self): """ diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py new file mode 100644 index 00000000..1de4ab57 --- /dev/null +++ b/gns3server/modules/qemu/qemu_vm.py @@ -0,0 +1,1190 @@ +# -*- 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 . + +""" +QEMU VM instance. +""" + +import sys +import os +import shutil +import random +import subprocess +import shlex +import ntpath +import telnetlib +import time +import re + +from gns3server.config import Config + +from .qemu_error import QemuError +from ..adapters.ethernet_adapter import EthernetAdapter +from ..nios.nio_udp import NIO_UDP +from ..base_vm import BaseVM +from ...utils.asyncio import subprocess_check_output + +import logging +log = logging.getLogger(__name__) + + +class QemuVM(BaseVM): + module_name = 'qemu' + + """ + QEMU VM implementation. + + :param name: name of this Qemu vm + :param vm_id: IOU instance identifier + :param project: Project instance + :param manager: parent VM Manager + :param console: TCP console port + :param qemu_path: path to the QEMU binary + :param host: host/address to bind for console and UDP connections + :param qemu_id: QEMU VM instance ID + :param console: TCP console port + :param console_host: IP address to bind for console connections + :param console_start_port_range: TCP console port range start + :param console_end_port_range: TCP console port range end + :param monitor: TCP monitor port + :param monitor_host: IP address to bind for monitor connections + :param monitor_start_port_range: TCP monitor port range start + :param monitor_end_port_range: TCP monitor port range end + """ + + _instances = [] + _allocated_console_ports = [] + _allocated_monitor_ports = [] + + def __init__(self, + name, + vm_id, + project, + manager, + qemu_path=None, + host="127.0.0.1", + console=None, + console_host="0.0.0.0", + console_start_port_range=5001, + console_end_port_range=5500, + monitor=None, + monitor_host="0.0.0.0", + monitor_start_port_range=5501, + monitor_end_port_range=6000): + + super().__init__(name, vm_id, project, manager, console=console) + + self._host = host + self._command = [] + self._started = False + self._process = None + self._cpulimit_process = None + self._stdout_file = "" + self._console_host = console_host + self._console_start_port_range = console_start_port_range + self._console_end_port_range = console_end_port_range + self._monitor_host = monitor_host + self._monitor_start_port_range = monitor_start_port_range + self._monitor_end_port_range = monitor_end_port_range + + # QEMU settings + self._qemu_path = qemu_path + self._hda_disk_image = "" + self._hdb_disk_image = "" + self._options = "" + self._ram = 256 + self._console = console + self._monitor = monitor + self._ethernet_adapters = [] + self._adapter_type = "e1000" + self._initrd = "" + self._kernel_image = "" + self._kernel_command_line = "" + self._legacy_networking = False + self._cpu_throttling = 0 # means no CPU throttling + self._process_priority = "low" + + if self._monitor is not None: + self._monitor = self._manager.port_manager.reserve_console_port(self._monitor) + else: + self._monitor = self._manager.port_manager.get_free_console_port() + + self.adapters = 1 # creates 1 adapter by default + log.info("QEMU VM {name} [id={id}] has been created".format(name=self._name, + id=self._id)) + + def defaults(self): + """ + Returns all the default attribute values for this QEMU VM. + + :returns: default values (dictionary) + """ + + qemu_defaults = {"name": self._name, + "qemu_path": self._qemu_path, + "ram": self._ram, + "hda_disk_image": self._hda_disk_image, + "hdb_disk_image": self._hdb_disk_image, + "options": self._options, + "adapters": self.adapters, + "adapter_type": self._adapter_type, + "console": self._console, + "monitor": self._monitor, + "initrd": self._initrd, + "kernel_image": self._kernel_image, + "kernel_command_line": self._kernel_command_line, + "legacy_networking": self._legacy_networking, + "cpu_throttling": self._cpu_throttling, + "process_priority": self._process_priority + } + + return qemu_defaults + + @property + def id(self): + """ + Returns the unique ID for this QEMU VM. + + :returns: id (integer) + """ + + return self._id + + @classmethod + def reset(cls): + """ + Resets allocated instance list. + """ + + cls._instances.clear() + cls._allocated_console_ports.clear() + cls._allocated_monitor_ports.clear() + + @property + def name(self): + """ + Returns the name of this QEMU VM. + + :returns: name + """ + + return self._name + + @name.setter + def name(self, new_name): + """ + Sets the name of this QEMU VM. + + :param new_name: name + """ + + log.info("QEMU VM {name} [id={id}]: renamed to {new_name}".format(name=self._name, + id=self._id, + new_name=new_name)) + + self._name = new_name + + @property + def working_dir(self): + """ + Returns current working directory + + :returns: path to the working directory + """ + + return self._working_dir + + @working_dir.setter + def working_dir(self, working_dir): + """ + Sets the working directory this QEMU VM. + + :param working_dir: path to the working directory + """ + + try: + os.makedirs(working_dir) + except FileExistsError: + pass + except OSError as e: + raise QemuError("Could not create working directory {}: {}".format(working_dir, e)) + + self._working_dir = working_dir + log.info("QEMU VM {name} [id={id}]: working directory changed to {wd}".format(name=self._name, + id=self._id, + wd=self._working_dir)) + + @property + def console(self): + """ + Returns the TCP console port. + + :returns: console port (integer) + """ + + return self._console + + @console.setter + def console(self, console): + """ + Sets the TCP console port. + + :param console: console port (integer) + """ + + if console in self._allocated_console_ports: + raise QemuError("Console port {} is already used by another QEMU VM".format(console)) + + self._allocated_console_ports.remove(self._console) + self._console = console + self._allocated_console_ports.append(self._console) + + log.info("QEMU VM {name} [id={id}]: console port set to {port}".format(name=self._name, + id=self._id, + port=console)) + + @property + def monitor(self): + """ + Returns the TCP monitor port. + + :returns: monitor port (integer) + """ + + return self._monitor + + @monitor.setter + def monitor(self, monitor): + """ + Sets the TCP monitor port. + + :param monitor: monitor port (integer) + """ + + if monitor in self._allocated_monitor_ports: + raise QemuError("Monitor port {} is already used by another QEMU VM".format(monitor)) + + self._allocated_monitor_ports.remove(self._monitor) + self._monitor = monitor + self._allocated_monitor_ports.append(self._monitor) + + log.info("QEMU VM {name} [id={id}]: monitor port set to {port}".format(name=self._name, + id=self._id, + port=monitor)) + + def delete(self): + """ + Deletes this QEMU VM. + """ + + self.stop() + if self._id in self._instances: + self._instances.remove(self._id) + + if self._console and self._console in self._allocated_console_ports: + self._allocated_console_ports.remove(self._console) + + if self._monitor and self._monitor in self._allocated_monitor_ports: + self._allocated_monitor_ports.remove(self._monitor) + + log.info("QEMU VM {name} [id={id}] has been deleted".format(name=self._name, + id=self._id)) + + def clean_delete(self): + """ + Deletes this QEMU VM & all files. + """ + + self.stop() + if self._id in self._instances: + self._instances.remove(self._id) + + if self._console: + self._allocated_console_ports.remove(self._console) + + if self._monitor: + self._allocated_monitor_ports.remove(self._monitor) + + try: + shutil.rmtree(self._working_dir) + except OSError as e: + log.error("could not delete QEMU VM {name} [id={id}]: {error}".format(name=self._name, + id=self._id, + error=e)) + return + + log.info("QEMU VM {name} [id={id}] has been deleted (including associated files)".format(name=self._name, + id=self._id)) + + @property + def cloud_path(self): + """ + Returns the cloud path where images can be downloaded from. + + :returns: cloud path + """ + + return self._cloud_path + + @cloud_path.setter + def cloud_path(self, cloud_path): + """ + Sets the cloud path where images can be downloaded from. + + :param cloud_path: + :return: + """ + + self._cloud_path = cloud_path + + @property + def qemu_path(self): + """ + Returns the QEMU binary path for this QEMU VM. + + :returns: QEMU path + """ + + return self._qemu_path + + @qemu_path.setter + def qemu_path(self, qemu_path): + """ + Sets the QEMU binary path this QEMU VM. + + :param qemu_path: QEMU path + """ + + log.info("QEMU VM {name} [id={id}] has set the QEMU path to {qemu_path}".format(name=self._name, + id=self._id, + qemu_path=qemu_path)) + self._qemu_path = qemu_path + + @property + def hda_disk_image(self): + """ + Returns the hda disk image path for this QEMU VM. + + :returns: QEMU hda disk image path + """ + + return self._hda_disk_image + + @hda_disk_image.setter + def hda_disk_image(self, hda_disk_image): + """ + Sets the hda disk image for this QEMU VM. + + :param hda_disk_image: QEMU hda disk image path + """ + + log.info("QEMU VM {name} [id={id}] has set the QEMU hda disk image path to {disk_image}".format(name=self._name, + id=self._id, + disk_image=hda_disk_image)) + self._hda_disk_image = hda_disk_image + + @property + def hdb_disk_image(self): + """ + Returns the hdb disk image path for this QEMU VM. + + :returns: QEMU hdb disk image path + """ + + return self._hdb_disk_image + + @hdb_disk_image.setter + def hdb_disk_image(self, hdb_disk_image): + """ + Sets the hdb disk image for this QEMU VM. + + :param hdb_disk_image: QEMU hdb disk image path + """ + + log.info("QEMU VM {name} [id={id}] has set the QEMU hdb disk image path to {disk_image}".format(name=self._name, + id=self._id, + disk_image=hdb_disk_image)) + self._hdb_disk_image = hdb_disk_image + + @property + def adapters(self): + """ + Returns the number of Ethernet adapters for this QEMU VM instance. + + :returns: number of adapters + """ + + return len(self._ethernet_adapters) + + @adapters.setter + def adapters(self, adapters): + """ + Sets the number of Ethernet adapters for this QEMU VM instance. + + :param adapters: number of adapters + """ + + self._ethernet_adapters.clear() + for adapter_id in range(0, adapters): + self._ethernet_adapters.append(EthernetAdapter()) + + log.info("QEMU VM {name} [id={id}]: number of Ethernet adapters changed to {adapters}".format(name=self._name, + id=self._id, + adapters=adapters)) + + @property + def adapter_type(self): + """ + Returns the adapter type for this QEMU VM instance. + + :returns: adapter type (string) + """ + + return self._adapter_type + + @adapter_type.setter + def adapter_type(self, adapter_type): + """ + Sets the adapter type for this QEMU VM instance. + + :param adapter_type: adapter type (string) + """ + + self._adapter_type = adapter_type + + log.info("QEMU VM {name} [id={id}]: adapter type changed to {adapter_type}".format(name=self._name, + id=self._id, + adapter_type=adapter_type)) + + @property + def legacy_networking(self): + """ + Returns either QEMU legacy networking commands are used. + + :returns: boolean + """ + + return self._legacy_networking + + @legacy_networking.setter + def legacy_networking(self, legacy_networking): + """ + Sets either QEMU legacy networking commands are used. + + :param legacy_networking: boolean + """ + + if legacy_networking: + log.info("QEMU VM {name} [id={id}] has enabled legacy networking".format(name=self._name, id=self._id)) + else: + log.info("QEMU VM {name} [id={id}] has disabled legacy networking".format(name=self._name, id=self._id)) + self._legacy_networking = legacy_networking + + @property + def cpu_throttling(self): + """ + Returns the percentage of CPU allowed. + + :returns: integer + """ + + return self._cpu_throttling + + @cpu_throttling.setter + def cpu_throttling(self, cpu_throttling): + """ + Sets the percentage of CPU allowed. + + :param cpu_throttling: integer + """ + + log.info("QEMU VM {name} [id={id}] has set the percentage of CPU allowed to {cpu}".format(name=self._name, + id=self._id, + cpu=cpu_throttling)) + self._cpu_throttling = cpu_throttling + self._stop_cpulimit() + if cpu_throttling: + self._set_cpu_throttling() + + @property + def process_priority(self): + """ + Returns the process priority. + + :returns: string + """ + + return self._process_priority + + @process_priority.setter + def process_priority(self, process_priority): + """ + Sets the process priority. + + :param process_priority: string + """ + + log.info("QEMU VM {name} [id={id}] has set the process priority to {priority}".format(name=self._name, + id=self._id, + priority=process_priority)) + self._process_priority = process_priority + + @property + def ram(self): + """ + Returns the RAM amount for this QEMU VM. + + :returns: RAM amount in MB + """ + + return self._ram + + @ram.setter + def ram(self, ram): + """ + Sets the amount of RAM for this QEMU VM. + + :param ram: RAM amount in MB + """ + + log.info("QEMU VM {name} [id={id}] has set the RAM to {ram}".format(name=self._name, + id=self._id, + ram=ram)) + self._ram = ram + + @property + def options(self): + """ + Returns the options for this QEMU VM. + + :returns: QEMU options + """ + + return self._options + + @options.setter + def options(self, options): + """ + Sets the options for this QEMU VM. + + :param options: QEMU options + """ + + log.info("QEMU VM {name} [id={id}] has set the QEMU options to {options}".format(name=self._name, + id=self._id, + options=options)) + self._options = options + + @property + def initrd(self): + """ + Returns the initrd path for this QEMU VM. + + :returns: QEMU initrd path + """ + + return self._initrd + + @initrd.setter + def initrd(self, initrd): + """ + Sets the initrd path for this QEMU VM. + + :param initrd: QEMU initrd path + """ + + log.info("QEMU VM {name} [id={id}] has set the QEMU initrd path to {initrd}".format(name=self._name, + id=self._id, + initrd=initrd)) + self._initrd = initrd + + @property + def kernel_image(self): + """ + Returns the kernel image path for this QEMU VM. + + :returns: QEMU kernel image path + """ + + return self._kernel_image + + @kernel_image.setter + def kernel_image(self, kernel_image): + """ + Sets the kernel image path for this QEMU VM. + + :param kernel_image: QEMU kernel image path + """ + + log.info("QEMU VM {name} [id={id}] has set the QEMU kernel image path to {kernel_image}".format(name=self._name, + id=self._id, + kernel_image=kernel_image)) + self._kernel_image = kernel_image + + @property + def kernel_command_line(self): + """ + Returns the kernel command line for this QEMU VM. + + :returns: QEMU kernel command line + """ + + return self._kernel_command_line + + @kernel_command_line.setter + def kernel_command_line(self, kernel_command_line): + """ + Sets the kernel command line for this QEMU VM. + + :param kernel_command_line: QEMU kernel command line + """ + + log.info("QEMU VM {name} [id={id}] has set the QEMU kernel command line to {kernel_command_line}".format(name=self._name, + id=self._id, + kernel_command_line=kernel_command_line)) + self._kernel_command_line = kernel_command_line + + def _set_process_priority(self): + """ + Changes the process priority + """ + + if sys.platform.startswith("win"): + try: + import win32api + import win32con + import win32process + except ImportError: + log.error("pywin32 must be installed to change the priority class for QEMU VM {}".format(self._name)) + else: + log.info("setting QEMU VM {} priority class to BELOW_NORMAL".format(self._name)) + handle = win32api.OpenProcess(win32con.PROCESS_ALL_ACCESS, 0, self._process.pid) + if self._process_priority == "realtime": + priority = win32process.REALTIME_PRIORITY_CLASS + elif self._process_priority == "very high": + priority = win32process.HIGH_PRIORITY_CLASS + elif self._process_priority == "high": + priority = win32process.ABOVE_NORMAL_PRIORITY_CLASS + elif self._process_priority == "low": + priority = win32process.BELOW_NORMAL_PRIORITY_CLASS + elif self._process_priority == "very low": + priority = win32process.IDLE_PRIORITY_CLASS + else: + priority = win32process.NORMAL_PRIORITY_CLASS + win32process.SetPriorityClass(handle, priority) + else: + if self._process_priority == "realtime": + priority = -20 + elif self._process_priority == "very high": + priority = -15 + elif self._process_priority == "high": + priority = -5 + elif self._process_priority == "low": + priority = 5 + elif self._process_priority == "very low": + priority = 19 + else: + priority = 0 + try: + subprocess.call(['renice', '-n', str(priority), '-p', str(self._process.pid)]) + except (OSError, subprocess.SubprocessError) as e: + log.error("could not change process priority for QEMU VM {}: {}".format(self._name, e)) + + def _stop_cpulimit(self): + """ + Stops the cpulimit process. + """ + + if self._cpulimit_process and self._cpulimit_process.poll() is None: + self._cpulimit_process.kill() + try: + self._process.wait(3) + except subprocess.TimeoutExpired: + log.error("could not kill cpulimit process {}".format(self._cpulimit_process.pid)) + + def _set_cpu_throttling(self): + """ + Limits the CPU usage for current QEMU process. + """ + + if not self.is_running(): + return + + try: + if sys.platform.startswith("win") and hasattr(sys, "frozen"): + cpulimit_exec = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), "cpulimit", "cpulimit.exe") + else: + cpulimit_exec = "cpulimit" + subprocess.Popen([cpulimit_exec, "--lazy", "--pid={}".format(self._process.pid), "--limit={}".format(self._cpu_throttling)], cwd=self._working_dir) + log.info("CPU throttled to {}%".format(self._cpu_throttling)) + except FileNotFoundError: + raise QemuError("cpulimit could not be found, please install it or deactivate CPU throttling") + except (OSError, subprocess.SubprocessError) as e: + raise QemuError("Could not throttle CPU: {}".format(e)) + + def start(self): + """ + Starts this QEMU VM. + """ + + if self.is_running(): + + # resume the VM if it is paused + self.resume() + return + + else: + + if not os.path.isfile(self._qemu_path) or not os.path.exists(self._qemu_path): + found = False + paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep) + # look for the qemu binary in the current working directory and $PATH + for path in paths: + try: + if self._qemu_path in os.listdir(path) and os.access(os.path.join(path, self._qemu_path), os.X_OK): + self._qemu_path = os.path.join(path, self._qemu_path) + found = True + break + except OSError: + continue + + if not found: + raise QemuError("QEMU binary '{}' is not accessible".format(self._qemu_path)) + + if self.cloud_path is not None: + # Download from Cloud Files + if self.hda_disk_image != "": + _, filename = ntpath.split(self.hda_disk_image) + src = '{}/{}'.format(self.cloud_path, filename) + dst = os.path.join(self.working_dir, filename) + if not os.path.isfile(dst): + cloud_settings = Config.instance().cloud_settings() + provider = get_provider(cloud_settings) + log.debug("Downloading file from {} to {}...".format(src, dst)) + provider.download_file(src, dst) + log.debug("Download of {} complete.".format(src)) + self.hda_disk_image = dst + if self.hdb_disk_image != "": + _, filename = ntpath.split(self.hdb_disk_image) + src = '{}/{}'.format(self.cloud_path, filename) + dst = os.path.join(self.working_dir, filename) + if not os.path.isfile(dst): + cloud_settings = Config.instance().cloud_settings() + provider = get_provider(cloud_settings) + log.debug("Downloading file from {} to {}...".format(src, dst)) + provider.download_file(src, dst) + log.debug("Download of {} complete.".format(src)) + self.hdb_disk_image = dst + + if self.initrd != "": + _, filename = ntpath.split(self.initrd) + src = '{}/{}'.format(self.cloud_path, filename) + dst = os.path.join(self.working_dir, filename) + if not os.path.isfile(dst): + cloud_settings = Config.instance().cloud_settings() + provider = get_provider(cloud_settings) + log.debug("Downloading file from {} to {}...".format(src, dst)) + provider.download_file(src, dst) + log.debug("Download of {} complete.".format(src)) + self.initrd = dst + if self.kernel_image != "": + _, filename = ntpath.split(self.kernel_image) + src = '{}/{}'.format(self.cloud_path, filename) + dst = os.path.join(self.working_dir, filename) + if not os.path.isfile(dst): + cloud_settings = Config.instance().cloud_settings() + provider = get_provider(cloud_settings) + log.debug("Downloading file from {} to {}...".format(src, dst)) + provider.download_file(src, dst) + log.debug("Download of {} complete.".format(src)) + self.kernel_image = dst + + self._command = self._build_command() + try: + log.info("starting QEMU: {}".format(self._command)) + self._stdout_file = os.path.join(self._working_dir, "qemu.log") + log.info("logging to {}".format(self._stdout_file)) + with open(self._stdout_file, "w") as fd: + self._process = subprocess.Popen(self._command, + stdout=fd, + stderr=subprocess.STDOUT, + cwd=self._working_dir) + log.info("QEMU VM instance {} started PID={}".format(self._id, self._process.pid)) + self._started = True + except (OSError, subprocess.SubprocessError) as e: + stdout = self.read_stdout() + log.error("could not start QEMU {}: {}\n{}".format(self._qemu_path, e, stdout)) + raise QemuError("could not start QEMU {}: {}\n{}".format(self._qemu_path, e, stdout)) + + self._set_process_priority() + if self._cpu_throttling: + self._set_cpu_throttling() + + def stop(self): + """ + Stops this QEMU VM. + """ + + # stop the QEMU process + if self.is_running(): + log.info("stopping QEMU VM instance {} PID={}".format(self._id, self._process.pid)) + try: + self._process.terminate() + self._process.wait(1) + except subprocess.TimeoutExpired: + self._process.kill() + if self._process.poll() is None: + log.warn("QEMU VM instance {} PID={} is still running".format(self._id, + self._process.pid)) + self._process = None + self._started = False + self._stop_cpulimit() + + def _control_vm(self, command, expected=None, timeout=30): + """ + Executes a command with QEMU monitor when this VM is running. + + :param command: QEMU monitor command (e.g. info status, stop etc.) + :param timeout: how long to wait for QEMU monitor + + :returns: result of the command (Match object or None) + """ + + result = None + if self.is_running() and self._monitor: + log.debug("Execute QEMU monitor command: {}".format(command)) + try: + tn = telnetlib.Telnet(self._monitor_host, self._monitor, timeout=timeout) + except OSError as e: + log.warn("Could not connect to QEMU monitor: {}".format(e)) + return result + try: + tn.write(command.encode('ascii') + b"\n") + time.sleep(0.1) + except OSError as e: + log.warn("Could not write to QEMU monitor: {}".format(e)) + tn.close() + return result + if expected: + try: + ind, match, dat = tn.expect(list=expected, timeout=timeout) + if match: + result = match + except EOFError as e: + log.warn("Could not read from QEMU monitor: {}".format(e)) + tn.close() + return result + + def _get_vm_status(self): + """ + Returns this VM suspend status (running|paused) + + :returns: status (string) + """ + + result = None + + match = self._control_vm("info status", [b"running", b"paused"]) + if match: + result = match.group(0).decode('ascii') + return result + + def suspend(self): + """ + Suspends this QEMU VM. + """ + + vm_status = self._get_vm_status() + if vm_status == "running": + self._control_vm("stop") + log.debug("QEMU VM has been suspended") + else: + log.info("QEMU VM is not running to be suspended, current status is {}".format(vm_status)) + + def reload(self): + """ + Reloads this QEMU VM. + """ + + self._control_vm("system_reset") + log.debug("QEMU VM has been reset") + + def resume(self): + """ + Resumes this QEMU VM. + """ + + vm_status = self._get_vm_status() + if vm_status == "paused": + self._control_vm("cont") + log.debug("QEMU VM has been resumed") + else: + log.info("QEMU VM is not paused to be resumed, current status is {}".format(vm_status)) + + def port_add_nio_binding(self, adapter_id, nio): + """ + Adds a port NIO binding. + + :param adapter_id: adapter ID + :param nio: NIO instance to add to the slot/port + """ + + try: + adapter = self._ethernet_adapters[adapter_id] + except IndexError: + raise QemuError("Adapter {adapter_id} doesn't exist on QEMU VM {name}".format(name=self._name, + adapter_id=adapter_id)) + + if self.is_running(): + # dynamically configure an UDP tunnel on the QEMU VM adapter + if nio and isinstance(nio, NIO_UDP): + if self._legacy_networking: + self._control_vm("host_net_remove {} gns3-{}".format(adapter_id, adapter_id)) + self._control_vm("host_net_add udp vlan={},name=gns3-{},sport={},dport={},daddr={}".format(adapter_id, + adapter_id, + nio.lport, + nio.rport, + nio.rhost)) + else: + self._control_vm("host_net_remove {} gns3-{}".format(adapter_id, adapter_id)) + self._control_vm("host_net_add socket vlan={},name=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_id, + adapter_id, + nio.rhost, + nio.rport, + self._host, + nio.lport)) + + adapter.add_nio(0, nio) + log.info("QEMU VM {name} [id={id}]: {nio} added to adapter {adapter_id}".format(name=self._name, + id=self._id, + nio=nio, + adapter_id=adapter_id)) + + def port_remove_nio_binding(self, adapter_id): + """ + Removes a port NIO binding. + + :param adapter_id: adapter ID + + :returns: NIO instance + """ + + try: + adapter = self._ethernet_adapters[adapter_id] + except IndexError: + raise QemuError("Adapter {adapter_id} doesn't exist on QEMU VM {name}".format(name=self._name, + adapter_id=adapter_id)) + + if self.is_running(): + # dynamically disable the QEMU VM adapter + self._control_vm("host_net_remove {} gns3-{}".format(adapter_id, adapter_id)) + self._control_vm("host_net_add user vlan={},name=gns3-{}".format(adapter_id, adapter_id)) + + nio = adapter.get_nio(0) + adapter.remove_nio(0) + log.info("QEMU VM {name} [id={id}]: {nio} removed from adapter {adapter_id}".format(name=self._name, + id=self._id, + nio=nio, + adapter_id=adapter_id)) + return nio + + @property + def started(self): + """ + Returns either this QEMU VM has been started or not. + + :returns: boolean + """ + + return self._started + + def read_stdout(self): + """ + Reads the standard output of the QEMU process. + Only use when the process has been stopped or has crashed. + """ + + output = "" + if self._stdout_file: + try: + with open(self._stdout_file, errors="replace") as file: + output = file.read() + except OSError as e: + log.warn("could not read {}: {}".format(self._stdout_file, e)) + return output + + def is_running(self): + """ + Checks if the QEMU process is running + + :returns: True or False + """ + + if self._process and self._process.poll() is None: + return True + return False + + def command(self): + """ + Returns the QEMU command line. + + :returns: QEMU command line (string) + """ + + return " ".join(self._build_command()) + + def _serial_options(self): + + if self._console: + return ["-serial", "telnet:{}:{},server,nowait".format(self._console_host, self._console)] + else: + return [] + + def _monitor_options(self): + + if self._monitor: + return ["-monitor", "telnet:{}:{},server,nowait".format(self._monitor_host, self._monitor)] + else: + return [] + + def _disk_options(self): + + options = [] + qemu_img_path = "" + qemu_path_dir = os.path.dirname(self._qemu_path) + try: + for f in os.listdir(qemu_path_dir): + if f.startswith("qemu-img"): + qemu_img_path = os.path.join(qemu_path_dir, f) + except OSError as e: + raise QemuError("Error while looking for qemu-img in {}: {}".format(qemu_path_dir, e)) + + if not qemu_img_path: + raise QemuError("Could not find qemu-img in {}".format(qemu_path_dir)) + + try: + if self._hda_disk_image: + if not os.path.isfile(self._hda_disk_image) or not os.path.exists(self._hda_disk_image): + if os.path.islink(self._hda_disk_image): + raise QemuError("hda disk image '{}' linked to '{}' is not accessible".format(self._hda_disk_image, os.path.realpath(self._hda_disk_image))) + else: + raise QemuError("hda disk image '{}' is not accessible".format(self._hda_disk_image)) + hda_disk = os.path.join(self._working_dir, "hda_disk.qcow2") + if not os.path.exists(hda_disk): + retcode = subprocess.call([qemu_img_path, "create", "-o", + "backing_file={}".format(self._hda_disk_image), + "-f", "qcow2", hda_disk]) + log.info("{} returned with {}".format(qemu_img_path, retcode)) + else: + # create a "FLASH" with 256MB if no disk image has been specified + hda_disk = os.path.join(self._working_dir, "flash.qcow2") + if not os.path.exists(hda_disk): + retcode = subprocess.call([qemu_img_path, "create", "-f", "qcow2", hda_disk, "128M"]) + log.info("{} returned with {}".format(qemu_img_path, retcode)) + + except (OSError, subprocess.SubprocessError) as e: + raise QemuError("Could not create disk image {}".format(e)) + + options.extend(["-hda", hda_disk]) + if self._hdb_disk_image: + if not os.path.isfile(self._hdb_disk_image) or not os.path.exists(self._hdb_disk_image): + if os.path.islink(self._hdb_disk_image): + raise QemuError("hdb disk image '{}' linked to '{}' is not accessible".format(self._hdb_disk_image, os.path.realpath(self._hdb_disk_image))) + else: + raise QemuError("hdb disk image '{}' is not accessible".format(self._hdb_disk_image)) + hdb_disk = os.path.join(self._working_dir, "hdb_disk.qcow2") + if not os.path.exists(hdb_disk): + try: + retcode = subprocess.call([qemu_img_path, "create", "-o", + "backing_file={}".format(self._hdb_disk_image), + "-f", "qcow2", hdb_disk]) + log.info("{} returned with {}".format(qemu_img_path, retcode)) + except (OSError, subprocess.SubprocessError) as e: + raise QemuError("Could not create disk image {}".format(e)) + options.extend(["-hdb", hdb_disk]) + + return options + + def _linux_boot_options(self): + + options = [] + if self._initrd: + if not os.path.isfile(self._initrd) or not os.path.exists(self._initrd): + if os.path.islink(self._initrd): + raise QemuError("initrd file '{}' linked to '{}' is not accessible".format(self._initrd, os.path.realpath(self._initrd))) + else: + raise QemuError("initrd file '{}' is not accessible".format(self._initrd)) + options.extend(["-initrd", self._initrd]) + if self._kernel_image: + if not os.path.isfile(self._kernel_image) or not os.path.exists(self._kernel_image): + if os.path.islink(self._kernel_image): + raise QemuError("kernel image '{}' linked to '{}' is not accessible".format(self._kernel_image, os.path.realpath(self._kernel_image))) + else: + raise QemuError("kernel image '{}' is not accessible".format(self._kernel_image)) + options.extend(["-kernel", self._kernel_image]) + if self._kernel_command_line: + options.extend(["-append", self._kernel_command_line]) + + return options + + def _network_options(self): + + network_options = [] + adapter_id = 0 + for adapter in self._ethernet_adapters: + # TODO: let users specify a base mac address + mac = "00:00:ab:%02x:%02x:%02d" % (random.randint(0x00, 0xff), random.randint(0x00, 0xff), adapter_id) + network_options.extend(["-net", "nic,vlan={},macaddr={},model={}".format(adapter_id, mac, self._adapter_type)]) + nio = adapter.get_nio(0) + if nio and isinstance(nio, NIO_UDP): + if self._legacy_networking: + network_options.extend(["-net", "udp,vlan={},name=gns3-{},sport={},dport={},daddr={}".format(adapter_id, + adapter_id, + nio.lport, + nio.rport, + nio.rhost)]) + else: + network_options.extend(["-net", "socket,vlan={},name=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_id, + adapter_id, + nio.rhost, + nio.rport, + self._host, + nio.lport)]) + else: + network_options.extend(["-net", "user,vlan={},name=gns3-{}".format(adapter_id, adapter_id)]) + adapter_id += 1 + + return network_options + + def _build_command(self): + """ + Command to start the QEMU process. + (to be passed to subprocess.Popen()) + """ + + command = [self._qemu_path] + command.extend(["-name", self._name]) + command.extend(["-m", str(self._ram)]) + command.extend(self._disk_options()) + command.extend(self._linux_boot_options()) + command.extend(self._serial_options()) + command.extend(self._monitor_options()) + additional_options = self._options.strip() + if additional_options: + command.extend(shlex.split(additional_options)) + command.extend(self._network_options()) + return command diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 318733a2..708aa6ec 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -60,7 +60,6 @@ class VirtualBoxVM(BaseVM): self._closed = False # VirtualBox settings - self._console = None self._adapters = adapters self._ethernet_adapters = [] self._headless = False @@ -69,11 +68,6 @@ class VirtualBoxVM(BaseVM): self._use_any_adapter = False self._adapter_type = "Intel PRO/1000 MT Desktop (82540EM)" - if self._console is not None: - self._console = self._manager.port_manager.reserve_console_port(self._console) - else: - self._console = self._manager.port_manager.get_free_console_port() - def __json__(self): return {"name": self.name, @@ -253,32 +247,6 @@ class VirtualBoxVM(BaseVM): log.info("VirtualBox VM '{name}' [{id}] reloaded".format(name=self.name, id=self.id)) log.debug("Reload result: {}".format(result)) - @property - def console(self): - """ - Returns the TCP console port. - - :returns: console port (integer) - """ - - return self._console - - @console.setter - def console(self, console): - """ - Sets the TCP console port. - - :param console: console port (integer) - """ - - if self._console: - self._manager.port_manager.release_console_port(self._console) - - self._console = self._manager.port_manager.reserve_console_port(console) - log.info("VirtualBox VM '{name}' [{id}]: console port set to {port}".format(name=self.name, - id=self.id, - port=console)) - @asyncio.coroutine def _get_all_hdd_files(self): diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index 0d0bab91..6bd5a4ba 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -55,9 +55,8 @@ class VPCSVM(BaseVM): def __init__(self, name, vm_id, project, manager, console=None, startup_script=None): - super().__init__(name, vm_id, project, manager) + super().__init__(name, vm_id, project, manager, console=console) - self._console = console self._command = [] self._process = None self._vpcs_stdout_file = "" @@ -68,11 +67,6 @@ class VPCSVM(BaseVM): self.startup_script = startup_script self._ethernet_adapter = EthernetAdapter() # one adapter with 1 Ethernet interface - if self._console is not None: - self._console = self._manager.port_manager.reserve_console_port(self._console) - else: - self._console = self._manager.port_manager.get_free_console_port() - @asyncio.coroutine def close(self): @@ -119,30 +113,6 @@ class VPCSVM(BaseVM): path = shutil.which("vpcs") return path - @property - def console(self): - """ - Returns the console port of this VPCS vm. - - :returns: console port - """ - - return self._console - - @console.setter - def console(self, console): - """ - Change console port - - :params console: Console port (integer) - """ - - if console == self._console: - return - if self._console: - self._manager.port_manager.release_console_port(self._console) - self._console = self._manager.port_manager.reserve_console_port(console) - @BaseVM.name.setter def name(self, new_name): """ From b03b9226ff9921e20b72099dafdd99585d45df89 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 19 Feb 2015 16:46:57 +0100 Subject: [PATCH 274/485] So we have running code for a qemu module Now the handlers. The telnet code is not yet async --- gns3server/modules/qemu/__init__.py | 41 +++ gns3server/modules/qemu/qemu_error.py | 27 ++ gns3server/modules/qemu/qemu_vm.py | 399 +++++++------------------- tests/modules/qemu/test_qemu_vm.py | 188 ++++++++++++ 4 files changed, 352 insertions(+), 303 deletions(-) create mode 100644 gns3server/modules/qemu/__init__.py create mode 100644 gns3server/modules/qemu/qemu_error.py create mode 100644 tests/modules/qemu/test_qemu_vm.py diff --git a/gns3server/modules/qemu/__init__.py b/gns3server/modules/qemu/__init__.py new file mode 100644 index 00000000..58c87484 --- /dev/null +++ b/gns3server/modules/qemu/__init__.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 . + +""" +Qemu server module. +""" + +import asyncio + +from ..base_manager import BaseManager +from .qemu_error import QemuError +from .qemu_vm import QemuVM + + +class Qemu(BaseManager): + _VM_CLASS = QemuVM + + @staticmethod + def get_legacy_vm_workdir_name(legacy_vm_id): + """ + Returns the name of the legacy working directory name for a VM. + + :param legacy_vm_id: legacy VM identifier (integer) + :returns: working directory name + """ + + return "pc-{}".format(legacy_vm_id) diff --git a/gns3server/modules/qemu/qemu_error.py b/gns3server/modules/qemu/qemu_error.py new file mode 100644 index 00000000..48aca696 --- /dev/null +++ b/gns3server/modules/qemu/qemu_error.py @@ -0,0 +1,27 @@ +# -*- 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 . + +""" +Custom exceptions for Qemu module. +""" + +from ..vm_error import VMError + + +class QemuError(VMError): + + pass diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index 1de4ab57..9cb5960e 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -29,6 +29,7 @@ import ntpath import telnetlib import time import re +import asyncio from gns3server.config import Config @@ -66,10 +67,6 @@ class QemuVM(BaseVM): :param monitor_end_port_range: TCP monitor port range end """ - _instances = [] - _allocated_console_ports = [] - _allocated_monitor_ports = [] - def __init__(self, name, vm_id, @@ -102,7 +99,7 @@ class QemuVM(BaseVM): self._monitor_end_port_range = monitor_end_port_range # QEMU settings - self._qemu_path = qemu_path + self.qemu_path = qemu_path self._hda_disk_image = "" self._hdb_disk_image = "" self._options = "" @@ -127,136 +124,6 @@ class QemuVM(BaseVM): log.info("QEMU VM {name} [id={id}] has been created".format(name=self._name, id=self._id)) - def defaults(self): - """ - Returns all the default attribute values for this QEMU VM. - - :returns: default values (dictionary) - """ - - qemu_defaults = {"name": self._name, - "qemu_path": self._qemu_path, - "ram": self._ram, - "hda_disk_image": self._hda_disk_image, - "hdb_disk_image": self._hdb_disk_image, - "options": self._options, - "adapters": self.adapters, - "adapter_type": self._adapter_type, - "console": self._console, - "monitor": self._monitor, - "initrd": self._initrd, - "kernel_image": self._kernel_image, - "kernel_command_line": self._kernel_command_line, - "legacy_networking": self._legacy_networking, - "cpu_throttling": self._cpu_throttling, - "process_priority": self._process_priority - } - - return qemu_defaults - - @property - def id(self): - """ - Returns the unique ID for this QEMU VM. - - :returns: id (integer) - """ - - return self._id - - @classmethod - def reset(cls): - """ - Resets allocated instance list. - """ - - cls._instances.clear() - cls._allocated_console_ports.clear() - cls._allocated_monitor_ports.clear() - - @property - def name(self): - """ - Returns the name of this QEMU VM. - - :returns: name - """ - - return self._name - - @name.setter - def name(self, new_name): - """ - Sets the name of this QEMU VM. - - :param new_name: name - """ - - log.info("QEMU VM {name} [id={id}]: renamed to {new_name}".format(name=self._name, - id=self._id, - new_name=new_name)) - - self._name = new_name - - @property - def working_dir(self): - """ - Returns current working directory - - :returns: path to the working directory - """ - - return self._working_dir - - @working_dir.setter - def working_dir(self, working_dir): - """ - Sets the working directory this QEMU VM. - - :param working_dir: path to the working directory - """ - - try: - os.makedirs(working_dir) - except FileExistsError: - pass - except OSError as e: - raise QemuError("Could not create working directory {}: {}".format(working_dir, e)) - - self._working_dir = working_dir - log.info("QEMU VM {name} [id={id}]: working directory changed to {wd}".format(name=self._name, - id=self._id, - wd=self._working_dir)) - - @property - def console(self): - """ - Returns the TCP console port. - - :returns: console port (integer) - """ - - return self._console - - @console.setter - def console(self, console): - """ - Sets the TCP console port. - - :param console: console port (integer) - """ - - if console in self._allocated_console_ports: - raise QemuError("Console port {} is already used by another QEMU VM".format(console)) - - self._allocated_console_ports.remove(self._console) - self._console = console - self._allocated_console_ports.append(self._console) - - log.info("QEMU VM {name} [id={id}]: console port set to {port}".format(name=self._name, - id=self._id, - port=console)) - @property def monitor(self): """ @@ -275,16 +142,16 @@ class QemuVM(BaseVM): :param monitor: monitor port (integer) """ - if monitor in self._allocated_monitor_ports: - raise QemuError("Monitor port {} is already used by another QEMU VM".format(monitor)) - - self._allocated_monitor_ports.remove(self._monitor) - self._monitor = monitor - self._allocated_monitor_ports.append(self._monitor) - - log.info("QEMU VM {name} [id={id}]: monitor port set to {port}".format(name=self._name, - id=self._id, - port=monitor)) + if monitor == self._monitor: + return + if self._monitor: + self._manager.port_manager.release_monitor_port(self._monitor) + self._monitor = self._manager.port_manager.reserve_monitor_port(monitor) + log.info("{module}: '{name}' [{id}]: monitor port set to {port}".format( + module=self.manager.module_name, + name=self.name, + id=self.id, + port=monitor)) def delete(self): """ @@ -304,53 +171,6 @@ class QemuVM(BaseVM): log.info("QEMU VM {name} [id={id}] has been deleted".format(name=self._name, id=self._id)) - def clean_delete(self): - """ - Deletes this QEMU VM & all files. - """ - - self.stop() - if self._id in self._instances: - self._instances.remove(self._id) - - if self._console: - self._allocated_console_ports.remove(self._console) - - if self._monitor: - self._allocated_monitor_ports.remove(self._monitor) - - try: - shutil.rmtree(self._working_dir) - except OSError as e: - log.error("could not delete QEMU VM {name} [id={id}]: {error}".format(name=self._name, - id=self._id, - error=e)) - return - - log.info("QEMU VM {name} [id={id}] has been deleted (including associated files)".format(name=self._name, - id=self._id)) - - @property - def cloud_path(self): - """ - Returns the cloud path where images can be downloaded from. - - :returns: cloud path - """ - - return self._cloud_path - - @cloud_path.setter - def cloud_path(self, cloud_path): - """ - Sets the cloud path where images can be downloaded from. - - :param cloud_path: - :return: - """ - - self._cloud_path = cloud_path - @property def qemu_path(self): """ @@ -369,10 +189,20 @@ class QemuVM(BaseVM): :param qemu_path: QEMU path """ + if qemu_path and os.pathsep not in qemu_path: + qemu_path = shutil.which(qemu_path) + + if qemu_path is None: + raise QemuError("QEMU binary path is not set or not found in the path") + if not os.path.exists(qemu_path): + raise QemuError("QEMU binary '{}' is not accessible".format(qemu_path)) + if not os.access(qemu_path, os.X_OK): + raise QemuError("QEMU binary '{}' is not executable".format(qemu_path)) + + self._qemu_path = qemu_path log.info("QEMU VM {name} [id={id}] has set the QEMU path to {qemu_path}".format(name=self._name, id=self._id, qemu_path=qemu_path)) - self._qemu_path = qemu_path @property def hda_disk_image(self): @@ -658,6 +488,7 @@ class QemuVM(BaseVM): kernel_command_line=kernel_command_line)) self._kernel_command_line = kernel_command_line + @asyncio.coroutine def _set_process_priority(self): """ Changes the process priority @@ -700,7 +531,8 @@ class QemuVM(BaseVM): else: priority = 0 try: - subprocess.call(['renice', '-n', str(priority), '-p', str(self._process.pid)]) + process = yield from asyncio.create_subprocess_exec('renice', '-n', str(priority), '-p', str(self._process.pid)) + yield from process.wait() except (OSError, subprocess.SubprocessError) as e: log.error("could not change process priority for QEMU VM {}: {}".format(self._name, e)) @@ -729,13 +561,14 @@ class QemuVM(BaseVM): cpulimit_exec = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), "cpulimit", "cpulimit.exe") else: cpulimit_exec = "cpulimit" - subprocess.Popen([cpulimit_exec, "--lazy", "--pid={}".format(self._process.pid), "--limit={}".format(self._cpu_throttling)], cwd=self._working_dir) + subprocess.Popen([cpulimit_exec, "--lazy", "--pid={}".format(self._process.pid), "--limit={}".format(self._cpu_throttling)], cwd=self.working_dir) log.info("CPU throttled to {}%".format(self._cpu_throttling)) except FileNotFoundError: raise QemuError("cpulimit could not be found, please install it or deactivate CPU throttling") except (OSError, subprocess.SubprocessError) as e: raise QemuError("Could not throttle CPU: {}".format(e)) + @asyncio.coroutine def start(self): """ Starts this QEMU VM. @@ -748,92 +581,28 @@ class QemuVM(BaseVM): return else: - - if not os.path.isfile(self._qemu_path) or not os.path.exists(self._qemu_path): - found = False - paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep) - # look for the qemu binary in the current working directory and $PATH - for path in paths: - try: - if self._qemu_path in os.listdir(path) and os.access(os.path.join(path, self._qemu_path), os.X_OK): - self._qemu_path = os.path.join(path, self._qemu_path) - found = True - break - except OSError: - continue - - if not found: - raise QemuError("QEMU binary '{}' is not accessible".format(self._qemu_path)) - - if self.cloud_path is not None: - # Download from Cloud Files - if self.hda_disk_image != "": - _, filename = ntpath.split(self.hda_disk_image) - src = '{}/{}'.format(self.cloud_path, filename) - dst = os.path.join(self.working_dir, filename) - if not os.path.isfile(dst): - cloud_settings = Config.instance().cloud_settings() - provider = get_provider(cloud_settings) - log.debug("Downloading file from {} to {}...".format(src, dst)) - provider.download_file(src, dst) - log.debug("Download of {} complete.".format(src)) - self.hda_disk_image = dst - if self.hdb_disk_image != "": - _, filename = ntpath.split(self.hdb_disk_image) - src = '{}/{}'.format(self.cloud_path, filename) - dst = os.path.join(self.working_dir, filename) - if not os.path.isfile(dst): - cloud_settings = Config.instance().cloud_settings() - provider = get_provider(cloud_settings) - log.debug("Downloading file from {} to {}...".format(src, dst)) - provider.download_file(src, dst) - log.debug("Download of {} complete.".format(src)) - self.hdb_disk_image = dst - - if self.initrd != "": - _, filename = ntpath.split(self.initrd) - src = '{}/{}'.format(self.cloud_path, filename) - dst = os.path.join(self.working_dir, filename) - if not os.path.isfile(dst): - cloud_settings = Config.instance().cloud_settings() - provider = get_provider(cloud_settings) - log.debug("Downloading file from {} to {}...".format(src, dst)) - provider.download_file(src, dst) - log.debug("Download of {} complete.".format(src)) - self.initrd = dst - if self.kernel_image != "": - _, filename = ntpath.split(self.kernel_image) - src = '{}/{}'.format(self.cloud_path, filename) - dst = os.path.join(self.working_dir, filename) - if not os.path.isfile(dst): - cloud_settings = Config.instance().cloud_settings() - provider = get_provider(cloud_settings) - log.debug("Downloading file from {} to {}...".format(src, dst)) - provider.download_file(src, dst) - log.debug("Download of {} complete.".format(src)) - self.kernel_image = dst - - self._command = self._build_command() + self._command = yield from self._build_command() try: log.info("starting QEMU: {}".format(self._command)) - self._stdout_file = os.path.join(self._working_dir, "qemu.log") + self._stdout_file = os.path.join(self.working_dir, "qemu.log") log.info("logging to {}".format(self._stdout_file)) with open(self._stdout_file, "w") as fd: - self._process = subprocess.Popen(self._command, - stdout=fd, - stderr=subprocess.STDOUT, - cwd=self._working_dir) + self._process = yield from asyncio.create_subprocess_exec(*self._command, + stdout=fd, + stderr=subprocess.STDOUT, + cwd=self.working_dir) log.info("QEMU VM instance {} started PID={}".format(self._id, self._process.pid)) self._started = True except (OSError, subprocess.SubprocessError) as e: stdout = self.read_stdout() - log.error("could not start QEMU {}: {}\n{}".format(self._qemu_path, e, stdout)) - raise QemuError("could not start QEMU {}: {}\n{}".format(self._qemu_path, e, stdout)) + log.error("could not start QEMU {}: {}\n{}".format(self.qemu_path, e, stdout)) + raise QemuError("could not start QEMU {}: {}\n{}".format(self.qemu_path, e, stdout)) self._set_process_priority() if self._cpu_throttling: self._set_cpu_throttling() + @asyncio.coroutine def stop(self): """ Stops this QEMU VM. @@ -854,6 +623,7 @@ class QemuVM(BaseVM): self._started = False self._stop_cpulimit() + @asyncio.coroutine def _control_vm(self, command, expected=None, timeout=30): """ Executes a command with QEMU monitor when this VM is running. @@ -889,6 +659,18 @@ class QemuVM(BaseVM): tn.close() return result + @asyncio.coroutine + def close(self): + + yield from self.stop() + if self._console: + self._manager.port_manager.release_console_port(self._console) + self._console = None + if self._monitor: + self._manager.port_manager.release_console_port(self._monitor) + self._monitor = None + + @asyncio.coroutine def _get_vm_status(self): """ Returns this VM suspend status (running|paused) @@ -898,43 +680,47 @@ class QemuVM(BaseVM): result = None - match = self._control_vm("info status", [b"running", b"paused"]) + match = yield from self._control_vm("info status", [b"running", b"paused"]) if match: result = match.group(0).decode('ascii') return result + @asyncio.coroutine def suspend(self): """ Suspends this QEMU VM. """ - vm_status = self._get_vm_status() + vm_status = yield from self._get_vm_status() if vm_status == "running": - self._control_vm("stop") + yield from self._control_vm("stop") log.debug("QEMU VM has been suspended") else: log.info("QEMU VM is not running to be suspended, current status is {}".format(vm_status)) + @asyncio.coroutine def reload(self): """ Reloads this QEMU VM. """ - self._control_vm("system_reset") + yield from self._control_vm("system_reset") log.debug("QEMU VM has been reset") + @asyncio.coroutine def resume(self): """ Resumes this QEMU VM. """ - vm_status = self._get_vm_status() + vm_status = yield from self._get_vm_status() if vm_status == "paused": - self._control_vm("cont") + yield from self._control_vm("cont") log.debug("QEMU VM has been resumed") else: log.info("QEMU VM is not paused to be resumed, current status is {}".format(vm_status)) + @asyncio.coroutine def port_add_nio_binding(self, adapter_id, nio): """ Adds a port NIO binding. @@ -953,20 +739,20 @@ class QemuVM(BaseVM): # dynamically configure an UDP tunnel on the QEMU VM adapter if nio and isinstance(nio, NIO_UDP): if self._legacy_networking: - self._control_vm("host_net_remove {} gns3-{}".format(adapter_id, adapter_id)) - self._control_vm("host_net_add udp vlan={},name=gns3-{},sport={},dport={},daddr={}".format(adapter_id, - adapter_id, - nio.lport, - nio.rport, - nio.rhost)) + yield from self._control_vm("host_net_remove {} gns3-{}".format(adapter_id, adapter_id)) + yield from self._control_vm("host_net_add udp vlan={},name=gns3-{},sport={},dport={},daddr={}".format(adapter_id, + adapter_id, + nio.lport, + nio.rport, + nio.rhost)) else: - self._control_vm("host_net_remove {} gns3-{}".format(adapter_id, adapter_id)) - self._control_vm("host_net_add socket vlan={},name=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_id, - adapter_id, - nio.rhost, - nio.rport, - self._host, - nio.lport)) + yield from self._control_vm("host_net_remove {} gns3-{}".format(adapter_id, adapter_id)) + yield from self._control_vm("host_net_add socket vlan={},name=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_id, + adapter_id, + nio.rhost, + nio.rport, + self._host, + nio.lport)) adapter.add_nio(0, nio) log.info("QEMU VM {name} [id={id}]: {nio} added to adapter {adapter_id}".format(name=self._name, @@ -974,6 +760,7 @@ class QemuVM(BaseVM): nio=nio, adapter_id=adapter_id)) + @asyncio.coroutine def port_remove_nio_binding(self, adapter_id): """ Removes a port NIO binding. @@ -991,8 +778,8 @@ class QemuVM(BaseVM): if self.is_running(): # dynamically disable the QEMU VM adapter - self._control_vm("host_net_remove {} gns3-{}".format(adapter_id, adapter_id)) - self._control_vm("host_net_add user vlan={},name=gns3-{}".format(adapter_id, adapter_id)) + yield from self._control_vm("host_net_remove {} gns3-{}".format(adapter_id, adapter_id)) + yield from self._control_vm("host_net_add user vlan={},name=gns3-{}".format(adapter_id, adapter_id)) nio = adapter.get_nio(0) adapter.remove_nio(0) @@ -1034,7 +821,7 @@ class QemuVM(BaseVM): :returns: True or False """ - if self._process and self._process.poll() is None: + if self._process: return True return False @@ -1061,11 +848,12 @@ class QemuVM(BaseVM): else: return [] + @asyncio.coroutine def _disk_options(self): options = [] qemu_img_path = "" - qemu_path_dir = os.path.dirname(self._qemu_path) + qemu_path_dir = os.path.dirname(self.qemu_path) try: for f in os.listdir(qemu_path_dir): if f.startswith("qemu-img"): @@ -1083,17 +871,19 @@ class QemuVM(BaseVM): raise QemuError("hda disk image '{}' linked to '{}' is not accessible".format(self._hda_disk_image, os.path.realpath(self._hda_disk_image))) else: raise QemuError("hda disk image '{}' is not accessible".format(self._hda_disk_image)) - hda_disk = os.path.join(self._working_dir, "hda_disk.qcow2") + hda_disk = os.path.join(self.working_dir, "hda_disk.qcow2") if not os.path.exists(hda_disk): - retcode = subprocess.call([qemu_img_path, "create", "-o", - "backing_file={}".format(self._hda_disk_image), - "-f", "qcow2", hda_disk]) + process = yield from asyncio.create_subprocess_exec(qemu_img_path, "create", "-o", + "backing_file={}".format(self._hda_disk_image), + "-f", "qcow2", hda_disk) + retcode = yield from process.wait() log.info("{} returned with {}".format(qemu_img_path, retcode)) else: # create a "FLASH" with 256MB if no disk image has been specified - hda_disk = os.path.join(self._working_dir, "flash.qcow2") + hda_disk = os.path.join(self.working_dir, "flash.qcow2") if not os.path.exists(hda_disk): - retcode = subprocess.call([qemu_img_path, "create", "-f", "qcow2", hda_disk, "128M"]) + process = yield from asyncio.create_subprocess_exec(qemu_img_path, "create", "-f", "qcow2", hda_disk, "128M") + retcode = yield from process.wait() log.info("{} returned with {}".format(qemu_img_path, retcode)) except (OSError, subprocess.SubprocessError) as e: @@ -1106,12 +896,13 @@ class QemuVM(BaseVM): raise QemuError("hdb disk image '{}' linked to '{}' is not accessible".format(self._hdb_disk_image, os.path.realpath(self._hdb_disk_image))) else: raise QemuError("hdb disk image '{}' is not accessible".format(self._hdb_disk_image)) - hdb_disk = os.path.join(self._working_dir, "hdb_disk.qcow2") + hdb_disk = os.path.join(self.working_dir, "hdb_disk.qcow2") if not os.path.exists(hdb_disk): try: - retcode = subprocess.call([qemu_img_path, "create", "-o", - "backing_file={}".format(self._hdb_disk_image), - "-f", "qcow2", hdb_disk]) + process = yield from asyncio.create_subprocess_exec(qemu_img_path, "create", "-o", + "backing_file={}".format(self._hdb_disk_image), + "-f", "qcow2", hdb_disk) + retcode = yield from process.wait() log.info("{} returned with {}".format(qemu_img_path, retcode)) except (OSError, subprocess.SubprocessError) as e: raise QemuError("Could not create disk image {}".format(e)) @@ -1170,16 +961,18 @@ class QemuVM(BaseVM): return network_options + @asyncio.coroutine def _build_command(self): """ Command to start the QEMU process. (to be passed to subprocess.Popen()) """ - command = [self._qemu_path] + command = [self.qemu_path] command.extend(["-name", self._name]) command.extend(["-m", str(self._ram)]) - command.extend(self._disk_options()) + disk_options = yield from self._disk_options() + command.extend(disk_options) command.extend(self._linux_boot_options()) command.extend(self._serial_options()) command.extend(self._monitor_options()) diff --git a/tests/modules/qemu/test_qemu_vm.py b/tests/modules/qemu/test_qemu_vm.py new file mode 100644 index 00000000..238520d7 --- /dev/null +++ b/tests/modules/qemu/test_qemu_vm.py @@ -0,0 +1,188 @@ +# -*- 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 pytest +import aiohttp +import asyncio +import os +import stat +from tests.utils import asyncio_patch + + +from unittest.mock import patch, MagicMock +from gns3server.modules.qemu.qemu_vm import QemuVM +from gns3server.modules.qemu.qemu_error import QemuError +from gns3server.modules.qemu import Qemu + + +@pytest.fixture(scope="module") +def manager(port_manager): + m = Qemu.instance() + m.port_manager = port_manager + return m + + +@pytest.fixture +def fake_qemu_img_binary(): + + bin_path = os.path.join(os.environ["PATH"], "qemu-img") + with open(bin_path, "w+") as f: + f.write("1") + os.chmod(bin_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) + return bin_path + + +@pytest.fixture +def fake_qemu_binary(): + + bin_path = os.path.join(os.environ["PATH"], "qemu_x42") + with open(bin_path, "w+") as f: + f.write("1") + os.chmod(bin_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) + return bin_path + + +@pytest.fixture(scope="function") +def vm(project, manager, fake_qemu_binary, fake_qemu_img_binary): + return QemuVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager, qemu_path=fake_qemu_binary) + + +def test_vm(project, manager, fake_qemu_binary): + vm = QemuVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager, qemu_path=fake_qemu_binary) + assert vm.name == "test" + assert vm.id == "00010203-0405-0607-0809-0a0b0c0d0e0f" + + +def test_start(loop, vm): + with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): + loop.run_until_complete(asyncio.async(vm.start())) + assert vm.is_running() + + +def test_stop(loop, vm): + process = MagicMock() + + # Wait process kill success + future = asyncio.Future() + future.set_result(True) + process.wait.return_value = future + + with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): + nio = Qemu.instance().create_nio(vm.qemu_path, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) + vm.port_add_nio_binding(0, nio) + loop.run_until_complete(asyncio.async(vm.start())) + assert vm.is_running() + loop.run_until_complete(asyncio.async(vm.stop())) + assert vm.is_running() is False + process.terminate.assert_called_with() + + +def test_reload(loop, vm): + + with asyncio_patch("gns3server.modules.qemu.QemuVM._control_vm") as mock: + loop.run_until_complete(asyncio.async(vm.reload())) + assert mock.called_with("system_reset") + + +def test_add_nio_binding_udp(vm, loop): + nio = Qemu.instance().create_nio(vm.qemu_path, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) + loop.run_until_complete(asyncio.async(vm.port_add_nio_binding(0, nio))) + assert nio.lport == 4242 + + +def test_add_nio_binding_tap(vm, loop): + with patch("gns3server.modules.base_manager.BaseManager._has_privileged_access", return_value=True): + nio = Qemu.instance().create_nio(vm.qemu_path, {"type": "nio_tap", "tap_device": "test"}) + loop.run_until_complete(asyncio.async(vm.port_add_nio_binding(0, nio))) + assert nio.tap_device == "test" + + +def test_port_remove_nio_binding(vm, loop): + nio = Qemu.instance().create_nio(vm.qemu_path, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) + loop.run_until_complete(asyncio.async(vm.port_add_nio_binding(0, nio))) + loop.run_until_complete(asyncio.async(vm.port_remove_nio_binding(0))) + assert vm._ethernet_adapters[0].ports[0] is None + + +def test_close(vm, port_manager, loop): + with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): + loop.run_until_complete(asyncio.async(vm.start())) + + console_port = vm.console + monitor_port = vm.monitor + + loop.run_until_complete(asyncio.async(vm.close())) + + # Raise an exception if the port is not free + port_manager.reserve_console_port(console_port) + # Raise an exception if the port is not free + port_manager.reserve_console_port(monitor_port) + + assert vm.is_running() is False + + +def test_set_qemu_path(vm, tmpdir, fake_qemu_binary): + + # Raise because none + with pytest.raises(QemuError): + vm.qemu_path = None + + path = str(tmpdir / "bla") + + # Raise because file doesn't exists + with pytest.raises(QemuError): + vm.qemu_path = path + + with open(path, "w+") as f: + f.write("1") + + # Raise because file is not executable + with pytest.raises(QemuError): + vm.qemu_path = path + + os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) + + vm.qemu_path = path + assert vm.qemu_path == path + + +def test_set_qemu_path_environ(vm, tmpdir, fake_qemu_binary): + + # It should find the binary in the path + vm.qemu_path = "qemu_x42" + + assert vm.qemu_path == fake_qemu_binary + + +def test_disk_options(vm, loop, fake_qemu_img_binary): + + with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()) as process: + loop.run_until_complete(asyncio.async(vm._disk_options())) + assert process.called + args, kwargs = process.call_args + assert args == (fake_qemu_img_binary, "create", "-f", "qcow2", os.path.join(vm.working_dir, "flash.qcow2"), "128M") + + +def test_set_process_priority(vm, loop, fake_qemu_img_binary): + + with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()) as process: + vm._process = MagicMock() + vm._process.pid = 42 + loop.run_until_complete(asyncio.async(vm._set_process_priority())) + assert process.called + args, kwargs = process.call_args + assert args == ("renice", "-n", "5", "-p", "42") From d0244824bffed27e714c162c850e86102a55abf9 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 19 Feb 2015 19:43:45 +0100 Subject: [PATCH 275/485] Get a working Qemu handler. Next step add all parameters --- gns3server/handlers/__init__.py | 3 +- gns3server/handlers/qemu_handler.py | 254 +++++++++++++++++++++ gns3server/modules/__init__.py | 3 +- gns3server/modules/base_vm.py | 9 +- gns3server/modules/qemu/qemu_vm.py | 33 ++- gns3server/schemas/qemu.py | 332 ++++++++++++++++++++++++++++ tests/api/test_qemu.py | 169 ++++++++++++++ tests/modules/qemu/test_qemu_vm.py | 26 ++- 8 files changed, 799 insertions(+), 30 deletions(-) create mode 100644 gns3server/handlers/qemu_handler.py create mode 100644 gns3server/schemas/qemu.py create mode 100644 tests/api/test_qemu.py diff --git a/gns3server/handlers/__init__.py b/gns3server/handlers/__init__.py index 03e6dcab..5d558245 100644 --- a/gns3server/handlers/__init__.py +++ b/gns3server/handlers/__init__.py @@ -5,4 +5,5 @@ __all__ = ["version_handler", "virtualbox_handler", "dynamips_vm_handler", "dynamips_device_handler", - "iou_handler"] + "iou_handler", + "qemu_handler"] diff --git a/gns3server/handlers/qemu_handler.py b/gns3server/handlers/qemu_handler.py new file mode 100644 index 00000000..a40bfd78 --- /dev/null +++ b/gns3server/handlers/qemu_handler.py @@ -0,0 +1,254 @@ +# -*- 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 os + + +from ..web.route import Route +from ..modules.port_manager import PortManager +from ..schemas.qemu import QEMU_CREATE_SCHEMA +from ..schemas.qemu import QEMU_UPDATE_SCHEMA +from ..schemas.qemu import QEMU_OBJECT_SCHEMA +from ..schemas.qemu import QEMU_NIO_SCHEMA +from ..modules.qemu import Qemu + + +class QEMUHandler: + + """ + API entry points for QEMU. + """ + + @classmethod + @Route.post( + r"/projects/{project_id}/qemu/vms", + parameters={ + "project_id": "UUID for the project" + }, + status_codes={ + 201: "Instance created", + 400: "Invalid request", + 409: "Conflict" + }, + description="Create a new Qemu.instance", + input=QEMU_CREATE_SCHEMA, + output=QEMU_OBJECT_SCHEMA) + def create(request, response): + + qemu = Qemu.instance() + vm = yield from qemu.create_vm(request.json["name"], + request.match_info["project_id"], + request.json.get("vm_id"), + qemu_path=request.json.get("qemu_path"), + console=request.json.get("console"), + monitor=request.json.get("monitor"), + console_host=PortManager.instance().console_host, + monitor_host=PortManager.instance().console_host, + ) + response.set_status(201) + response.json(vm) + + @classmethod + @Route.get( + r"/projects/{project_id}/qemu/vms/{vm_id}", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 200: "Success", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Get a Qemu.instance", + output=QEMU_OBJECT_SCHEMA) + def show(request, response): + + qemu_manager = Qemu.instance() + vm = qemu_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + response.json(vm) + + @classmethod + @Route.put( + r"/projects/{project_id}/qemu/vms/{vm_id}", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 200: "Instance updated", + 400: "Invalid request", + 404: "Instance doesn't exist", + 409: "Conflict" + }, + description="Update a Qemu.instance", + input=QEMU_UPDATE_SCHEMA, + output=QEMU_OBJECT_SCHEMA) + def update(request, response): + + qemu_manager = Qemu.instance() + vm = qemu_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + vm.name = request.json.get("name", vm.name) + vm.console = request.json.get("console", vm.console) + vm.qemu_path = request.json.get("qemu_path", vm.qemu_path) + + response.json(vm) + + @classmethod + @Route.delete( + r"/projects/{project_id}/qemu/vms/{vm_id}", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 204: "Instance deleted", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Delete a Qemu.instance") + def delete(request, response): + + yield from Qemu.instance().delete_vm(request.match_info["vm_id"]) + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/qemu/vms/{vm_id}/start", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 204: "Instance started", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Start a Qemu.instance") + def start(request, response): + + qemu_manager = Qemu.instance() + vm = qemu_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + yield from vm.start() + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/qemu/vms/{vm_id}/stop", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 204: "Instance stopped", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Stop a Qemu.instance") + def stop(request, response): + + qemu_manager = Qemu.instance() + vm = qemu_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + yield from vm.stop() + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/qemu/vms/{vm_id}/reload", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance", + }, + status_codes={ + 204: "Instance reloaded", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Reload a Qemu.instance") + def reload(request, response): + + qemu_manager = Qemu.instance() + vm = qemu_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + yield from vm.reload() + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/qemu/vms/{vm_id}/suspend", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance", + }, + status_codes={ + 204: "Instance suspended", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Reload a Qemu.instance") + def suspend(request, response): + + qemu_manager = Qemu.instance() + vm = qemu_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + yield from vm.suspend() + response.set_status(204) + + @Route.post( + r"/projects/{project_id}/qemu/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance", + "adapter_number": "Network adapter where the nio is located", + "port_number": "Port where the nio should be added" + }, + status_codes={ + 201: "NIO created", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Add a NIO to a Qemu.instance", + input=QEMU_NIO_SCHEMA, + output=QEMU_NIO_SCHEMA) + def create_nio(request, response): + + qemu_manager = Qemu.instance() + vm = qemu_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + nio = qemu_manager.create_nio(vm.qemu_path, request.json) + vm.adapter_add_nio_binding(int(request.match_info["adapter_number"]), int(request.match_info["port_number"]), nio) + response.set_status(201) + response.json(nio) + + @classmethod + @Route.delete( + r"/projects/{project_id}/qemu/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance", + "adapter_number": "Network adapter where the nio is located", + "port_number": "Port from where the nio should be removed" + }, + status_codes={ + 204: "NIO deleted", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Remove a NIO from a Qemu.instance") + def delete_nio(request, response): + + qemu_manager = Qemu.instance() + vm = qemu_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + vm.adapter_remove_nio_binding(int(request.match_info["adapter_number"]), int(request.match_info["port_number"])) + response.set_status(204) diff --git a/gns3server/modules/__init__.py b/gns3server/modules/__init__.py index 25c1012f..0f55e396 100644 --- a/gns3server/modules/__init__.py +++ b/gns3server/modules/__init__.py @@ -19,5 +19,6 @@ from .vpcs import VPCS from .virtualbox import VirtualBox from .dynamips import Dynamips from .iou import IOU +from .qemu import Qemu -MODULES = [VPCS, VirtualBox, Dynamips, IOU] +MODULES = [VPCS, VirtualBox, Dynamips, IOU, Qemu] diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index c5b39405..6a3f29c1 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -51,9 +51,12 @@ class BaseVM: else: self._console = self._manager.port_manager.get_free_console_port() - log.debug("{module}: {name} [{id}] initialized".format(module=self.manager.module_name, - name=self.name, - id=self.id)) + log.debug("{module}: {name} [{id}] initialized. Console port {console}".format( + module=self.manager.module_name, + name=self.name, + id=self.id, + console=self._console + )) def __del__(self): diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index 9cb5960e..01b36e47 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -59,12 +59,8 @@ class QemuVM(BaseVM): :param qemu_id: QEMU VM instance ID :param console: TCP console port :param console_host: IP address to bind for console connections - :param console_start_port_range: TCP console port range start - :param console_end_port_range: TCP console port range end :param monitor: TCP monitor port :param monitor_host: IP address to bind for monitor connections - :param monitor_start_port_range: TCP monitor port range start - :param monitor_end_port_range: TCP monitor port range end """ def __init__(self, @@ -76,27 +72,19 @@ class QemuVM(BaseVM): host="127.0.0.1", console=None, console_host="0.0.0.0", - console_start_port_range=5001, - console_end_port_range=5500, monitor=None, - monitor_host="0.0.0.0", - monitor_start_port_range=5501, - monitor_end_port_range=6000): + monitor_host="0.0.0.0"): super().__init__(name, vm_id, project, manager, console=console) self._host = host + self._console_host = console_host self._command = [] self._started = False self._process = None self._cpulimit_process = None self._stdout_file = "" - self._console_host = console_host - self._console_start_port_range = console_start_port_range - self._console_end_port_range = console_end_port_range self._monitor_host = monitor_host - self._monitor_start_port_range = monitor_start_port_range - self._monitor_end_port_range = monitor_end_port_range # QEMU settings self.qemu_path = qemu_path @@ -104,7 +92,6 @@ class QemuVM(BaseVM): self._hdb_disk_image = "" self._options = "" self._ram = 256 - self._console = console self._monitor = monitor self._ethernet_adapters = [] self._adapter_type = "e1000" @@ -629,6 +616,7 @@ class QemuVM(BaseVM): Executes a command with QEMU monitor when this VM is running. :param command: QEMU monitor command (e.g. info status, stop etc.) + :params expected: An array with the string attended (Default None) :param timeout: how long to wait for QEMU monitor :returns: result of the command (Match object or None) @@ -721,11 +709,12 @@ class QemuVM(BaseVM): log.info("QEMU VM is not paused to be resumed, current status is {}".format(vm_status)) @asyncio.coroutine - def port_add_nio_binding(self, adapter_id, nio): + def adapter_add_nio_binding(self, adapter_id, port_id, nio): """ Adds a port NIO binding. :param adapter_id: adapter ID + :param port_id: port ID :param nio: NIO instance to add to the slot/port """ @@ -761,11 +750,12 @@ class QemuVM(BaseVM): adapter_id=adapter_id)) @asyncio.coroutine - def port_remove_nio_binding(self, adapter_id): + def adapter_remove_nio_binding(self, adapter_id, port_id): """ Removes a port NIO binding. :param adapter_id: adapter ID + :param port_id: port ID :returns: NIO instance """ @@ -981,3 +971,12 @@ class QemuVM(BaseVM): command.extend(shlex.split(additional_options)) command.extend(self._network_options()) return command + + def __json__(self): + return { + "vm_id": self.id, + "project_id": self.project.id, + "name": self.name, + "console": self.console, + "monitor": self.monitor + } diff --git a/gns3server/schemas/qemu.py b/gns3server/schemas/qemu.py new file mode 100644 index 00000000..bbd2aabf --- /dev/null +++ b/gns3server/schemas/qemu.py @@ -0,0 +1,332 @@ +# -*- 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 . + + +QEMU_CREATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to create a new QEMU VM instance", + "type": "object", + "properties": { + "name": { + "description": "QEMU VM instance name", + "type": "string", + "minLength": 1, + }, + "qemu_path": { + "description": "Path to QEMU", + "type": "string", + "minLength": 1, + }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, + "monitor": { + "description": "monitor TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["name", "qemu_path"], +} + +QEMU_UPDATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to update a QEMU VM instance", + "type": "object", + "properties": { + "name": { + "description": "QEMU VM instance name", + "type": "string", + "minLength": 1, + }, + "qemu_path": { + "description": "path to QEMU", + "type": "string", + "minLength": 1, + }, + "hda_disk_image": { + "description": "QEMU hda disk image path", + "type": "string", + }, + "hdb_disk_image": { + "description": "QEMU hdb disk image path", + "type": "string", + }, + "ram": { + "description": "amount of RAM in MB", + "type": "integer" + }, + "adapters": { + "description": "number of adapters", + "type": "integer", + "minimum": 0, + "maximum": 32, + }, + "adapter_type": { + "description": "QEMU adapter type", + "type": "string", + "minLength": 1, + }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, + "monitor": { + "description": "monitor TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, + "initrd": { + "description": "QEMU initrd path", + "type": "string", + }, + "kernel_image": { + "description": "QEMU kernel image path", + "type": "string", + }, + "kernel_command_line": { + "description": "QEMU kernel command line", + "type": "string", + }, + "cloud_path": { + "description": "Path to the image in the cloud object store", + "type": "string", + }, + "legacy_networking": { + "description": "Use QEMU legagy networking commands (-net syntax)", + "type": "boolean", + }, + "cpu_throttling": { + "description": "Percentage of CPU allowed for QEMU", + "minimum": 0, + "maximum": 800, + "type": "integer", + }, + "process_priority": { + "description": "Process priority for QEMU", + "enum": ["realtime", + "very high", + "high", + "normal", + "low", + "very low"] + }, + "options": { + "description": "Additional QEMU options", + "type": "string", + }, + }, + "additionalProperties": False, +} + +QEMU_START_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to start a QEMU VM instance", + "type": "object", + "properties": { + "id": { + "description": "QEMU VM instance ID", + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["id"] +} + +QEMU_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 + }, + "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 + }, + }, + "oneOf": [ + {"$ref": "#/definitions/UDP"}, + {"$ref": "#/definitions/Ethernet"}, + {"$ref": "#/definitions/TAP"}, + ], + "additionalProperties": True, + "required": ["type"] +} + +QEMU_OBJECT_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation for a QEMU VM instance", + "type": "object", + "properties": { + "vm_id": { + "description": "QEMU VM uuid", + "type": "string", + "minLength": 1, + }, + "project_id": { + "description": "Project uuid", + "type": "string", + "minLength": 1, + }, + "name": { + "description": "QEMU VM instance name", + "type": "string", + "minLength": 1, + }, + "qemu_path": { + "description": "path to QEMU", + "type": "string", + "minLength": 1, + }, + "hda_disk_image": { + "description": "QEMU hda disk image path", + "type": "string", + }, + "hdb_disk_image": { + "description": "QEMU hdb disk image path", + "type": "string", + }, + "ram": { + "description": "amount of RAM in MB", + "type": "integer" + }, + "adapters": { + "description": "number of adapters", + "type": "integer", + "minimum": 0, + "maximum": 32, + }, + "adapter_type": { + "description": "QEMU adapter type", + "type": "string", + "minLength": 1, + }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, + "monitor": { + "description": "monitor TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, + "initrd": { + "description": "QEMU initrd path", + "type": "string", + }, + "kernel_image": { + "description": "QEMU kernel image path", + "type": "string", + }, + "kernel_command_line": { + "description": "QEMU kernel command line", + "type": "string", + }, + "cloud_path": { + "description": "Path to the image in the cloud object store", + "type": "string", + }, + "legacy_networking": { + "description": "Use QEMU legagy networking commands (-net syntax)", + "type": "boolean", + }, + "cpu_throttling": { + "description": "Percentage of CPU allowed for QEMU", + "minimum": 0, + "maximum": 800, + "type": "integer", + }, + "process_priority": { + "description": "Process priority for QEMU", + "enum": ["realtime", + "very high", + "high", + "normal", + "low", + "very low"] + }, + "options": { + "description": "Additional QEMU options", + "type": "string", + }, + }, + "additionalProperties": False, + "required": ["vm_id"] +} diff --git a/tests/api/test_qemu.py b/tests/api/test_qemu.py new file mode 100644 index 00000000..25cc9f9b --- /dev/null +++ b/tests/api/test_qemu.py @@ -0,0 +1,169 @@ +# -*- 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 pytest +import os +import stat +from tests.utils import asyncio_patch +from unittest.mock import patch + + +@pytest.fixture +def fake_qemu_bin(): + + bin_path = os.path.join(os.environ["PATH"], "qemu_x42") + with open(bin_path, "w+") as f: + f.write("1") + os.chmod(bin_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) + return bin_path + + +@pytest.fixture +def base_params(tmpdir, fake_qemu_bin): + """Return standard parameters""" + return {"name": "PC TEST 1", "qemu_path": fake_qemu_bin} + + +@pytest.fixture +def vm(server, project, base_params): + response = server.post("/projects/{project_id}/qemu/vms".format(project_id=project.id), base_params) + assert response.status == 201 + return response.json + + +def test_qemu_create(server, project, base_params): + response = server.post("/projects/{project_id}/qemu/vms".format(project_id=project.id), base_params) + assert response.status == 201 + assert response.route == "/projects/{project_id}/qemu/vms" + assert response.json["name"] == "PC TEST 1" + assert response.json["project_id"] == project.id + + +def test_qemu_create_with_params(server, project, base_params): + params = base_params + + response = server.post("/projects/{project_id}/qemu/vms".format(project_id=project.id), params, example=True) + assert response.status == 201 + assert response.route == "/projects/{project_id}/qemu/vms" + assert response.json["name"] == "PC TEST 1" + assert response.json["project_id"] == project.id + + +def test_qemu_get(server, project, vm): + response = server.get("/projects/{project_id}/qemu/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) + assert response.status == 200 + assert response.route == "/projects/{project_id}/qemu/vms/{vm_id}" + assert response.json["name"] == "PC TEST 1" + assert response.json["project_id"] == project.id + + +def test_qemu_start(server, vm): + with asyncio_patch("gns3server.modules.qemu.qemu_vm.QemuVM.start", return_value=True) as mock: + response = server.post("/projects/{project_id}/qemu/vms/{vm_id}/start".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + assert mock.called + assert response.status == 204 + + +def test_qemu_stop(server, vm): + with asyncio_patch("gns3server.modules.qemu.qemu_vm.QemuVM.stop", return_value=True) as mock: + response = server.post("/projects/{project_id}/qemu/vms/{vm_id}/stop".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + assert mock.called + assert response.status == 204 + + +def test_qemu_reload(server, vm): + with asyncio_patch("gns3server.modules.qemu.qemu_vm.QemuVM.reload", return_value=True) as mock: + response = server.post("/projects/{project_id}/qemu/vms/{vm_id}/reload".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + assert mock.called + assert response.status == 204 + + +def test_qemu_suspend(server, vm): + with asyncio_patch("gns3server.modules.qemu.qemu_vm.QemuVM.suspend", return_value=True) as mock: + response = server.post("/projects/{project_id}/qemu/vms/{vm_id}/suspend".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + assert mock.called + assert response.status == 204 + + +def test_qemu_delete(server, vm): + with asyncio_patch("gns3server.modules.qemu.Qemu.delete_vm", return_value=True) as mock: + response = server.delete("/projects/{project_id}/qemu/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + assert mock.called + assert response.status == 204 + + +def test_qemu_update(server, vm, tmpdir, free_console_port, project): + params = { + "name": "test", + "console": free_console_port, + } + response = server.put("/projects/{project_id}/qemu/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), params) + assert response.status == 200 + assert response.json["name"] == "test" + assert response.json["console"] == free_console_port + + +def test_qemu_nio_create_udp(server, vm): + response = server.post("/projects/{project_id}/qemu/vms/{vm_id}/adapters/1/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}, + example=True) + assert response.status == 201 + assert response.route == "/projects/{project_id}/qemu/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" + assert response.json["type"] == "nio_udp" + + +def test_qemu_nio_create_ethernet(server, vm): + response = server.post("/projects/{project_id}/qemu/vms/{vm_id}/adapters/1/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_generic_ethernet", + "ethernet_device": "eth0", + }, + example=True) + assert response.status == 201 + assert response.route == "/projects/{project_id}/qemu/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" + assert response.json["type"] == "nio_generic_ethernet" + assert response.json["ethernet_device"] == "eth0" + + +def test_qemu_nio_create_ethernet_different_port(server, vm): + response = server.post("/projects/{project_id}/qemu/vms/{vm_id}/adapters/0/ports/3/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_generic_ethernet", + "ethernet_device": "eth0", + }, + example=False) + assert response.status == 201 + assert response.route == "/projects/{project_id}/qemu/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" + assert response.json["type"] == "nio_generic_ethernet" + assert response.json["ethernet_device"] == "eth0" + + +def test_qemu_nio_create_tap(server, vm): + with patch("gns3server.modules.base_manager.BaseManager._has_privileged_access", return_value=True): + response = server.post("/projects/{project_id}/qemu/vms/{vm_id}/adapters/1/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_tap", + "tap_device": "test"}) + assert response.status == 201 + assert response.route == "/projects/{project_id}/qemu/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" + assert response.json["type"] == "nio_tap" + + +def test_qemu_delete_nio(server, vm): + server.post("/projects/{project_id}/qemu/vms/{vm_id}/adapters/1/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}) + response = server.delete("/projects/{project_id}/qemu/vms/{vm_id}/adapters/1/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) + assert response.status == 204 + assert response.route == "/projects/{project_id}/qemu/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" diff --git a/tests/modules/qemu/test_qemu_vm.py b/tests/modules/qemu/test_qemu_vm.py index 238520d7..63b89572 100644 --- a/tests/modules/qemu/test_qemu_vm.py +++ b/tests/modules/qemu/test_qemu_vm.py @@ -20,6 +20,7 @@ import aiohttp import asyncio import os import stat +import re from tests.utils import asyncio_patch @@ -83,7 +84,7 @@ def test_stop(loop, vm): with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): nio = Qemu.instance().create_nio(vm.qemu_path, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) - vm.port_add_nio_binding(0, nio) + vm.adapter_add_nio_binding(0, 0, nio) loop.run_until_complete(asyncio.async(vm.start())) assert vm.is_running() loop.run_until_complete(asyncio.async(vm.stop())) @@ -98,23 +99,32 @@ def test_reload(loop, vm): assert mock.called_with("system_reset") +def test_suspend(loop, vm): + + control_vm_result = MagicMock() + control_vm_result.match.group.decode.return_value = "running" + with asyncio_patch("gns3server.modules.qemu.QemuVM._control_vm", return_value=control_vm_result) as mock: + loop.run_until_complete(asyncio.async(vm.suspend())) + assert mock.called_with("system_reset") + + def test_add_nio_binding_udp(vm, loop): nio = Qemu.instance().create_nio(vm.qemu_path, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) - loop.run_until_complete(asyncio.async(vm.port_add_nio_binding(0, nio))) + loop.run_until_complete(asyncio.async(vm.adapter_add_nio_binding(0, 0, nio))) assert nio.lport == 4242 -def test_add_nio_binding_tap(vm, loop): +def test_add_nio_binding_ethernet(vm, loop): with patch("gns3server.modules.base_manager.BaseManager._has_privileged_access", return_value=True): - nio = Qemu.instance().create_nio(vm.qemu_path, {"type": "nio_tap", "tap_device": "test"}) - loop.run_until_complete(asyncio.async(vm.port_add_nio_binding(0, nio))) - assert nio.tap_device == "test" + nio = Qemu.instance().create_nio(vm.qemu_path, {"type": "nio_generic_ethernet", "ethernet_device": "eth0"}) + loop.run_until_complete(asyncio.async(vm.adapter_add_nio_binding(0, 0, nio))) + assert nio.ethernet_device == "eth0" def test_port_remove_nio_binding(vm, loop): nio = Qemu.instance().create_nio(vm.qemu_path, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) - loop.run_until_complete(asyncio.async(vm.port_add_nio_binding(0, nio))) - loop.run_until_complete(asyncio.async(vm.port_remove_nio_binding(0))) + loop.run_until_complete(asyncio.async(vm.adapter_add_nio_binding(0, 0, nio))) + loop.run_until_complete(asyncio.async(vm.adapter_remove_nio_binding(0, 0))) assert vm._ethernet_adapters[0].ports[0] is None From 48f5c7c8b30aae982e6e539542a15b8200cdfb72 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 19 Feb 2015 20:22:30 +0100 Subject: [PATCH 276/485] All params for qemu --- gns3server/handlers/qemu_handler.py | 12 +- gns3server/modules/qemu/qemu_vm.py | 13 ++- gns3server/schemas/qemu.py | 167 +++++++++++++++++----------- tests/api/test_qemu.py | 28 ++--- tests/modules/qemu/test_qemu_vm.py | 7 ++ 5 files changed, 131 insertions(+), 96 deletions(-) diff --git a/gns3server/handlers/qemu_handler.py b/gns3server/handlers/qemu_handler.py index a40bfd78..9ccbd237 100644 --- a/gns3server/handlers/qemu_handler.py +++ b/gns3server/handlers/qemu_handler.py @@ -59,6 +59,13 @@ class QEMUHandler: console_host=PortManager.instance().console_host, monitor_host=PortManager.instance().console_host, ) + # Clear already used keys + map(request.json.__delitem__, ["name", "project_id", "vm_id", + "qemu_path", "console", "monitor"]) + + for field in request.json: + setattr(vm, field, request.json[field]) + response.set_status(201) response.json(vm) @@ -102,9 +109,8 @@ class QEMUHandler: qemu_manager = Qemu.instance() vm = qemu_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) - vm.name = request.json.get("name", vm.name) - vm.console = request.json.get("console", vm.console) - vm.qemu_path = request.json.get("qemu_path", vm.qemu_path) + for field in request.json: + setattr(vm, field, request.json[field]) response.json(vm) diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index 01b36e47..23ddf018 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -38,6 +38,7 @@ from ..adapters.ethernet_adapter import EthernetAdapter from ..nios.nio_udp import NIO_UDP from ..base_vm import BaseVM from ...utils.asyncio import subprocess_check_output +from ...schemas.qemu import QEMU_OBJECT_SCHEMA import logging log = logging.getLogger(__name__) @@ -973,10 +974,12 @@ class QemuVM(BaseVM): return command def __json__(self): - return { - "vm_id": self.id, + answer = { "project_id": self.project.id, - "name": self.name, - "console": self.console, - "monitor": self.monitor + "vm_id": self.id } + # Qemu has a long list of options. The JSON schema is the single source of informations + for field in QEMU_OBJECT_SCHEMA["required"]: + if field not in answer: + answer[field] = getattr(self, field) + return answer diff --git a/gns3server/schemas/qemu.py b/gns3server/schemas/qemu.py index bbd2aabf..065f4a73 100644 --- a/gns3server/schemas/qemu.py +++ b/gns3server/schemas/qemu.py @@ -21,6 +21,11 @@ QEMU_CREATE_SCHEMA = { "description": "Request validation to create a new QEMU VM instance", "type": "object", "properties": { + "vm_id": { + "description": "QEMU VM UUID", + "type": ["string", "null"], + "minLength": 1, + }, "name": { "description": "QEMU VM instance name", "type": "string", @@ -35,13 +40,72 @@ QEMU_CREATE_SCHEMA = { "description": "console TCP port", "minimum": 1, "maximum": 65535, - "type": "integer" + "type": ["integer", "null"] }, "monitor": { "description": "monitor TCP port", "minimum": 1, "maximum": 65535, - "type": "integer" + "type": ["integer", "null"] + }, + "hda_disk_image": { + "description": "QEMU hda disk image path", + "type": ["string", "null"], + }, + "hdb_disk_image": { + "description": "QEMU hdb disk image path", + "type": ["string", "null"], + }, + "ram": { + "description": "amount of RAM in MB", + "type": ["integer", "null"] + }, + "adapters": { + "description": "number of adapters", + "type": ["integer", "null"], + "minimum": 0, + "maximum": 32, + }, + "adapter_type": { + "description": "QEMU adapter type", + "type": ["string", "null"], + "minLength": 1, + }, + "initrd": { + "description": "QEMU initrd path", + "type": ["string", "null"], + }, + "kernel_image": { + "description": "QEMU kernel image path", + "type": ["string", "null"], + }, + "kernel_command_line": { + "description": "QEMU kernel command line", + "type": ["string", "null"], + }, + "legacy_networking": { + "description": "Use QEMU legagy networking commands (-net syntax)", + "type": ["boolean", "null"], + }, + "cpu_throttling": { + "description": "Percentage of CPU allowed for QEMU", + "minimum": 0, + "maximum": 800, + "type": ["integer", "null"], + }, + "process_priority": { + "description": "Process priority for QEMU", + "enum": ["realtime", + "very high", + "high", + "normal", + "low", + "very low", + "null"] + }, + "options": { + "description": "Additional QEMU options", + "type": ["string", "null"], }, }, "additionalProperties": False, @@ -55,74 +119,70 @@ QEMU_UPDATE_SCHEMA = { "properties": { "name": { "description": "QEMU VM instance name", - "type": "string", + "type": ["string", "null"], "minLength": 1, }, "qemu_path": { - "description": "path to QEMU", - "type": "string", + "description": "Path to QEMU", + "type": ["string", "null"], "minLength": 1, }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": ["integer", "null"] + }, + "monitor": { + "description": "monitor TCP port", + "minimum": 1, + "maximum": 65535, + "type": ["integer", "null"] + }, "hda_disk_image": { "description": "QEMU hda disk image path", - "type": "string", + "type": ["string", "null"], }, "hdb_disk_image": { "description": "QEMU hdb disk image path", - "type": "string", + "type": ["string", "null"], }, "ram": { "description": "amount of RAM in MB", - "type": "integer" + "type": ["integer", "null"] }, "adapters": { "description": "number of adapters", - "type": "integer", + "type": ["integer", "null"], "minimum": 0, "maximum": 32, }, "adapter_type": { "description": "QEMU adapter type", - "type": "string", + "type": ["string", "null"], "minLength": 1, }, - "console": { - "description": "console TCP port", - "minimum": 1, - "maximum": 65535, - "type": "integer" - }, - "monitor": { - "description": "monitor TCP port", - "minimum": 1, - "maximum": 65535, - "type": "integer" - }, "initrd": { "description": "QEMU initrd path", - "type": "string", + "type": ["string", "null"], }, "kernel_image": { "description": "QEMU kernel image path", - "type": "string", + "type": ["string", "null"], }, "kernel_command_line": { "description": "QEMU kernel command line", - "type": "string", - }, - "cloud_path": { - "description": "Path to the image in the cloud object store", - "type": "string", + "type": ["string", "null"], }, "legacy_networking": { "description": "Use QEMU legagy networking commands (-net syntax)", - "type": "boolean", + "type": ["boolean", "null"], }, "cpu_throttling": { "description": "Percentage of CPU allowed for QEMU", "minimum": 0, "maximum": 800, - "type": "integer", + "type": ["integer", "null"], }, "process_priority": { "description": "Process priority for QEMU", @@ -131,30 +191,17 @@ QEMU_UPDATE_SCHEMA = { "high", "normal", "low", - "very low"] + "very low", + "null"] }, "options": { "description": "Additional QEMU options", - "type": "string", + "type": ["string", "null"], }, }, "additionalProperties": False, } -QEMU_START_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to start a QEMU VM instance", - "type": "object", - "properties": { - "id": { - "description": "QEMU VM instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - QEMU_NIO_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", "description": "Request validation to add a NIO for a VPCS instance", @@ -202,26 +249,10 @@ QEMU_NIO_SCHEMA = { "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 - }, }, "oneOf": [ {"$ref": "#/definitions/UDP"}, {"$ref": "#/definitions/Ethernet"}, - {"$ref": "#/definitions/TAP"}, ], "additionalProperties": True, "required": ["type"] @@ -299,10 +330,6 @@ QEMU_OBJECT_SCHEMA = { "description": "QEMU kernel command line", "type": "string", }, - "cloud_path": { - "description": "Path to the image in the cloud object store", - "type": "string", - }, "legacy_networking": { "description": "Use QEMU legagy networking commands (-net syntax)", "type": "boolean", @@ -328,5 +355,9 @@ QEMU_OBJECT_SCHEMA = { }, }, "additionalProperties": False, - "required": ["vm_id"] + "required": ["vm_id", "project_id", "name", "qemu_path", "hda_disk_image", + "hdb_disk_image", "ram", "adapters", "adapter_type", "console", + "monitor", "initrd", "kernel_image", "kernel_command_line", + "legacy_networking", "cpu_throttling", "process_priority", "options" + ] } diff --git a/tests/api/test_qemu.py b/tests/api/test_qemu.py index 25cc9f9b..9e62fd73 100644 --- a/tests/api/test_qemu.py +++ b/tests/api/test_qemu.py @@ -55,12 +55,16 @@ def test_qemu_create(server, project, base_params): def test_qemu_create_with_params(server, project, base_params): params = base_params + params["ram"] = 1024 + params["hda_disk_image"] = "hda" response = server.post("/projects/{project_id}/qemu/vms".format(project_id=project.id), params, example=True) assert response.status == 201 assert response.route == "/projects/{project_id}/qemu/vms" assert response.json["name"] == "PC TEST 1" assert response.json["project_id"] == project.id + assert response.json["ram"] == 1024 + assert response.json["hda_disk_image"] == "hda" def test_qemu_get(server, project, vm): @@ -110,11 +114,15 @@ def test_qemu_update(server, vm, tmpdir, free_console_port, project): params = { "name": "test", "console": free_console_port, + "ram": 1024, + "hdb_disk_image": "hdb" } response = server.put("/projects/{project_id}/qemu/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), params) assert response.status == 200 assert response.json["name"] == "test" assert response.json["console"] == free_console_port + assert response.json["hdb_disk_image"] == "hdb" + assert response.json["ram"] == 1024 def test_qemu_nio_create_udp(server, vm): @@ -139,26 +147,6 @@ def test_qemu_nio_create_ethernet(server, vm): assert response.json["ethernet_device"] == "eth0" -def test_qemu_nio_create_ethernet_different_port(server, vm): - response = server.post("/projects/{project_id}/qemu/vms/{vm_id}/adapters/0/ports/3/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_generic_ethernet", - "ethernet_device": "eth0", - }, - example=False) - assert response.status == 201 - assert response.route == "/projects/{project_id}/qemu/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" - assert response.json["type"] == "nio_generic_ethernet" - assert response.json["ethernet_device"] == "eth0" - - -def test_qemu_nio_create_tap(server, vm): - with patch("gns3server.modules.base_manager.BaseManager._has_privileged_access", return_value=True): - response = server.post("/projects/{project_id}/qemu/vms/{vm_id}/adapters/1/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_tap", - "tap_device": "test"}) - assert response.status == 201 - assert response.route == "/projects/{project_id}/qemu/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" - assert response.json["type"] == "nio_tap" - - def test_qemu_delete_nio(server, vm): server.post("/projects/{project_id}/qemu/vms/{vm_id}/adapters/1/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", "lport": 4242, diff --git a/tests/modules/qemu/test_qemu_vm.py b/tests/modules/qemu/test_qemu_vm.py index 63b89572..1a59acca 100644 --- a/tests/modules/qemu/test_qemu_vm.py +++ b/tests/modules/qemu/test_qemu_vm.py @@ -196,3 +196,10 @@ def test_set_process_priority(vm, loop, fake_qemu_img_binary): assert process.called args, kwargs = process.call_args assert args == ("renice", "-n", "5", "-p", "42") + + +def test_json(vm, project): + + json = vm.__json__() + assert json["name"] == vm.name + assert json["project_id"] == project.id From 3797e27de54e1199752e2ce45e017083cf17c219 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 19 Feb 2015 20:23:27 +0100 Subject: [PATCH 277/485] Update documentation --- .../v1projectsprojectiddynamipsdevicesdeviceid.rst | 6 +++--- ...rojectiddynamipsdevicesdeviceidportsportnumberdnio.rst | 4 ++-- ...ynamipsdevicesdeviceidportsportnumberdstartcapture.rst | 2 +- ...dynamipsdevicesdeviceidportsportnumberdstopcapture.rst | 2 +- .../dynamips_vm/v1projectsprojectiddynamipsvmsvmid.rst | 6 +++--- ...psvmsvmidadaptersadapternumberdportsportnumberdnio.rst | 8 ++++---- ...adaptersadapternumberdportsportnumberdstartcapture.rst | 4 ++-- ...dadaptersadapternumberdportsportnumberdstopcapture.rst | 4 ++-- .../v1projectsprojectiddynamipsvmsvmidreload.rst | 2 +- .../v1projectsprojectiddynamipsvmsvmidresume.rst | 2 +- .../v1projectsprojectiddynamipsvmsvmidstart.rst | 2 +- .../v1projectsprojectiddynamipsvmsvmidstop.rst | 2 +- .../v1projectsprojectiddynamipsvmsvmidsuspend.rst | 2 +- docs/api/iou/v1projectsprojectidiouvmsvmid.rst | 6 +++--- ...ouvmsvmidadaptersadapternumberdportsportnumberdnio.rst | 8 ++++---- ...adaptersadapternumberdportsportnumberdstartcapture.rst | 4 ++-- ...dadaptersadapternumberdportsportnumberdstopcapture.rst | 4 ++-- docs/api/iou/v1projectsprojectidiouvmsvmidreload.rst | 2 +- docs/api/iou/v1projectsprojectidiouvmsvmidstart.rst | 2 +- docs/api/iou/v1projectsprojectidiouvmsvmidstop.rst | 2 +- .../virtualbox/v1projectsprojectidvirtualboxvmsvmid.rst | 6 +++--- ...oxvmsvmidadaptersadapternumberdportsportnumberdnio.rst | 8 ++++---- ...adaptersadapternumberdportsportnumberdstartcapture.rst | 4 ++-- ...dadaptersadapternumberdportsportnumberdstopcapture.rst | 4 ++-- .../v1projectsprojectidvirtualboxvmsvmidreload.rst | 2 +- .../v1projectsprojectidvirtualboxvmsvmidresume.rst | 2 +- .../v1projectsprojectidvirtualboxvmsvmidstart.rst | 2 +- .../v1projectsprojectidvirtualboxvmsvmidstop.rst | 2 +- .../v1projectsprojectidvirtualboxvmsvmidsuspend.rst | 2 +- docs/api/vpcs/v1projectsprojectidvpcsvmsvmid.rst | 6 +++--- ...csvmsvmidadaptersadapternumberdportsportnumberdnio.rst | 8 ++++---- docs/api/vpcs/v1projectsprojectidvpcsvmsvmidreload.rst | 2 +- docs/api/vpcs/v1projectsprojectidvpcsvmsvmidstart.rst | 2 +- docs/api/vpcs/v1projectsprojectidvpcsvmsvmidstop.rst | 2 +- 34 files changed, 63 insertions(+), 63 deletions(-) diff --git a/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceid.rst b/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceid.rst index 1ff726ba..7f0e65e4 100644 --- a/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceid.rst +++ b/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceid.rst @@ -9,8 +9,8 @@ Get a Dynamips device instance Parameters ********** -- **device_id**: UUID for the instance - **project_id**: UUID for the project +- **device_id**: UUID for the instance Response status codes ********************** @@ -38,8 +38,8 @@ Update a Dynamips device instance Parameters ********** -- **device_id**: UUID for the instance - **project_id**: UUID for the project +- **device_id**: UUID for the instance Response status codes ********************** @@ -95,8 +95,8 @@ Delete a Dynamips device instance Parameters ********** -- **device_id**: UUID for the instance - **project_id**: UUID for the project +- **device_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst b/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst index bb237012..0533ad08 100644 --- a/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst +++ b/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst @@ -9,9 +9,9 @@ Add a NIO to a Dynamips device instance Parameters ********** +- **project_id**: UUID for the project - **port_number**: Port on the device - **device_id**: UUID for the instance -- **project_id**: UUID for the project Response status codes ********************** @@ -128,9 +128,9 @@ Remove a NIO from a Dynamips device instance Parameters ********** +- **project_id**: UUID for the project - **port_number**: Port on the device - **device_id**: UUID for the instance -- **project_id**: UUID for the project Response status codes ********************** diff --git a/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst b/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst index 41cc168f..117cd928 100644 --- a/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst +++ b/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst @@ -9,9 +9,9 @@ Start a packet capture on a Dynamips device instance Parameters ********** +- **project_id**: UUID for the project - **port_number**: Port on the device - **device_id**: UUID for the instance -- **project_id**: UUID for the project Response status codes ********************** diff --git a/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst b/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst index 9318bdd1..9674ef65 100644 --- a/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst +++ b/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst @@ -9,9 +9,9 @@ Stop a packet capture on a Dynamips device instance Parameters ********** +- **project_id**: UUID for the project - **port_number**: Port on the device - **device_id**: UUID for the instance -- **project_id**: UUID for the project Response status codes ********************** diff --git a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmid.rst b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmid.rst index 6eb63363..191d71a0 100644 --- a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmid.rst +++ b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmid.rst @@ -9,8 +9,8 @@ Get a Dynamips VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** @@ -75,8 +75,8 @@ Update a Dynamips VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** @@ -189,8 +189,8 @@ Delete a Dynamips VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst index 4db67440..25d0a246 100644 --- a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -9,10 +9,10 @@ Add a NIO to a Dynamips VM instance Parameters ********** +- **project_id**: UUID for the project - **vm_id**: UUID for the instance -- **port_number**: Port on the adapter - **adapter_number**: Adapter where the nio should be added -- **project_id**: UUID for the project +- **port_number**: Port on the adapter Response status codes ********************** @@ -27,10 +27,10 @@ Remove a NIO from a Dynamips VM instance Parameters ********** +- **project_id**: UUID for the project - **vm_id**: UUID for the instance -- **port_number**: Port on the adapter - **adapter_number**: Adapter from where the nio should be removed -- **project_id**: UUID for the project +- **port_number**: Port on the adapter Response status codes ********************** diff --git a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst index a00b56bc..0d54a8c6 100644 --- a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst +++ b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst @@ -9,10 +9,10 @@ Start a packet capture on a Dynamips VM instance Parameters ********** +- **project_id**: UUID for the project - **vm_id**: UUID for the instance -- **port_number**: Port on the adapter - **adapter_number**: Adapter to start a packet capture -- **project_id**: UUID for the project +- **port_number**: Port on the adapter Response status codes ********************** diff --git a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst index 12efdbdb..f89a083c 100644 --- a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst +++ b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst @@ -9,10 +9,10 @@ Stop a packet capture on a Dynamips VM instance Parameters ********** +- **project_id**: UUID for the project - **vm_id**: UUID for the instance -- **port_number**: Port on the adapter (always 0) - **adapter_number**: Adapter to stop a packet capture -- **project_id**: UUID for the project +- **port_number**: Port on the adapter (always 0) Response status codes ********************** diff --git a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidreload.rst b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidreload.rst index d7d7df55..23bb67f3 100644 --- a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidreload.rst +++ b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidreload.rst @@ -9,8 +9,8 @@ Reload a Dynamips VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidresume.rst b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidresume.rst index fcd48ab5..73ce9d01 100644 --- a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidresume.rst +++ b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidresume.rst @@ -9,8 +9,8 @@ Resume a suspended Dynamips VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidstart.rst b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidstart.rst index 2dbd25b8..24c3d2af 100644 --- a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidstart.rst +++ b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidstart.rst @@ -9,8 +9,8 @@ Start a Dynamips VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidstop.rst b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidstop.rst index ff62c2c9..fca471b6 100644 --- a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidstop.rst +++ b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidstop.rst @@ -9,8 +9,8 @@ Stop a Dynamips VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidsuspend.rst b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidsuspend.rst index b6fb8a13..54394e01 100644 --- a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidsuspend.rst +++ b/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidsuspend.rst @@ -9,8 +9,8 @@ Suspend a Dynamips VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/iou/v1projectsprojectidiouvmsvmid.rst b/docs/api/iou/v1projectsprojectidiouvmsvmid.rst index a3ea1cbe..4ba73e3a 100644 --- a/docs/api/iou/v1projectsprojectidiouvmsvmid.rst +++ b/docs/api/iou/v1projectsprojectidiouvmsvmid.rst @@ -9,8 +9,8 @@ Get a IOU instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** @@ -44,8 +44,8 @@ Update a IOU instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** @@ -97,8 +97,8 @@ Delete a IOU instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst index 59088dce..890f73bc 100644 --- a/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -9,10 +9,10 @@ Add a NIO to a IOU instance Parameters ********** +- **project_id**: UUID for the project - **vm_id**: UUID for the instance -- **port_number**: Port where the nio should be added - **adapter_number**: Network adapter where the nio is located -- **project_id**: UUID for the project +- **port_number**: Port where the nio should be added Response status codes ********************** @@ -27,10 +27,10 @@ Remove a NIO from a IOU instance Parameters ********** +- **project_id**: UUID for the project - **vm_id**: UUID for the instance -- **port_number**: Port from where the nio should be removed - **adapter_number**: Network adapter where the nio is located -- **project_id**: UUID for the project +- **port_number**: Port from where the nio should be removed Response status codes ********************** diff --git a/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst b/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst index ff553a9c..11c808bf 100644 --- a/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst +++ b/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst @@ -9,10 +9,10 @@ Start a packet capture on a IOU VM instance Parameters ********** +- **project_id**: UUID for the project - **vm_id**: UUID for the instance -- **port_number**: Port on the adapter - **adapter_number**: Adapter to start a packet capture -- **project_id**: UUID for the project +- **port_number**: Port on the adapter Response status codes ********************** diff --git a/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst b/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst index dee6e612..51a784d6 100644 --- a/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst +++ b/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst @@ -9,10 +9,10 @@ Stop a packet capture on a IOU VM instance Parameters ********** +- **project_id**: UUID for the project - **vm_id**: UUID for the instance -- **port_number**: Port on the adapter (always 0) - **adapter_number**: Adapter to stop a packet capture -- **project_id**: UUID for the project +- **port_number**: Port on the adapter (always 0) Response status codes ********************** diff --git a/docs/api/iou/v1projectsprojectidiouvmsvmidreload.rst b/docs/api/iou/v1projectsprojectidiouvmsvmidreload.rst index 90f3c56b..ffdf82b3 100644 --- a/docs/api/iou/v1projectsprojectidiouvmsvmidreload.rst +++ b/docs/api/iou/v1projectsprojectidiouvmsvmidreload.rst @@ -9,8 +9,8 @@ Reload a IOU instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/iou/v1projectsprojectidiouvmsvmidstart.rst b/docs/api/iou/v1projectsprojectidiouvmsvmidstart.rst index c781dfc3..7ef3f580 100644 --- a/docs/api/iou/v1projectsprojectidiouvmsvmidstart.rst +++ b/docs/api/iou/v1projectsprojectidiouvmsvmidstart.rst @@ -9,8 +9,8 @@ Start a IOU instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/iou/v1projectsprojectidiouvmsvmidstop.rst b/docs/api/iou/v1projectsprojectidiouvmsvmidstop.rst index 4f60ad43..dd2e81f5 100644 --- a/docs/api/iou/v1projectsprojectidiouvmsvmidstop.rst +++ b/docs/api/iou/v1projectsprojectidiouvmsvmidstop.rst @@ -9,8 +9,8 @@ Stop a IOU instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmid.rst b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmid.rst index ffb8670a..b1cf371d 100644 --- a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmid.rst +++ b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmid.rst @@ -9,8 +9,8 @@ Get a VirtualBox VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** @@ -43,8 +43,8 @@ Update a VirtualBox VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** @@ -94,8 +94,8 @@ Delete a VirtualBox VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst index 9d22751c..7150561f 100644 --- a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -9,10 +9,10 @@ Add a NIO to a VirtualBox VM instance Parameters ********** +- **project_id**: UUID for the project - **vm_id**: UUID for the instance -- **port_number**: Port on the adapter (always 0) - **adapter_number**: Adapter where the nio should be added -- **project_id**: UUID for the project +- **port_number**: Port on the adapter (always 0) Response status codes ********************** @@ -27,10 +27,10 @@ Remove a NIO from a VirtualBox VM instance Parameters ********** +- **project_id**: UUID for the project - **vm_id**: UUID for the instance -- **port_number**: Port on the adapter (always) - **adapter_number**: Adapter from where the nio should be removed -- **project_id**: UUID for the project +- **port_number**: Port on the adapter (always) Response status codes ********************** diff --git a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst index 67bba0ab..402ccc5e 100644 --- a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst +++ b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst @@ -9,10 +9,10 @@ Start a packet capture on a VirtualBox VM instance Parameters ********** +- **project_id**: UUID for the project - **vm_id**: UUID for the instance -- **port_number**: Port on the adapter (always 0) - **adapter_number**: Adapter to start a packet capture -- **project_id**: UUID for the project +- **port_number**: Port on the adapter (always 0) Response status codes ********************** diff --git a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst index ea6fea53..63e1f22d 100644 --- a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst +++ b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst @@ -9,10 +9,10 @@ Stop a packet capture on a VirtualBox VM instance Parameters ********** +- **project_id**: UUID for the project - **vm_id**: UUID for the instance -- **port_number**: Port on the adapter (always 0) - **adapter_number**: Adapter to stop a packet capture -- **project_id**: UUID for the project +- **port_number**: Port on the adapter (always 0) Response status codes ********************** diff --git a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidreload.rst b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidreload.rst index 6001b143..66771462 100644 --- a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidreload.rst +++ b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidreload.rst @@ -9,8 +9,8 @@ Reload a VirtualBox VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidresume.rst b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidresume.rst index cfdc6edc..58e70528 100644 --- a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidresume.rst +++ b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidresume.rst @@ -9,8 +9,8 @@ Resume a suspended VirtualBox VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidstart.rst b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidstart.rst index 695c2712..bd324d65 100644 --- a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidstart.rst +++ b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidstart.rst @@ -9,8 +9,8 @@ Start a VirtualBox VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidstop.rst b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidstop.rst index 838a33c6..7d4ef0ee 100644 --- a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidstop.rst +++ b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidstop.rst @@ -9,8 +9,8 @@ Stop a VirtualBox VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidsuspend.rst b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidsuspend.rst index a978baa5..6f84582e 100644 --- a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidsuspend.rst +++ b/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidsuspend.rst @@ -9,8 +9,8 @@ Suspend a VirtualBox VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/vpcs/v1projectsprojectidvpcsvmsvmid.rst b/docs/api/vpcs/v1projectsprojectidvpcsvmsvmid.rst index f49c5964..a7867e67 100644 --- a/docs/api/vpcs/v1projectsprojectidvpcsvmsvmid.rst +++ b/docs/api/vpcs/v1projectsprojectidvpcsvmsvmid.rst @@ -9,8 +9,8 @@ Get a VPCS instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** @@ -38,8 +38,8 @@ Update a VPCS instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** @@ -79,8 +79,8 @@ Delete a VPCS instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst index 8be4efb3..4b84af47 100644 --- a/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -9,10 +9,10 @@ Add a NIO to a VPCS instance Parameters ********** +- **project_id**: UUID for the project - **vm_id**: UUID for the instance -- **port_number**: Port where the nio should be added - **adapter_number**: Network adapter where the nio is located -- **project_id**: UUID for the project +- **port_number**: Port where the nio should be added Response status codes ********************** @@ -27,10 +27,10 @@ Remove a NIO from a VPCS instance Parameters ********** +- **project_id**: UUID for the project - **vm_id**: UUID for the instance -- **port_number**: Port from where the nio should be removed - **adapter_number**: Network adapter where the nio is located -- **project_id**: UUID for the project +- **port_number**: Port from where the nio should be removed Response status codes ********************** diff --git a/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidreload.rst b/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidreload.rst index 4b5e197e..4736a952 100644 --- a/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidreload.rst +++ b/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidreload.rst @@ -9,8 +9,8 @@ Reload a VPCS instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidstart.rst b/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidstart.rst index c8e7a550..285b711b 100644 --- a/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidstart.rst +++ b/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidstart.rst @@ -9,8 +9,8 @@ Start a VPCS instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidstop.rst b/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidstop.rst index bf3b7fdb..9f65fe19 100644 --- a/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidstop.rst +++ b/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidstop.rst @@ -9,8 +9,8 @@ Stop a VPCS instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** From 8d02f464c53b7a1bbf35d9011b4fe2cad7450fd4 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 19 Feb 2015 16:04:15 -0700 Subject: [PATCH 278/485] Dynamips import/export configs. --- gns3server/handlers/dynamips_vm_handler.py | 28 ++++++++++++++++++---- gns3server/modules/dynamips/__init__.py | 3 +++ gns3server/modules/iou/iou_vm.py | 3 +-- gns3server/schemas/dynamips_vm.py | 28 ++++++++++++++++++++++ 4 files changed, 56 insertions(+), 6 deletions(-) diff --git a/gns3server/handlers/dynamips_vm_handler.py b/gns3server/handlers/dynamips_vm_handler.py index f88c9297..4b284a0c 100644 --- a/gns3server/handlers/dynamips_vm_handler.py +++ b/gns3server/handlers/dynamips_vm_handler.py @@ -17,13 +17,14 @@ import os -import asyncio +import base64 from ..web.route import Route from ..schemas.dynamips_vm import VM_CREATE_SCHEMA from ..schemas.dynamips_vm import VM_UPDATE_SCHEMA from ..schemas.dynamips_vm import VM_CAPTURE_SCHEMA from ..schemas.dynamips_vm import VM_OBJECT_SCHEMA from ..schemas.dynamips_vm import VM_NIO_SCHEMA +from ..schemas.dynamips_vm import VM_CONFIGS_SCHEMA from ..modules.dynamips import Dynamips from ..modules.project_manager import ProjectManager @@ -59,9 +60,6 @@ class DynamipsVMHandler: yield from dynamips_manager.update_vm_settings(vm, request.json) yield from dynamips_manager.ghost_ios_support(vm) - yield from dynamips_manager.create_vm_configs(vm, - request.json.get("startup_config_content"), - request.json.get("private_config_content")) response.set_status(201) response.json(vm) @@ -329,3 +327,25 @@ class DynamipsVMHandler: yield from vm.stop_capture(slot_number, port_number) response.set_status(204) + @Route.get( + r"/projects/{project_id}/dynamips/vms/{vm_id}/configs", + status_codes={ + 200: "Configs retrieved", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + output=VM_CONFIGS_SCHEMA, + description="Retrieve the startup and private configs content") + def show_initial_config(request, response): + + dynamips_manager = Dynamips.instance() + vm = dynamips_manager.get_vm(request.match_info["vm_id"], + project_id=request.match_info["project_id"]) + + startup_config, private_config = yield from vm.extract_config() + startup_config_content = base64.decodebytes(startup_config.encode("utf-8")).decode("utf-8") + private_config_content = base64.decodebytes(private_config.encode("utf-8")).decode("utf-8") + + response.set_status(200) + response.json({"startup_config_content": startup_config_content, + "private_config_content": private_config_content}) diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index 03eb326c..fc0eb7d8 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -480,6 +480,9 @@ class Dynamips(BaseManager): if vm.slots[0].wics and vm.slots[0].wics[wic_slot_id]: yield from vm.uninstall_wic(wic_slot_id) + # update the configs if needed + yield from self.create_vm_configs(vm, settings.get("startup_config_content"), settings.get("private_config_content")) + @asyncio.coroutine def create_vm_configs(self, vm, startup_config_content, private_config_content): """ diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 8fef72a9..ab0d064d 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -21,7 +21,6 @@ order to run an IOU instance. """ import os -import sys import signal import re import asyncio @@ -32,12 +31,12 @@ import threading import configparser import glob -from pkg_resources import parse_version from .iou_error import IOUError from ..adapters.ethernet_adapter import EthernetAdapter from ..adapters.serial_adapter import SerialAdapter from ..nios.nio_udp import NIO_UDP from ..nios.nio_tap import NIO_TAP +from ..nios.nio_generic_ethernet import NIO_GenericEthernet from ..base_vm import BaseVM from .ioucon import start_ioucon import gns3server.utils.asyncio diff --git a/gns3server/schemas/dynamips_vm.py b/gns3server/schemas/dynamips_vm.py index 0d767dd9..304bcc65 100644 --- a/gns3server/schemas/dynamips_vm.py +++ b/gns3server/schemas/dynamips_vm.py @@ -295,11 +295,19 @@ VM_UPDATE_SCHEMA = { "type": "string", "minLength": 1, }, + "startup_config_content": { + "description": "Content of IOS startup configuration file", + "type": "string", + }, "private_config": { "description": "path to the IOS private configuration file", "type": "string", "minLength": 1, }, + "private_config_content": { + "description": "Content of IOS private configuration file", + "type": "string", + }, "ram": { "description": "amount of RAM in MB", "type": "integer" @@ -888,3 +896,23 @@ VM_OBJECT_SCHEMA = { "additionalProperties": False, "required": ["name", "vm_id", "project_id", "dynamips_id"] } + +VM_CONFIGS_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to get the startup and private configuration file", + "type": "object", + "properties": { + "startup_config_content": { + "description": "Content of the startup configuration file", + "type": ["string", "null"], + "minLength": 1, + }, + "private_config_content": { + "description": "Content of the private configuration file", + "type": ["string", "null"], + "minLength": 1, + }, + }, + "additionalProperties": False, + "required": ["startup_config_content", "private_config_content"] +} From b393948b67014075dbc9ef5033a5460c75f2bda8 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 19 Feb 2015 16:58:44 -0700 Subject: [PATCH 279/485] Fixes iouyap shutdown. --- gns3server/modules/iou/iou_vm.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index ab0d064d..7751cd3e 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -474,7 +474,6 @@ class IOUVM(BaseVM): self._iou_process.kill() if self._iou_process.returncode is None: log.warn("IOU process {} is still running".format(self._iou_process.pid)) - self._iou_process = None if self._iouyap_process is not None: @@ -482,9 +481,10 @@ class IOUVM(BaseVM): try: yield from asyncio.wait_for(self._iouyap_process.wait(), timeout=3) except asyncio.TimeoutError: - self._iou_process.kill() + self._iouyap_process.kill() if self._iouyap_process.returncode is None: - log.warn("IOUYAP process {} is still running".format(self._iou_process.pid)) + log.warn("IOUYAP process {} is still running".format(self._iouyap_process.pid)) + self._iouyap_process = None self._started = False From 90f71e7581c90c5549268d5ac6e0301161214960 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 19 Feb 2015 19:14:30 -0700 Subject: [PATCH 280/485] Idle-PC proposals for Dynamips. --- gns3server/handlers/dynamips_vm_handler.py | 21 +++++++++++++++++++++ gns3server/modules/dynamips/__init__.py | 13 ++++++++----- gns3server/modules/dynamips/nodes/router.py | 6 +++--- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/gns3server/handlers/dynamips_vm_handler.py b/gns3server/handlers/dynamips_vm_handler.py index 4b284a0c..bd3c4d4c 100644 --- a/gns3server/handlers/dynamips_vm_handler.py +++ b/gns3server/handlers/dynamips_vm_handler.py @@ -349,3 +349,24 @@ class DynamipsVMHandler: response.set_status(200) response.json({"startup_config_content": startup_config_content, "private_config_content": private_config_content}) + + @Route.get( + r"/projects/{project_id}/dynamips/vms/{vm_id}/idlepc_proposals", + status_codes={ + 200: "Idle-PCs retrieved", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Retrieve the idlepc proposals") + def get_idlepcs(request, response): + + dynamips_manager = Dynamips.instance() + vm = dynamips_manager.get_vm(request.match_info["vm_id"], + project_id=request.match_info["project_id"]) + + yield from vm.set_idlepc("0x0") + idlepcs = yield from vm.get_idle_pc_prop() + + #idlepcs = yield from vm.show_idle_pc_prop() + response.set_status(200) + response.json({"idlepcs": idlepcs}) diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index fc0eb7d8..c03ada14 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -409,6 +409,10 @@ class Dynamips(BaseManager): if not vm.mmap: raise DynamipsError("mmap support is required to enable ghost IOS support") + if vm.platform == "c7200" and vm.npe == "npe-g2": + log.warning("Ghost IOS is not support for c7200 with NPE-G2") + return + ghost_file = vm.formatted_ghost_file() ghost_file_path = os.path.join(vm.hypervisor.working_dir, ghost_file) if ghost_file_path not in self._ghost_files: @@ -418,9 +422,6 @@ class Dynamips(BaseManager): try: yield from ghost.create() yield from ghost.set_image(vm.image) - # for 7200s, the NPE must be set when using an NPE-G2. - if vm.platform == "c7200": - yield from ghost.set_npe(vm.npe) yield from ghost.set_ghost_status(1) yield from ghost.set_ghost_file(ghost_file) yield from ghost.set_ram(vm.ram) @@ -463,7 +464,8 @@ class Dynamips(BaseManager): adapter = ADAPTER_MATRIX[adapter_name]() if vm.slots[slot_id] and type(vm.slots[slot_id]) != type(adapter): yield from vm.slot_remove_binding(slot_id) - yield from vm.slot_add_binding(slot_id, adapter) + if type(vm.slots[slot_id]) != type(adapter): + yield from vm.slot_add_binding(slot_id, adapter) elif name.startswith("slot") and value is None: slot_id = int(name[-1]) if vm.slots[slot_id]: @@ -474,7 +476,8 @@ class Dynamips(BaseManager): wic = WIC_MATRIX[wic_name]() if vm.slots[0].wics[wic_slot_id] and type(vm.slots[0].wics[wic_slot_id]) != type(wic): yield from vm.uninstall_wic(wic_slot_id) - yield from vm.install_wic(wic_slot_id, wic) + if type(vm.slots[0].wics[wic_slot_id]) != type(wic): + yield from vm.install_wic(wic_slot_id, wic) elif name.startswith("wic") and value is None: wic_slot_id = int(name[-1]) if vm.slots[0].wics and vm.slots[0].wics[wic_slot_id]: diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index 36db166d..92f14c02 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -305,6 +305,9 @@ class Router(BaseVM): # router is already closed return + if self._dynamips_id in self._dynamips_ids[self._project.id]: + self._dynamips_ids[self._project.id].remove(self._dynamips_id) + self._hypervisor.devices.remove(self) if self._hypervisor and not self._hypervisor.devices: try: @@ -323,9 +326,6 @@ class Router(BaseVM): self._manager.port_manager.release_console_port(self._aux) self._aux = None - if self._dynamips_id in self._dynamips_ids[self._project.id]: - self._dynamips_ids[self._project.id].remove(self._dynamips_id) - self._closed = True @property From 8aa5514890fec865661fa3e530b5c4436965daa2 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 20 Feb 2015 14:39:13 +0100 Subject: [PATCH 281/485] Qemu binary list --- docs/api/qemu.rst | 8 ++ docs/api/qemu/v1projectsprojectidqemuvms.rst | 70 ++++++++++ .../qemu/v1projectsprojectidqemuvmsvmid.rst | 129 ++++++++++++++++++ ...ptersadapternumberdportsportnumberdnio.rst | 40 ++++++ .../v1projectsprojectidqemuvmsvmidreload.rst | 20 +++ .../v1projectsprojectidqemuvmsvmidstart.rst | 20 +++ .../v1projectsprojectidqemuvmsvmidstop.rst | 20 +++ .../v1projectsprojectidqemuvmsvmidsuspend.rst | 20 +++ gns3server/handlers/qemu_handler.py | 20 +++ gns3server/modules/qemu/__init__.py | 71 +++++++++- gns3server/schemas/qemu.py | 25 ++++ tests/api/test_qemu.py | 10 ++ tests/modules/qemu/test_qemu_manager.py | 53 +++++++ 13 files changed, 505 insertions(+), 1 deletion(-) create mode 100644 docs/api/qemu.rst create mode 100644 docs/api/qemu/v1projectsprojectidqemuvms.rst create mode 100644 docs/api/qemu/v1projectsprojectidqemuvmsvmid.rst create mode 100644 docs/api/qemu/v1projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst create mode 100644 docs/api/qemu/v1projectsprojectidqemuvmsvmidreload.rst create mode 100644 docs/api/qemu/v1projectsprojectidqemuvmsvmidstart.rst create mode 100644 docs/api/qemu/v1projectsprojectidqemuvmsvmidstop.rst create mode 100644 docs/api/qemu/v1projectsprojectidqemuvmsvmidsuspend.rst create mode 100644 tests/modules/qemu/test_qemu_manager.py diff --git a/docs/api/qemu.rst b/docs/api/qemu.rst new file mode 100644 index 00000000..70fd8fc2 --- /dev/null +++ b/docs/api/qemu.rst @@ -0,0 +1,8 @@ +Qemu +--------------------- + +.. toctree:: + :glob: + :maxdepth: 2 + + qemu/* diff --git a/docs/api/qemu/v1projectsprojectidqemuvms.rst b/docs/api/qemu/v1projectsprojectidqemuvms.rst new file mode 100644 index 00000000..4a88a237 --- /dev/null +++ b/docs/api/qemu/v1projectsprojectidqemuvms.rst @@ -0,0 +1,70 @@ +/v1/projects/{project_id}/qemu/vms +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/qemu/vms +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Create a new Qemu.instance + +Parameters +********** +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **201**: Instance created +- **409**: Conflict + +Input +******* +.. raw:: html + + + + + + + + + + + + + + + + + + + + +
Name Mandatory Type Description
adapter_type ['string', 'null'] QEMU adapter type
adapters ['integer', 'null'] number of adapters
console ['integer', 'null'] console TCP port
cpu_throttling ['integer', 'null'] Percentage of CPU allowed for QEMU
hda_disk_image ['string', 'null'] QEMU hda disk image path
hdb_disk_image ['string', 'null'] QEMU hdb disk image path
initrd ['string', 'null'] QEMU initrd path
kernel_command_line ['string', 'null'] QEMU kernel command line
kernel_image ['string', 'null'] QEMU kernel image path
legacy_networking ['boolean', 'null'] Use QEMU legagy networking commands (-net syntax)
monitor ['integer', 'null'] monitor TCP port
name string QEMU VM instance name
options ['string', 'null'] Additional QEMU options
process_priority enum Possible values: realtime, very high, high, normal, low, very low, null
qemu_path string Path to QEMU
ram ['integer', 'null'] amount of RAM in MB
vm_id ['string', 'null'] QEMU VM UUID
+ +Output +******* +.. raw:: html + + + + + + + + + + + + + + + + + + + + + +
Name Mandatory Type Description
adapter_type string QEMU adapter type
adapters integer number of adapters
console integer console TCP port
cpu_throttling integer Percentage of CPU allowed for QEMU
hda_disk_image string QEMU hda disk image path
hdb_disk_image string QEMU hdb disk image path
initrd string QEMU initrd path
kernel_command_line string QEMU kernel command line
kernel_image string QEMU kernel image path
legacy_networking boolean Use QEMU legagy networking commands (-net syntax)
monitor integer monitor TCP port
name string QEMU VM instance name
options string Additional QEMU options
process_priority enum Possible values: realtime, very high, high, normal, low, very low
project_id string Project uuid
qemu_path string path to QEMU
ram integer amount of RAM in MB
vm_id string QEMU VM uuid
+ diff --git a/docs/api/qemu/v1projectsprojectidqemuvmsvmid.rst b/docs/api/qemu/v1projectsprojectidqemuvmsvmid.rst new file mode 100644 index 00000000..e90b3cfd --- /dev/null +++ b/docs/api/qemu/v1projectsprojectidqemuvmsvmid.rst @@ -0,0 +1,129 @@ +/v1/projects/{project_id}/qemu/vms/{vm_id} +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +GET /v1/projects/**{project_id}**/qemu/vms/**{vm_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Get a Qemu.instance + +Parameters +********** +- **project_id**: UUID for the project +- **vm_id**: UUID for the instance + +Response status codes +********************** +- **200**: Success +- **400**: Invalid request +- **404**: Instance doesn't exist + +Output +******* +.. raw:: html + + + + + + + + + + + + + + + + + + + + + +
Name Mandatory Type Description
adapter_type string QEMU adapter type
adapters integer number of adapters
console integer console TCP port
cpu_throttling integer Percentage of CPU allowed for QEMU
hda_disk_image string QEMU hda disk image path
hdb_disk_image string QEMU hdb disk image path
initrd string QEMU initrd path
kernel_command_line string QEMU kernel command line
kernel_image string QEMU kernel image path
legacy_networking boolean Use QEMU legagy networking commands (-net syntax)
monitor integer monitor TCP port
name string QEMU VM instance name
options string Additional QEMU options
process_priority enum Possible values: realtime, very high, high, normal, low, very low
project_id string Project uuid
qemu_path string path to QEMU
ram integer amount of RAM in MB
vm_id string QEMU VM uuid
+ + +PUT /v1/projects/**{project_id}**/qemu/vms/**{vm_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Update a Qemu.instance + +Parameters +********** +- **project_id**: UUID for the project +- **vm_id**: UUID for the instance + +Response status codes +********************** +- **200**: Instance updated +- **400**: Invalid request +- **404**: Instance doesn't exist +- **409**: Conflict + +Input +******* +.. raw:: html + + + + + + + + + + + + + + + + + + + +
Name Mandatory Type Description
adapter_type ['string', 'null'] QEMU adapter type
adapters ['integer', 'null'] number of adapters
console ['integer', 'null'] console TCP port
cpu_throttling ['integer', 'null'] Percentage of CPU allowed for QEMU
hda_disk_image ['string', 'null'] QEMU hda disk image path
hdb_disk_image ['string', 'null'] QEMU hdb disk image path
initrd ['string', 'null'] QEMU initrd path
kernel_command_line ['string', 'null'] QEMU kernel command line
kernel_image ['string', 'null'] QEMU kernel image path
legacy_networking ['boolean', 'null'] Use QEMU legagy networking commands (-net syntax)
monitor ['integer', 'null'] monitor TCP port
name ['string', 'null'] QEMU VM instance name
options ['string', 'null'] Additional QEMU options
process_priority enum Possible values: realtime, very high, high, normal, low, very low, null
qemu_path ['string', 'null'] Path to QEMU
ram ['integer', 'null'] amount of RAM in MB
+ +Output +******* +.. raw:: html + + + + + + + + + + + + + + + + + + + + + +
Name Mandatory Type Description
adapter_type string QEMU adapter type
adapters integer number of adapters
console integer console TCP port
cpu_throttling integer Percentage of CPU allowed for QEMU
hda_disk_image string QEMU hda disk image path
hdb_disk_image string QEMU hdb disk image path
initrd string QEMU initrd path
kernel_command_line string QEMU kernel command line
kernel_image string QEMU kernel image path
legacy_networking boolean Use QEMU legagy networking commands (-net syntax)
monitor integer monitor TCP port
name string QEMU VM instance name
options string Additional QEMU options
process_priority enum Possible values: realtime, very high, high, normal, low, very low
project_id string Project uuid
qemu_path string path to QEMU
ram integer amount of RAM in MB
vm_id string QEMU VM uuid
+ + +DELETE /v1/projects/**{project_id}**/qemu/vms/**{vm_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Delete a Qemu.instance + +Parameters +********** +- **project_id**: UUID for the project +- **vm_id**: UUID for the instance + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance deleted + diff --git a/docs/api/qemu/v1projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/qemu/v1projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst new file mode 100644 index 00000000..8d0b3733 --- /dev/null +++ b/docs/api/qemu/v1projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -0,0 +1,40 @@ +/v1/projects/{project_id}/qemu/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/qemu/vms/**{vm_id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Add a NIO to a Qemu.instance + +Parameters +********** +- **project_id**: UUID for the project +- **vm_id**: UUID for the instance +- **adapter_number**: Network adapter where the nio is located +- **port_number**: Port where the nio should be added + +Response status codes +********************** +- **400**: Invalid request +- **201**: NIO created +- **404**: Instance doesn't exist + + +DELETE /v1/projects/**{project_id}**/qemu/vms/**{vm_id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Remove a NIO from a Qemu.instance + +Parameters +********** +- **project_id**: UUID for the project +- **vm_id**: UUID for the instance +- **adapter_number**: Network adapter where the nio is located +- **port_number**: Port from where the nio should be removed + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: NIO deleted + diff --git a/docs/api/qemu/v1projectsprojectidqemuvmsvmidreload.rst b/docs/api/qemu/v1projectsprojectidqemuvmsvmidreload.rst new file mode 100644 index 00000000..04e239f6 --- /dev/null +++ b/docs/api/qemu/v1projectsprojectidqemuvmsvmidreload.rst @@ -0,0 +1,20 @@ +/v1/projects/{project_id}/qemu/vms/{vm_id}/reload +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/qemu/vms/**{vm_id}**/reload +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Reload a Qemu.instance + +Parameters +********** +- **project_id**: UUID for the project +- **vm_id**: UUID for the instance + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance reloaded + diff --git a/docs/api/qemu/v1projectsprojectidqemuvmsvmidstart.rst b/docs/api/qemu/v1projectsprojectidqemuvmsvmidstart.rst new file mode 100644 index 00000000..d2649825 --- /dev/null +++ b/docs/api/qemu/v1projectsprojectidqemuvmsvmidstart.rst @@ -0,0 +1,20 @@ +/v1/projects/{project_id}/qemu/vms/{vm_id}/start +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/qemu/vms/**{vm_id}**/start +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Start a Qemu.instance + +Parameters +********** +- **project_id**: UUID for the project +- **vm_id**: UUID for the instance + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance started + diff --git a/docs/api/qemu/v1projectsprojectidqemuvmsvmidstop.rst b/docs/api/qemu/v1projectsprojectidqemuvmsvmidstop.rst new file mode 100644 index 00000000..be132747 --- /dev/null +++ b/docs/api/qemu/v1projectsprojectidqemuvmsvmidstop.rst @@ -0,0 +1,20 @@ +/v1/projects/{project_id}/qemu/vms/{vm_id}/stop +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/qemu/vms/**{vm_id}**/stop +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Stop a Qemu.instance + +Parameters +********** +- **project_id**: UUID for the project +- **vm_id**: UUID for the instance + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance stopped + diff --git a/docs/api/qemu/v1projectsprojectidqemuvmsvmidsuspend.rst b/docs/api/qemu/v1projectsprojectidqemuvmsvmidsuspend.rst new file mode 100644 index 00000000..c9da38a2 --- /dev/null +++ b/docs/api/qemu/v1projectsprojectidqemuvmsvmidsuspend.rst @@ -0,0 +1,20 @@ +/v1/projects/{project_id}/qemu/vms/{vm_id}/suspend +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/qemu/vms/**{vm_id}**/suspend +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Reload a Qemu.instance + +Parameters +********** +- **project_id**: UUID for the project +- **vm_id**: UUID for the instance + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance suspended + diff --git a/gns3server/handlers/qemu_handler.py b/gns3server/handlers/qemu_handler.py index 9ccbd237..11cdb928 100644 --- a/gns3server/handlers/qemu_handler.py +++ b/gns3server/handlers/qemu_handler.py @@ -24,6 +24,7 @@ from ..schemas.qemu import QEMU_CREATE_SCHEMA from ..schemas.qemu import QEMU_UPDATE_SCHEMA from ..schemas.qemu import QEMU_OBJECT_SCHEMA from ..schemas.qemu import QEMU_NIO_SCHEMA +from ..schemas.qemu import QEMU_BINARY_LIST_SCHEMA from ..modules.qemu import Qemu @@ -258,3 +259,22 @@ class QEMUHandler: vm = qemu_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) vm.adapter_remove_nio_binding(int(request.match_info["adapter_number"]), int(request.match_info["port_number"])) response.set_status(204) + + @classmethod + @Route.get( + r"/projects/{project_id}/qemu/binaries", + parameters={ + "project_id": "UUID for the project" + }, + status_codes={ + 200: "Success", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Get a list of available Qemu binaries", + output=QEMU_BINARY_LIST_SCHEMA) + def list_binaries(request, response): + + qemu_manager = Qemu.instance() + binaries = yield from Qemu.binary_list() + response.json(binaries) diff --git a/gns3server/modules/qemu/__init__.py b/gns3server/modules/qemu/__init__.py index 58c87484..69abd613 100644 --- a/gns3server/modules/qemu/__init__.py +++ b/gns3server/modules/qemu/__init__.py @@ -20,7 +20,12 @@ Qemu server module. """ import asyncio +import os +import sys +import re +import subprocess +from ...utils.asyncio import subprocess_check_output from ..base_manager import BaseManager from .qemu_error import QemuError from .qemu_vm import QemuVM @@ -38,4 +43,68 @@ class Qemu(BaseManager): :returns: working directory name """ - return "pc-{}".format(legacy_vm_id) + return "vm-{}".format(legacy_vm_id) + + @staticmethod + def binary_list(): + """ + Gets QEMU binaries list available on the matchine + + :returns: Array of dictionnary {"path": Qemu binaries path, "version": Version of Qemu} + """ + + qemus = [] + paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep) + # look for Qemu binaries in the current working directory and $PATH + if sys.platform.startswith("win"): + # add specific Windows paths + if hasattr(sys, "frozen"): + # add any qemu dir in the same location as gns3server.exe to the list of paths + exec_dir = os.path.dirname(os.path.abspath(sys.executable)) + for f in os.listdir(exec_dir): + if f.lower().startswith("qemu"): + paths.append(os.path.join(exec_dir, f)) + + if "PROGRAMFILES(X86)" in os.environ and os.path.exists(os.environ["PROGRAMFILES(X86)"]): + paths.append(os.path.join(os.environ["PROGRAMFILES(X86)"], "qemu")) + if "PROGRAMFILES" in os.environ and os.path.exists(os.environ["PROGRAMFILES"]): + paths.append(os.path.join(os.environ["PROGRAMFILES"], "qemu")) + elif sys.platform.startswith("darwin"): + # add specific locations on Mac OS X regardless of what's in $PATH + paths.extend(["/usr/local/bin", "/opt/local/bin"]) + if hasattr(sys, "frozen"): + paths.append(os.path.abspath(os.path.join(os.getcwd(), "../../../qemu/bin/"))) + for path in paths: + try: + for f in os.listdir(path): + if (f.startswith("qemu-system") or f == "qemu" or f == "qemu.exe") and \ + os.access(os.path.join(path, f), os.X_OK) and \ + os.path.isfile(os.path.join(path, f)): + qemu_path = os.path.join(path, f) + version = yield from Qemu._get_qemu_version(qemu_path) + qemus.append({"path": qemu_path, "version": version}) + except OSError: + continue + + return qemus + + @staticmethod + @asyncio.coroutine + def _get_qemu_version(qemu_path): + """ + Gets the Qemu version. + :param qemu_path: path to Qemu + """ + + if sys.platform.startswith("win"): + return "" + try: + output = yield from subprocess_check_output(qemu_path, "-version") + match = re.search("version\s+([0-9a-z\-\.]+)", output.decode("utf-8")) + if match: + version = match.group(1) + return version + else: + raise QemuError("Could not determine the Qemu version for {}".format(qemu_path)) + except subprocess.SubprocessError as e: + raise QemuError("Error while looking for the Qemu version: {}".format(e)) diff --git a/gns3server/schemas/qemu.py b/gns3server/schemas/qemu.py index 065f4a73..5c55c00d 100644 --- a/gns3server/schemas/qemu.py +++ b/gns3server/schemas/qemu.py @@ -361,3 +361,28 @@ QEMU_OBJECT_SCHEMA = { "legacy_networking", "cpu_throttling", "process_priority", "options" ] } + +QEMU_BINARY_LIST_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation for a list of qemu binaries", + "type": "array", + "items": { + "$ref": "#/definitions/QemuPath" + }, + "definitions": { + "QemuPath": { + "description": "Qemu path object", + "properties": { + "path": { + "description": "Qemu path", + "type": "string", + }, + "version": { + "description": "Qemu version", + "type": "string", + }, + }, + } + }, + "additionalProperties": False, +} diff --git a/tests/api/test_qemu.py b/tests/api/test_qemu.py index 9e62fd73..6d184b27 100644 --- a/tests/api/test_qemu.py +++ b/tests/api/test_qemu.py @@ -155,3 +155,13 @@ def test_qemu_delete_nio(server, vm): response = server.delete("/projects/{project_id}/qemu/vms/{vm_id}/adapters/1/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert response.status == 204 assert response.route == "/projects/{project_id}/qemu/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" + + +def test_qemu_list_binaries(server, vm): + ret = [{"path": "/tmp/1", "version": "2.2.0"}, + {"path": "/tmp/2", "version": "2.1.0"}] + with asyncio_patch("gns3server.modules.qemu.Qemu.binary_list", return_value=ret) as mock: + response = server.get("/projects/{project_id}/qemu/binaries".format(project_id=vm["project_id"])) + assert mock.called + assert response.status == 200 + assert response.json == ret diff --git a/tests/modules/qemu/test_qemu_manager.py b/tests/modules/qemu/test_qemu_manager.py new file mode 100644 index 00000000..9309f6da --- /dev/null +++ b/tests/modules/qemu/test_qemu_manager.py @@ -0,0 +1,53 @@ +# -*- 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 os +import stat +import asyncio + +from gns3server.modules.qemu import Qemu +from tests.utils import asyncio_patch + + +def test_get_qemu_version(loop): + + with asyncio_patch("gns3server.modules.qemu.subprocess_check_output", return_value=b"QEMU emulator version 2.2.0, Copyright (c) 2003-2008 Fabrice Bellard") as mock: + version = loop.run_until_complete(asyncio.async(Qemu._get_qemu_version("/tmp/qemu-test"))) + assert version == "2.2.0" + + +def test_binary_list(loop): + + files_to_create = ["qemu-system-x86", "qemu-system-x42", "hello"] + + for file_to_create in files_to_create: + path = os.path.join(os.environ["PATH"], file_to_create) + with open(path, "w+") as f: + f.write("1") + os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) + + with asyncio_patch("gns3server.modules.qemu.subprocess_check_output", return_value=b"QEMU emulator version 2.2.0, Copyright (c) 2003-2008 Fabrice Bellard") as mock: + qemus = loop.run_until_complete(asyncio.async(Qemu.binary_list())) + + assert {"path": os.path.join(os.environ["PATH"], "qemu-system-x86"), "version": "2.2.0"} in qemus + assert {"path": os.path.join(os.environ["PATH"], "qemu-system-x42"), "version": "2.2.0"} in qemus + assert {"path": os.path.join(os.environ["PATH"], "hello"), "version": "2.2.0"} not in qemus + + +def test_get_legacy_vm_workdir_name(): + + assert Qemu.get_legacy_vm_workdir_name(42) == "vm-42" From 15036837bb7e4ebea32e19bfcf4f8d33dbd6215c Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 20 Feb 2015 16:54:23 +0100 Subject: [PATCH 282/485] No project for qemu binaries list --- gns3server/handlers/qemu_handler.py | 5 +---- tests/api/test_qemu.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/gns3server/handlers/qemu_handler.py b/gns3server/handlers/qemu_handler.py index 11cdb928..4f8cb16c 100644 --- a/gns3server/handlers/qemu_handler.py +++ b/gns3server/handlers/qemu_handler.py @@ -262,10 +262,7 @@ class QEMUHandler: @classmethod @Route.get( - r"/projects/{project_id}/qemu/binaries", - parameters={ - "project_id": "UUID for the project" - }, + r"/qemu/binaries", status_codes={ 200: "Success", 400: "Invalid request", diff --git a/tests/api/test_qemu.py b/tests/api/test_qemu.py index 6d184b27..92501bc4 100644 --- a/tests/api/test_qemu.py +++ b/tests/api/test_qemu.py @@ -161,7 +161,7 @@ def test_qemu_list_binaries(server, vm): ret = [{"path": "/tmp/1", "version": "2.2.0"}, {"path": "/tmp/2", "version": "2.1.0"}] with asyncio_patch("gns3server.modules.qemu.Qemu.binary_list", return_value=ret) as mock: - response = server.get("/projects/{project_id}/qemu/binaries".format(project_id=vm["project_id"])) + response = server.get("/qemu/binaries".format(project_id=vm["project_id"])) assert mock.called assert response.status == 200 assert response.json == ret From 47be57dca71269da0277fb4d444aa26e550060fc Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 20 Feb 2015 17:31:02 +0100 Subject: [PATCH 283/485] Fix qemu close --- gns3server/modules/qemu/__init__.py | 2 +- gns3server/modules/qemu/qemu_vm.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gns3server/modules/qemu/__init__.py b/gns3server/modules/qemu/__init__.py index 69abd613..2d643204 100644 --- a/gns3server/modules/qemu/__init__.py +++ b/gns3server/modules/qemu/__init__.py @@ -100,7 +100,7 @@ class Qemu(BaseManager): return "" try: output = yield from subprocess_check_output(qemu_path, "-version") - match = re.search("version\s+([0-9a-z\-\.]+)", output.decode("utf-8")) + match = re.search("version\s+([0-9a-z\-\.]+)", output) if match: version = match.group(1) return version diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index 23ddf018..2464b725 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -601,7 +601,7 @@ class QemuVM(BaseVM): log.info("stopping QEMU VM instance {} PID={}".format(self._id, self._process.pid)) try: self._process.terminate() - self._process.wait(1) + self._process.wait() except subprocess.TimeoutExpired: self._process.kill() if self._process.poll() is None: From 71357fa7ab79b71dbb2c981928e7c6156272a2b0 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 20 Feb 2015 17:45:27 +0100 Subject: [PATCH 284/485] Fix tests --- tests/modules/qemu/test_qemu_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/modules/qemu/test_qemu_manager.py b/tests/modules/qemu/test_qemu_manager.py index 9309f6da..65870191 100644 --- a/tests/modules/qemu/test_qemu_manager.py +++ b/tests/modules/qemu/test_qemu_manager.py @@ -25,7 +25,7 @@ from tests.utils import asyncio_patch def test_get_qemu_version(loop): - with asyncio_patch("gns3server.modules.qemu.subprocess_check_output", return_value=b"QEMU emulator version 2.2.0, Copyright (c) 2003-2008 Fabrice Bellard") as mock: + with asyncio_patch("gns3server.modules.qemu.subprocess_check_output", return_value="QEMU emulator version 2.2.0, Copyright (c) 2003-2008 Fabrice Bellard") as mock: version = loop.run_until_complete(asyncio.async(Qemu._get_qemu_version("/tmp/qemu-test"))) assert version == "2.2.0" @@ -40,7 +40,7 @@ def test_binary_list(loop): f.write("1") os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) - with asyncio_patch("gns3server.modules.qemu.subprocess_check_output", return_value=b"QEMU emulator version 2.2.0, Copyright (c) 2003-2008 Fabrice Bellard") as mock: + with asyncio_patch("gns3server.modules.qemu.subprocess_check_output", return_value="QEMU emulator version 2.2.0, Copyright (c) 2003-2008 Fabrice Bellard") as mock: qemus = loop.run_until_complete(asyncio.async(Qemu.binary_list())) assert {"path": os.path.join(os.environ["PATH"], "qemu-system-x86"), "version": "2.2.0"} in qemus From a0f4c6d0215b1adebf7fce00c756202085afd236 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 20 Feb 2015 22:23:09 +0100 Subject: [PATCH 285/485] Repare live reload --- gns3server/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gns3server/main.py b/gns3server/main.py index f25ce6d9..1c664609 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -123,6 +123,7 @@ def set_config(args): server_config["certfile"] = args.certfile server_config["certkey"] = args.certkey server_config["debug"] = str(args.debug) + server_config["live"] = str(args.live) config.set_section_config("Server", server_config) From 45a48cfcc1eb416caf293d89f507f5952bf74b56 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 20 Feb 2015 22:40:20 +0100 Subject: [PATCH 286/485] Embeded debugging shell --- gns3server/main.py | 2 ++ gns3server/server.py | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/gns3server/main.py b/gns3server/main.py index 1c664609..889078d2 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -107,6 +107,7 @@ def parse_arguments(argv, config): parser.add_argument("-q", "--quiet", action="store_true", help="do not show logs on stdout") parser.add_argument("-d", "--debug", action="store_true", help="show debug logs") parser.add_argument("--live", action="store_true", help="enable code live reload") + parser.add_argument("--shell", action="store_true", help="start a shell inside the server (debugging purpose only you need to install ptpython before)") return parser.parse_args(argv) @@ -124,6 +125,7 @@ def set_config(args): server_config["certkey"] = args.certkey server_config["debug"] = str(args.debug) server_config["live"] = str(args.live) + server_config["shell"] = str(args.shell) config.set_section_config("Server", server_config) diff --git a/gns3server/server.py b/gns3server/server.py index baf7e1a9..1d7214f0 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -139,6 +139,11 @@ class Server: raise SystemExit return ssl_context + @asyncio.coroutine + def start_shell(self): + from ptpython.repl import embed + yield from embed(globals(), locals(), return_asyncio_coroutine=True, patch_stdout=True) + def run(self): """ Starts the server. @@ -176,4 +181,8 @@ class Server: if server_config.getboolean("live"): log.info("Code live reload is enabled, watching for file changes") self._loop.call_later(1, self._reload_hook) + + if server_config.getboolean("shell"): + asyncio.async(self.start_shell()) + self._loop.run_forever() From cecf2f501424f0c52e623fb909d980b6f5121361 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Sat, 21 Feb 2015 00:15:56 +0100 Subject: [PATCH 287/485] Async qemu monitor reading --- gns3server/handlers/qemu_handler.py | 22 ++++++++++++++++++++- gns3server/modules/qemu/qemu_vm.py | 29 ++++++++++++++-------------- gns3server/server.py | 2 ++ tests/api/test_qemu.py | 7 +++++++ tests/modules/qemu/test_qemu_vm.py | 30 +++++++++++++++++++++++++++++ 5 files changed, 74 insertions(+), 16 deletions(-) diff --git a/gns3server/handlers/qemu_handler.py b/gns3server/handlers/qemu_handler.py index 4f8cb16c..c170f7f6 100644 --- a/gns3server/handlers/qemu_handler.py +++ b/gns3server/handlers/qemu_handler.py @@ -205,7 +205,7 @@ class QEMUHandler: 400: "Invalid request", 404: "Instance doesn't exist" }, - description="Reload a Qemu.instance") + description="Suspend a Qemu.instance") def suspend(request, response): qemu_manager = Qemu.instance() @@ -213,6 +213,26 @@ class QEMUHandler: yield from vm.suspend() response.set_status(204) + @classmethod + @Route.post( + r"/projects/{project_id}/qemu/vms/{vm_id}/resume", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance", + }, + status_codes={ + 204: "Instance resumed", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Resume a Qemu.instance") + def resume(request, response): + + qemu_manager = Qemu.instance() + vm = qemu_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + yield from vm.resume() + response.set_status(204) + @Route.post( r"/projects/{project_id}/qemu/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio", parameters={ diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index 2464b725..6b491c76 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -612,13 +612,12 @@ class QemuVM(BaseVM): self._stop_cpulimit() @asyncio.coroutine - def _control_vm(self, command, expected=None, timeout=30): + def _control_vm(self, command, expected=None): """ Executes a command with QEMU monitor when this VM is running. :param command: QEMU monitor command (e.g. info status, stop etc.) :params expected: An array with the string attended (Default None) - :param timeout: how long to wait for QEMU monitor :returns: result of the command (Match object or None) """ @@ -627,25 +626,29 @@ class QemuVM(BaseVM): if self.is_running() and self._monitor: log.debug("Execute QEMU monitor command: {}".format(command)) try: - tn = telnetlib.Telnet(self._monitor_host, self._monitor, timeout=timeout) + reader, writer = yield from asyncio.open_connection("127.0.0.1", self._monitor) except OSError as e: log.warn("Could not connect to QEMU monitor: {}".format(e)) return result try: - tn.write(command.encode('ascii') + b"\n") - time.sleep(0.1) + writer.write(command.encode('ascii') + b"\n") except OSError as e: log.warn("Could not write to QEMU monitor: {}".format(e)) - tn.close() + writer.close() return result if expected: try: - ind, match, dat = tn.expect(list=expected, timeout=timeout) - if match: - result = match + while result is None: + line = yield from reader.readline() + if not line: + break + for expect in expected: + if expect in line: + result = line + break except EOFError as e: log.warn("Could not read from QEMU monitor: {}".format(e)) - tn.close() + writer.close() return result @asyncio.coroutine @@ -667,11 +670,7 @@ class QemuVM(BaseVM): :returns: status (string) """ - result = None - - match = yield from self._control_vm("info status", [b"running", b"paused"]) - if match: - result = match.group(0).decode('ascii') + result = yield from self._control_vm("info status", [b"running", b"paused"]) return result @asyncio.coroutine diff --git a/gns3server/server.py b/gns3server/server.py index 1d7214f0..81731d43 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -142,6 +142,8 @@ class Server: @asyncio.coroutine def start_shell(self): from ptpython.repl import embed + from gns3server.modules import Qemu + yield from embed(globals(), locals(), return_asyncio_coroutine=True, patch_stdout=True) def run(self): diff --git a/tests/api/test_qemu.py b/tests/api/test_qemu.py index 92501bc4..f0fe64d0 100644 --- a/tests/api/test_qemu.py +++ b/tests/api/test_qemu.py @@ -103,6 +103,13 @@ def test_qemu_suspend(server, vm): assert response.status == 204 +def test_qemu_resume(server, vm): + with asyncio_patch("gns3server.modules.qemu.qemu_vm.QemuVM.resume", return_value=True) as mock: + response = server.post("/projects/{project_id}/qemu/vms/{vm_id}/resume".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + assert mock.called + assert response.status == 204 + + def test_qemu_delete(server, vm): with asyncio_patch("gns3server.modules.qemu.Qemu.delete_vm", return_value=True) as mock: response = server.delete("/projects/{project_id}/qemu/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) diff --git a/tests/modules/qemu/test_qemu_vm.py b/tests/modules/qemu/test_qemu_vm.py index 1a59acca..a174f7fd 100644 --- a/tests/modules/qemu/test_qemu_vm.py +++ b/tests/modules/qemu/test_qemu_vm.py @@ -203,3 +203,33 @@ def test_json(vm, project): json = vm.__json__() assert json["name"] == vm.name assert json["project_id"] == project.id + + +def test_control_vm(vm, loop): + + vm._process = MagicMock() + vm._monitor = 4242 + reader = MagicMock() + writer = MagicMock() + with asyncio_patch("asyncio.open_connection", return_value=(reader, writer)) as open_connect: + res = loop.run_until_complete(asyncio.async(vm._control_vm("test"))) + assert writer.write.called_with("test") + assert res is None + + +def test_control_vm_expect_text(vm, loop): + + vm._process = MagicMock() + vm._monitor = 4242 + reader = MagicMock() + writer = MagicMock() + with asyncio_patch("asyncio.open_connection", return_value=(reader, writer)) as open_connect: + + future = asyncio.Future() + future.set_result("epic product") + reader.readline.return_value = future + + res = loop.run_until_complete(asyncio.async(vm._control_vm("test", ["epic"]))) + assert writer.write.called_with("test") + + assert res == "epic product" From af700e9bcbc580bc7b119328bde1a2a968803d86 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 20 Feb 2015 16:53:51 -0700 Subject: [PATCH 288/485] Idle-PC and auto idle-pc for Dynamips. --- gns3server/handlers/dynamips_vm_handler.py | 21 ++++++++- gns3server/modules/dynamips/__init__.py | 50 ++++++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/gns3server/handlers/dynamips_vm_handler.py b/gns3server/handlers/dynamips_vm_handler.py index bd3c4d4c..74387e74 100644 --- a/gns3server/handlers/dynamips_vm_handler.py +++ b/gns3server/handlers/dynamips_vm_handler.py @@ -18,6 +18,7 @@ import os import base64 +import asyncio from ..web.route import Route from ..schemas.dynamips_vm import VM_CREATE_SCHEMA from ..schemas.dynamips_vm import VM_UPDATE_SCHEMA @@ -366,7 +367,23 @@ class DynamipsVMHandler: yield from vm.set_idlepc("0x0") idlepcs = yield from vm.get_idle_pc_prop() + response.set_status(200) + response.json(idlepcs) + + @Route.get( + r"/projects/{project_id}/dynamips/vms/{vm_id}/auto_idlepc", + status_codes={ + 200: "Best Idle-pc value found", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Retrieve the idlepc proposals") + def get_auto_idlepc(request, response): - #idlepcs = yield from vm.show_idle_pc_prop() + dynamips_manager = Dynamips.instance() + vm = dynamips_manager.get_vm(request.match_info["vm_id"], + project_id=request.match_info["project_id"]) + idlepc = yield from dynamips_manager.auto_idlepc(vm) response.set_status(200) - response.json({"idlepcs": idlepcs}) + response.json({"idlepc": idlepc}) + diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index c03ada14..e6df32dd 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -536,3 +536,53 @@ class Dynamips(BaseManager): raise DynamipsError("Could not create config file {}: {}".format(path, e)) return os.path.join("configs", os.path.basename(path)) + + @asyncio.coroutine + def auto_idlepc(self, vm): + """ + Try to find the best possible idle-pc value. + + :param vm: VM instance + """ + + yield from vm.set_idlepc("0x0") + was_auto_started = False + try: + status = yield from vm.get_status() + if status != "running": + yield from vm.start() + was_auto_started = True + yield from asyncio.sleep(20) # leave time to the router to boot + validated_idlepc = None + idlepcs = yield from vm.get_idle_pc_prop() + if not idlepcs: + raise DynamipsError("No Idle-PC values found") + + for idlepc in idlepcs: + yield from vm.set_idlepc(idlepc.split()[0]) + log.debug("Auto Idle-PC: trying idle-PC value {}".format(vm.idlepc)) + start_time = time.time() + initial_cpu_usage = yield from vm.get_cpu_usage() + log.debug("Auto Idle-PC: initial CPU usage is {}%".format(initial_cpu_usage)) + yield from asyncio.sleep(3) # wait 3 seconds to probe the cpu again + elapsed_time = time.time() - start_time + cpu_usage = yield from vm.get_cpu_usage() + cpu_elapsed_usage = cpu_usage - initial_cpu_usage + cpu_usage = abs(cpu_elapsed_usage * 100.0 / elapsed_time) + if cpu_usage > 100: + cpu_usage = 100 + log.debug("Auto Idle-PC: CPU usage is {}% after {:.2} seconds".format(cpu_usage, elapsed_time)) + if cpu_usage < 70: + validated_idlepc = vm.idlepc + log.debug("Auto Idle-PC: idle-PC value {} has been validated".format(validated_idlepc)) + break + + if validated_idlepc is None: + raise DynamipsError("Sorry, no idle-pc value was suitable") + + except DynamipsError: + raise + finally: + if was_auto_started: + yield from vm.stop() + return validated_idlepc From 565a7b35a6f8b0862698acd75a5627e6dd6fa5d9 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sat, 21 Feb 2015 17:24:39 -0700 Subject: [PATCH 289/485] Default NVRAM and Idle-PC for some IOS images. --- gns3server/handlers/dynamips_vm_handler.py | 1 + gns3server/modules/dynamips/__init__.py | 3 +-- gns3server/modules/dynamips/nodes/c1700.py | 4 ++-- gns3server/modules/dynamips/nodes/c2600.py | 4 ++-- gns3server/modules/dynamips/nodes/c2691.py | 4 ++-- gns3server/modules/dynamips/nodes/c3600.py | 4 ++-- gns3server/modules/dynamips/nodes/c3725.py | 2 +- gns3server/modules/dynamips/nodes/c3745.py | 4 ++-- gns3server/modules/dynamips/nodes/c7200.py | 4 ++-- gns3server/modules/dynamips/nodes/router.py | 5 +++-- 10 files changed, 18 insertions(+), 17 deletions(-) diff --git a/gns3server/handlers/dynamips_vm_handler.py b/gns3server/handlers/dynamips_vm_handler.py index 74387e74..aa674e4c 100644 --- a/gns3server/handlers/dynamips_vm_handler.py +++ b/gns3server/handlers/dynamips_vm_handler.py @@ -106,6 +106,7 @@ class DynamipsVMHandler: vm = dynamips_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) yield from dynamips_manager.update_vm_settings(vm, request.json) + yield from dynamips_manager.ghost_ios_support(vm) response.json(vm) @classmethod diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index e6df32dd..5c388f14 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -410,7 +410,7 @@ class Dynamips(BaseManager): raise DynamipsError("mmap support is required to enable ghost IOS support") if vm.platform == "c7200" and vm.npe == "npe-g2": - log.warning("Ghost IOS is not support for c7200 with NPE-G2") + log.warning("Ghost IOS is not supported for c7200 with NPE-G2") return ghost_file = vm.formatted_ghost_file() @@ -530,7 +530,6 @@ class Dynamips(BaseManager): try: with open(path, "w") as f: - log.info("Creating config file {}".format(path)) f.write(content) except OSError as e: raise DynamipsError("Could not create config file {}: {}".format(path, e)) diff --git a/gns3server/modules/dynamips/nodes/c1700.py b/gns3server/modules/dynamips/nodes/c1700.py index 9ff6e51b..707c2baf 100644 --- a/gns3server/modules/dynamips/nodes/c1700.py +++ b/gns3server/modules/dynamips/nodes/c1700.py @@ -47,8 +47,8 @@ class C1700(Router): def __init__(self, name, vm_id, project, manager, dynamips_id, chassis="1720"): Router.__init__(self, name, vm_id, project, manager, dynamips_id, platform="c1700") - # Set default values for this platform - self._ram = 128 + # Set default values for this platform (must be the same as Dynamips) + self._ram = 64 self._nvram = 32 self._disk0 = 0 self._disk1 = 0 diff --git a/gns3server/modules/dynamips/nodes/c2600.py b/gns3server/modules/dynamips/nodes/c2600.py index 497a5d56..9a63c487 100644 --- a/gns3server/modules/dynamips/nodes/c2600.py +++ b/gns3server/modules/dynamips/nodes/c2600.py @@ -62,8 +62,8 @@ class C2600(Router): def __init__(self, name, vm_id, project, manager, dynamips_id, chassis="2610"): Router.__init__(self, name, vm_id, project, manager, dynamips_id, platform="c2600") - # Set default values for this platform - self._ram = 128 + # Set default values for this platform (must be the same as Dynamips) + self._ram = 64 self._nvram = 128 self._disk0 = 0 self._disk1 = 0 diff --git a/gns3server/modules/dynamips/nodes/c2691.py b/gns3server/modules/dynamips/nodes/c2691.py index d40efd2c..3b5b7332 100644 --- a/gns3server/modules/dynamips/nodes/c2691.py +++ b/gns3server/modules/dynamips/nodes/c2691.py @@ -43,8 +43,8 @@ class C2691(Router): def __init__(self, name, vm_id, project, manager, dynamips_id): Router.__init__(self, name, vm_id, project, manager, dynamips_id, platform="c2691") - # Set default values for this platform - self._ram = 192 + # Set default values for this platform (must be the same as Dynamips) + self._ram = 128 self._nvram = 112 self._disk0 = 16 self._disk1 = 0 diff --git a/gns3server/modules/dynamips/nodes/c3600.py b/gns3server/modules/dynamips/nodes/c3600.py index 415d9a74..aa0d2249 100644 --- a/gns3server/modules/dynamips/nodes/c3600.py +++ b/gns3server/modules/dynamips/nodes/c3600.py @@ -45,8 +45,8 @@ class C3600(Router): def __init__(self, name, vm_id, project, manager, dynamips_id, chassis="3640"): Router.__init__(self, name, vm_id, project, manager, dynamips_id, platform="c3600") - # Set default values for this platform - self._ram = 192 + # Set default values for this platform (must be the same as Dynamips) + self._ram = 128 self._nvram = 128 self._disk0 = 0 self._disk1 = 0 diff --git a/gns3server/modules/dynamips/nodes/c3725.py b/gns3server/modules/dynamips/nodes/c3725.py index 0d0ce36d..6a1481a1 100644 --- a/gns3server/modules/dynamips/nodes/c3725.py +++ b/gns3server/modules/dynamips/nodes/c3725.py @@ -43,7 +43,7 @@ class C3725(Router): def __init__(self, name, vm_id, project, manager, dynamips_id): Router.__init__(self, name, vm_id, project, manager, dynamips_id, platform="c3725") - # Set default values for this platform + # Set default values for this platform (must be the same as Dynamips) self._ram = 128 self._nvram = 112 self._disk0 = 16 diff --git a/gns3server/modules/dynamips/nodes/c3745.py b/gns3server/modules/dynamips/nodes/c3745.py index d94b9883..e9bd84e1 100644 --- a/gns3server/modules/dynamips/nodes/c3745.py +++ b/gns3server/modules/dynamips/nodes/c3745.py @@ -43,8 +43,8 @@ class C3745(Router): def __init__(self, name, vm_id, project, manager, dynamips_id): Router.__init__(self, name, vm_id, project, manager, dynamips_id, platform="c3745") - # Set default values for this platform - self._ram = 256 + # Set default values for this platform (must be the same as Dynamips) + self._ram = 128 self._nvram = 304 self._disk0 = 16 self._disk1 = 0 diff --git a/gns3server/modules/dynamips/nodes/c7200.py b/gns3server/modules/dynamips/nodes/c7200.py index ca70ecb7..218d35ab 100644 --- a/gns3server/modules/dynamips/nodes/c7200.py +++ b/gns3server/modules/dynamips/nodes/c7200.py @@ -47,8 +47,8 @@ class C7200(Router): def __init__(self, name, vm_id, project, manager, dynamips_id, npe="npe-400"): Router.__init__(self, name, vm_id, project, manager, dynamips_id, platform="c7200") - # Set default values for this platform - self._ram = 512 + # Set default values for this platform (must be the same as Dynamips) + self._ram = 256 self._nvram = 128 self._disk0 = 64 self._disk1 = 0 diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index 92f14c02..a94f4af5 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -308,7 +308,8 @@ class Router(BaseVM): if self._dynamips_id in self._dynamips_ids[self._project.id]: self._dynamips_ids[self._project.id].remove(self._dynamips_id) - self._hypervisor.devices.remove(self) + if self in self._hypervisor.devices: + self._hypervisor.devices.remove(self) if self._hypervisor and not self._hypervisor.devices: try: yield from self.stop() @@ -1478,7 +1479,7 @@ class Router(BaseVM): """ try: - reply = yield from self._hypervisor.send("vm extract_config {}".format(self._name)) + reply = yield from self._hypervisor.send('vm extract_config "{}"'.format(self._name)) except DynamipsError: # for some reason Dynamips gets frozen when it does not find the magic number in the NVRAM file. return None, None From da11343647efd4d3535ee1d43c5f41f7143622d8 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sun, 22 Feb 2015 12:36:44 -0700 Subject: [PATCH 290/485] Fixes aiohttp.errors.ClientDisconnectedError errors when SIGINT is received. --- gns3server/server.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/gns3server/server.py b/gns3server/server.py index 81731d43..7e47a307 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -52,10 +52,10 @@ class Server: self._port_manager = PortManager(host) @asyncio.coroutine - def _run_application(self, app, ssl_context=None): + def _run_application(self, handler, ssl_context=None): try: - server = yield from self._loop.create_server(app.make_handler(handler=RequestHandler), self._host, self._port, ssl=ssl_context) + server = yield from self._loop.create_server(handler, self._host, self._port, ssl=ssl_context) except OSError as e: log.critical("Could not start the server: {}".format(e)) self._loop.stop() @@ -74,11 +74,12 @@ class Server: yield from m.unload() self._loop.stop() - def _signal_handling(self): + def _signal_handling(self, handler): @asyncio.coroutine def signal_handler(signame): log.warning("Server has got signal {}, exiting...".format(signame)) + yield from handler.finish_connections() yield from self._stop_application() signals = ["SIGTERM", "SIGINT"] @@ -177,8 +178,9 @@ class Server: m.port_manager = self._port_manager log.info("Starting server on {}:{}".format(self._host, self._port)) - self._loop.run_until_complete(self._run_application(app, ssl_context)) - self._signal_handling() + handler = app.make_handler(handler=RequestHandler) + self._loop.run_until_complete(self._run_application(handler, ssl_context)) + self._signal_handling(handler) if server_config.getboolean("live"): log.info("Code live reload is enabled, watching for file changes") From 210aa6f12af98a22726a5dede60cdb6242f5c40c Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sun, 22 Feb 2015 19:56:52 -0700 Subject: [PATCH 291/485] Bit of cleaning. --- gns3server/handlers/qemu_handler.py | 1 - gns3server/modules/qemu/qemu_vm.py | 7 ------- gns3server/schemas/iou.py | 7 +------ 3 files changed, 1 insertion(+), 14 deletions(-) diff --git a/gns3server/handlers/qemu_handler.py b/gns3server/handlers/qemu_handler.py index c170f7f6..a802e3b2 100644 --- a/gns3server/handlers/qemu_handler.py +++ b/gns3server/handlers/qemu_handler.py @@ -292,6 +292,5 @@ class QEMUHandler: output=QEMU_BINARY_LIST_SCHEMA) def list_binaries(request, response): - qemu_manager = Qemu.instance() binaries = yield from Qemu.binary_list() response.json(binaries) diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index 6b491c76..eb4ab118 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -25,19 +25,12 @@ import shutil import random import subprocess import shlex -import ntpath -import telnetlib -import time -import re import asyncio -from gns3server.config import Config - from .qemu_error import QemuError from ..adapters.ethernet_adapter import EthernetAdapter from ..nios.nio_udp import NIO_UDP from ..base_vm import BaseVM -from ...utils.asyncio import subprocess_check_output from ...schemas.qemu import QEMU_OBJECT_SCHEMA import logging diff --git a/gns3server/schemas/iou.py b/gns3server/schemas/iou.py index e495af5e..9a19c229 100644 --- a/gns3server/schemas/iou.py +++ b/gns3server/schemas/iou.py @@ -288,12 +288,7 @@ IOU_INITIAL_CONFIG_SCHEMA = { "type": ["string", "null"], "minLength": 1, }, - "path": { - "description": "Relative path on the server of the initial configuration file", - "type": ["string", "null"], - "minLength": 1, - }, }, "additionalProperties": False, - "required": ["content", "path"] + "required": ["content"] } From f7cd09d5fb1ac2783809412551588e73b0ffff98 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 23 Feb 2015 11:27:07 +0100 Subject: [PATCH 292/485] Display an upload form (upload is not yet ready) --- gns3server/handlers/__init__.py | 28 ++++++++++----- gns3server/handlers/api/__init__.py | 9 +++++ .../{ => api}/dynamips_device_handler.py | 14 ++++---- .../handlers/{ => api}/dynamips_vm_handler.py | 18 +++++----- gns3server/handlers/{ => api}/iou_handler.py | 18 +++++----- .../handlers/{ => api}/network_handler.py | 6 ++-- .../handlers/{ => api}/project_handler.py | 8 ++--- gns3server/handlers/{ => api}/qemu_handler.py | 16 ++++----- .../handlers/{ => api}/version_handler.py | 6 ++-- .../handlers/{ => api}/virtualbox_handler.py | 16 ++++----- gns3server/handlers/{ => api}/vpcs_handler.py | 12 +++---- gns3server/handlers/upload_handler.py | 34 ++++++++++++++++++ gns3server/templates/layout.html | 12 +++++++ gns3server/templates/upload.html | 36 +++++++++---------- gns3server/web/response.py | 25 +++++++++++++ gns3server/web/route.py | 16 ++++++--- requirements.txt | 1 + setup.py | 3 +- tests/conftest.py | 2 +- tests/{ => handlers}/api/__init__.py | 0 tests/{ => handlers}/api/base.py | 23 +++++++----- tests/{ => handlers}/api/test_dynamips.py | 0 tests/{ => handlers}/api/test_iou.py | 0 tests/{ => handlers}/api/test_network.py | 0 tests/{ => handlers}/api/test_project.py | 0 tests/{ => handlers}/api/test_qemu.py | 0 tests/{ => handlers}/api/test_version.py | 0 tests/{ => handlers}/api/test_virtualbox.py | 0 tests/{ => handlers}/api/test_vpcs.py | 0 tests/handlers/test_upload.py | 31 ++++++++++++++++ 30 files changed, 234 insertions(+), 100 deletions(-) create mode 100644 gns3server/handlers/api/__init__.py rename gns3server/handlers/{ => api}/dynamips_device_handler.py (95%) rename gns3server/handlers/{ => api}/dynamips_vm_handler.py (97%) rename gns3server/handlers/{ => api}/iou_handler.py (96%) rename gns3server/handlers/{ => api}/network_handler.py (91%) rename gns3server/handlers/{ => api}/project_handler.py (95%) rename gns3server/handlers/{ => api}/qemu_handler.py (96%) rename gns3server/handlers/{ => api}/version_handler.py (93%) rename gns3server/handlers/{ => api}/virtualbox_handler.py (96%) rename gns3server/handlers/{ => api}/vpcs_handler.py (96%) create mode 100644 gns3server/handlers/upload_handler.py create mode 100644 gns3server/templates/layout.html rename tests/{ => handlers}/api/__init__.py (100%) rename tests/{ => handlers}/api/base.py (84%) rename tests/{ => handlers}/api/test_dynamips.py (100%) rename tests/{ => handlers}/api/test_iou.py (100%) rename tests/{ => handlers}/api/test_network.py (100%) rename tests/{ => handlers}/api/test_project.py (100%) rename tests/{ => handlers}/api/test_qemu.py (100%) rename tests/{ => handlers}/api/test_version.py (100%) rename tests/{ => handlers}/api/test_virtualbox.py (100%) rename tests/{ => handlers}/api/test_vpcs.py (100%) create mode 100644 tests/handlers/test_upload.py diff --git a/gns3server/handlers/__init__.py b/gns3server/handlers/__init__.py index 5d558245..9f824c16 100644 --- a/gns3server/handlers/__init__.py +++ b/gns3server/handlers/__init__.py @@ -1,9 +1,19 @@ -__all__ = ["version_handler", - "network_handler", - "vpcs_handler", - "project_handler", - "virtualbox_handler", - "dynamips_vm_handler", - "dynamips_device_handler", - "iou_handler", - "qemu_handler"] +# +# 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 gns3server.handlers.api import * +from gns3server.handlers.upload_handler import UploadHandler + diff --git a/gns3server/handlers/api/__init__.py b/gns3server/handlers/api/__init__.py new file mode 100644 index 00000000..5d558245 --- /dev/null +++ b/gns3server/handlers/api/__init__.py @@ -0,0 +1,9 @@ +__all__ = ["version_handler", + "network_handler", + "vpcs_handler", + "project_handler", + "virtualbox_handler", + "dynamips_vm_handler", + "dynamips_device_handler", + "iou_handler", + "qemu_handler"] diff --git a/gns3server/handlers/dynamips_device_handler.py b/gns3server/handlers/api/dynamips_device_handler.py similarity index 95% rename from gns3server/handlers/dynamips_device_handler.py rename to gns3server/handlers/api/dynamips_device_handler.py index 4be6be4c..3a6f8588 100644 --- a/gns3server/handlers/dynamips_device_handler.py +++ b/gns3server/handlers/api/dynamips_device_handler.py @@ -17,13 +17,13 @@ import os import asyncio -from ..web.route import Route -from ..schemas.dynamips_device import DEVICE_CREATE_SCHEMA -from ..schemas.dynamips_device import DEVICE_UPDATE_SCHEMA -from ..schemas.dynamips_device import DEVICE_CAPTURE_SCHEMA -from ..schemas.dynamips_device import DEVICE_OBJECT_SCHEMA -from ..schemas.dynamips_device import DEVICE_NIO_SCHEMA -from ..modules.dynamips import Dynamips +from ...web.route import Route +from ...schemas.dynamips_device import DEVICE_CREATE_SCHEMA +from ...schemas.dynamips_device import DEVICE_UPDATE_SCHEMA +from ...schemas.dynamips_device import DEVICE_CAPTURE_SCHEMA +from ...schemas.dynamips_device import DEVICE_OBJECT_SCHEMA +from ...schemas.dynamips_device import DEVICE_NIO_SCHEMA +from ...modules.dynamips import Dynamips class DynamipsDeviceHandler: diff --git a/gns3server/handlers/dynamips_vm_handler.py b/gns3server/handlers/api/dynamips_vm_handler.py similarity index 97% rename from gns3server/handlers/dynamips_vm_handler.py rename to gns3server/handlers/api/dynamips_vm_handler.py index aa674e4c..c2855a29 100644 --- a/gns3server/handlers/dynamips_vm_handler.py +++ b/gns3server/handlers/api/dynamips_vm_handler.py @@ -19,16 +19,16 @@ import os import base64 import asyncio -from ..web.route import Route -from ..schemas.dynamips_vm import VM_CREATE_SCHEMA -from ..schemas.dynamips_vm import VM_UPDATE_SCHEMA -from ..schemas.dynamips_vm import VM_CAPTURE_SCHEMA -from ..schemas.dynamips_vm import VM_OBJECT_SCHEMA -from ..schemas.dynamips_vm import VM_NIO_SCHEMA -from ..schemas.dynamips_vm import VM_CONFIGS_SCHEMA -from ..modules.dynamips import Dynamips -from ..modules.project_manager import ProjectManager +from ...web.route import Route +from ...schemas.dynamips_vm import VM_CREATE_SCHEMA +from ...schemas.dynamips_vm import VM_UPDATE_SCHEMA +from ...schemas.dynamips_vm import VM_CAPTURE_SCHEMA +from ...schemas.dynamips_vm import VM_OBJECT_SCHEMA +from ...schemas.dynamips_vm import VM_NIO_SCHEMA +from ...schemas.dynamips_vm import VM_CONFIGS_SCHEMA +from ...modules.dynamips import Dynamips +from ...modules.project_manager import ProjectManager class DynamipsVMHandler: diff --git a/gns3server/handlers/iou_handler.py b/gns3server/handlers/api/iou_handler.py similarity index 96% rename from gns3server/handlers/iou_handler.py rename to gns3server/handlers/api/iou_handler.py index 05d34614..30aada83 100644 --- a/gns3server/handlers/iou_handler.py +++ b/gns3server/handlers/api/iou_handler.py @@ -18,15 +18,15 @@ import os -from ..web.route import Route -from ..modules.port_manager import PortManager -from ..schemas.iou import IOU_CREATE_SCHEMA -from ..schemas.iou import IOU_UPDATE_SCHEMA -from ..schemas.iou import IOU_OBJECT_SCHEMA -from ..schemas.iou import IOU_NIO_SCHEMA -from ..schemas.iou import IOU_CAPTURE_SCHEMA -from ..schemas.iou import IOU_INITIAL_CONFIG_SCHEMA -from ..modules.iou import IOU +from ...web.route import Route +from ...modules.port_manager import PortManager +from ...schemas.iou import IOU_CREATE_SCHEMA +from ...schemas.iou import IOU_UPDATE_SCHEMA +from ...schemas.iou import IOU_OBJECT_SCHEMA +from ...schemas.iou import IOU_NIO_SCHEMA +from ...schemas.iou import IOU_CAPTURE_SCHEMA +from ...schemas.iou import IOU_INITIAL_CONFIG_SCHEMA +from ...modules.iou import IOU class IOUHandler: diff --git a/gns3server/handlers/network_handler.py b/gns3server/handlers/api/network_handler.py similarity index 91% rename from gns3server/handlers/network_handler.py rename to gns3server/handlers/api/network_handler.py index 87dedc29..e84cdb4f 100644 --- a/gns3server/handlers/network_handler.py +++ b/gns3server/handlers/api/network_handler.py @@ -15,9 +15,9 @@ # 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.port_manager import PortManager -from ..utils.interfaces import interfaces +from ...web.route import Route +from ...modules.port_manager import PortManager +from ...utils.interfaces import interfaces class NetworkHandler: diff --git a/gns3server/handlers/project_handler.py b/gns3server/handlers/api/project_handler.py similarity index 95% rename from gns3server/handlers/project_handler.py rename to gns3server/handlers/api/project_handler.py index 880b5473..00559dca 100644 --- a/gns3server/handlers/project_handler.py +++ b/gns3server/handlers/api/project_handler.py @@ -15,10 +15,10 @@ # 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 ..schemas.project import PROJECT_OBJECT_SCHEMA, PROJECT_CREATE_SCHEMA, PROJECT_UPDATE_SCHEMA -from ..modules.project_manager import ProjectManager -from ..modules import MODULES +from ...web.route import Route +from ...schemas.project import PROJECT_OBJECT_SCHEMA, PROJECT_CREATE_SCHEMA, PROJECT_UPDATE_SCHEMA +from ...modules.project_manager import ProjectManager +from ...modules import MODULES class ProjectHandler: diff --git a/gns3server/handlers/qemu_handler.py b/gns3server/handlers/api/qemu_handler.py similarity index 96% rename from gns3server/handlers/qemu_handler.py rename to gns3server/handlers/api/qemu_handler.py index a802e3b2..74102dbe 100644 --- a/gns3server/handlers/qemu_handler.py +++ b/gns3server/handlers/api/qemu_handler.py @@ -18,14 +18,14 @@ import os -from ..web.route import Route -from ..modules.port_manager import PortManager -from ..schemas.qemu import QEMU_CREATE_SCHEMA -from ..schemas.qemu import QEMU_UPDATE_SCHEMA -from ..schemas.qemu import QEMU_OBJECT_SCHEMA -from ..schemas.qemu import QEMU_NIO_SCHEMA -from ..schemas.qemu import QEMU_BINARY_LIST_SCHEMA -from ..modules.qemu import Qemu +from ...web.route import Route +from ...modules.port_manager import PortManager +from ...schemas.qemu import QEMU_CREATE_SCHEMA +from ...schemas.qemu import QEMU_UPDATE_SCHEMA +from ...schemas.qemu import QEMU_OBJECT_SCHEMA +from ...schemas.qemu import QEMU_NIO_SCHEMA +from ...schemas.qemu import QEMU_BINARY_LIST_SCHEMA +from ...modules.qemu import Qemu class QEMUHandler: diff --git a/gns3server/handlers/version_handler.py b/gns3server/handlers/api/version_handler.py similarity index 93% rename from gns3server/handlers/version_handler.py rename to gns3server/handlers/api/version_handler.py index 6d020967..a935e3ca 100644 --- a/gns3server/handlers/version_handler.py +++ b/gns3server/handlers/api/version_handler.py @@ -15,9 +15,9 @@ # 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 ..schemas.version import VERSION_SCHEMA -from ..version import __version__ +from ...web.route import Route +from ...schemas.version import VERSION_SCHEMA +from ...version import __version__ from aiohttp.web import HTTPConflict diff --git a/gns3server/handlers/virtualbox_handler.py b/gns3server/handlers/api/virtualbox_handler.py similarity index 96% rename from gns3server/handlers/virtualbox_handler.py rename to gns3server/handlers/api/virtualbox_handler.py index 51768bd3..be75c4cf 100644 --- a/gns3server/handlers/virtualbox_handler.py +++ b/gns3server/handlers/api/virtualbox_handler.py @@ -16,14 +16,14 @@ # along with this program. If not, see . import os -from ..web.route import Route -from ..schemas.virtualbox import VBOX_CREATE_SCHEMA -from ..schemas.virtualbox import VBOX_UPDATE_SCHEMA -from ..schemas.virtualbox import VBOX_NIO_SCHEMA -from ..schemas.virtualbox import VBOX_CAPTURE_SCHEMA -from ..schemas.virtualbox import VBOX_OBJECT_SCHEMA -from ..modules.virtualbox import VirtualBox -from ..modules.project_manager import ProjectManager +from ...web.route import Route +from ...schemas.virtualbox import VBOX_CREATE_SCHEMA +from ...schemas.virtualbox import VBOX_UPDATE_SCHEMA +from ...schemas.virtualbox import VBOX_NIO_SCHEMA +from ...schemas.virtualbox import VBOX_CAPTURE_SCHEMA +from ...schemas.virtualbox import VBOX_OBJECT_SCHEMA +from ...modules.virtualbox import VirtualBox +from ...modules.project_manager import ProjectManager class VirtualBoxHandler: diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/api/vpcs_handler.py similarity index 96% rename from gns3server/handlers/vpcs_handler.py rename to gns3server/handlers/api/vpcs_handler.py index 24458405..588ff50c 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/api/vpcs_handler.py @@ -15,12 +15,12 @@ # 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 ..schemas.vpcs import VPCS_CREATE_SCHEMA -from ..schemas.vpcs import VPCS_UPDATE_SCHEMA -from ..schemas.vpcs import VPCS_OBJECT_SCHEMA -from ..schemas.vpcs import VPCS_NIO_SCHEMA -from ..modules.vpcs import VPCS +from ...web.route import Route +from ...schemas.vpcs import VPCS_CREATE_SCHEMA +from ...schemas.vpcs import VPCS_UPDATE_SCHEMA +from ...schemas.vpcs import VPCS_OBJECT_SCHEMA +from ...schemas.vpcs import VPCS_NIO_SCHEMA +from ...modules.vpcs import VPCS class VPCSHandler: diff --git a/gns3server/handlers/upload_handler.py b/gns3server/handlers/upload_handler.py new file mode 100644 index 00000000..7bc6a5b7 --- /dev/null +++ b/gns3server/handlers/upload_handler.py @@ -0,0 +1,34 @@ +# -*- 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 ..schemas.version import VERSION_SCHEMA +from ..version import __version__ +from aiohttp.web import HTTPConflict + + +class UploadHandler: + + @classmethod + @Route.get( + r"/upload", + description="Manage upload of GNS3 images", + api_version=None + ) + def index(request, response): + response.template("upload.html") + diff --git a/gns3server/templates/layout.html b/gns3server/templates/layout.html new file mode 100644 index 00000000..9ecbc82c --- /dev/null +++ b/gns3server/templates/layout.html @@ -0,0 +1,12 @@ + + + +GNS3 Server + + +{% block body %}{% endblock %} + + + Powered by GNS3 {{gns3_version}} + + diff --git a/gns3server/templates/upload.html b/gns3server/templates/upload.html index ceec7f68..c912e49d 100644 --- a/gns3server/templates/upload.html +++ b/gns3server/templates/upload.html @@ -1,20 +1,16 @@ - - - -Upload Form for GNS3 server {{version}} - - -

Select & Upload (v{{version}})

-
-File: -
-
- -
-{%if items%} -

Files on {{host}}

-{%for item in items%} -

{{path}}/{{item}}

-{%end%} -{%end%} - \ No newline at end of file +{% extends "layout.html" %} +{% block body %} +

Select & Upload an image for GNS3

+
+ File: +
+
+ +
+ {%if items%} +

Files on {{host}}

+ {%for item in items%} +

{{path}}/{{item}}

+ {%endfor%} + {%endif%} +{% endblock %} diff --git a/gns3server/web/response.py b/gns3server/web/response.py index 9241d0c9..9bd453c6 100644 --- a/gns3server/web/response.py +++ b/gns3server/web/response.py @@ -20,9 +20,12 @@ import jsonschema import aiohttp.web import logging import sys +import jinja2 + from ..version import __version__ log = logging.getLogger(__name__) +renderer = jinja2.Environment(loader=jinja2.PackageLoader('gns3server', 'templates')) class Response(aiohttp.web.Response): @@ -47,6 +50,28 @@ class Response(aiohttp.web.Response): log.debug(json.loads(self.body.decode('utf-8'))) return super().start(request) + def html(self, answer): + """ + Set the response content type to text/html and serialize + the content. + + :param anwser The response as a Python object + """ + + self.content_type = "text/html" + self.body = answer.encode('utf-8') + + def template(self, template_filename, **kwargs): + """ + Render a template + + :params template: Template name + :params kwargs: Template parameters + """ + template = renderer.get_template(template_filename) + kwargs["gns3_version"] = __version__ + self.html(template.render(**kwargs)) + def json(self, answer): """ Set the response content type to application/json and serialize diff --git a/gns3server/web/route.py b/gns3server/web/route.py index 1fee37c7..4ec4d2b8 100644 --- a/gns3server/web/route.py +++ b/gns3server/web/route.py @@ -81,8 +81,11 @@ class Route(object): # This block is executed only the first time output_schema = kw.get("output", {}) input_schema = kw.get("input", {}) - api_version = kw.get("version", 1) - cls._path = "/v{version}{path}".format(path=path, version=api_version) + api_version = kw.get("api_version", 1) + if api_version is None: + cls._path = path + else: + cls._path = "/v{version}{path}".format(path=path, version=api_version) def register(func): route = cls._path @@ -123,8 +126,13 @@ class Route(object): response.set_status(500) exc_type, exc_value, exc_tb = sys.exc_info() lines = traceback.format_exception(exc_type, exc_value, exc_tb) - tb = "".join(lines) - response.json({"message": tb, "status": 500}) + if api_version is not None: + tb = "".join(lines) + response.json({"message": tb, "status": 500}) + else: + tb = "\n".join(lines) + response.html("

Internal error

{}
".format(tb)) + return response cls._routes.append((method, cls._path, control_schema)) diff --git a/requirements.txt b/requirements.txt index 1c9c8dbc..cd8c3f8d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ python-dateutil==2.3 apache-libcloud==0.16.0 requests==2.5.0 aiohttp==0.14.4 +Jinja2==2.7.3 diff --git a/setup.py b/setup.py index 2a06203d..238efc3d 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,8 @@ class PyTest(TestCommand): dependencies = ["aiohttp==0.14.4", "jsonschema==2.4.0", "apache-libcloud==0.16.0", - "requests==2.5.0"] + "requests==2.5.0", + "Jinja2==2.7.3"] if sys.version_info == (3, 3): dependencies.append("asyncio==3.4.2") diff --git a/tests/conftest.py b/tests/conftest.py index cf146e81..51b5cee6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -30,7 +30,7 @@ from gns3server.handlers import * from gns3server.modules import MODULES from gns3server.modules.port_manager import PortManager from gns3server.modules.project_manager import ProjectManager -from tests.api.base import Query +from tests.handlers.api.base import Query # Prevent execution of external binaries diff --git a/tests/api/__init__.py b/tests/handlers/api/__init__.py similarity index 100% rename from tests/api/__init__.py rename to tests/handlers/api/__init__.py diff --git a/tests/api/base.py b/tests/handlers/api/base.py similarity index 84% rename from tests/api/base.py rename to tests/handlers/api/base.py index 312c3175..d0f38878 100644 --- a/tests/api/base.py +++ b/tests/handlers/api/base.py @@ -44,22 +44,25 @@ class Query: def delete(self, path, **kwargs): return self._fetch("DELETE", path, **kwargs) - def _get_url(self, path): - return "http://{}:{}/v1{}".format(self._host, self._port, path) + def _get_url(self, path, version): + if version is None: + return "http://{}:{}{}".format(self._host, self._port, path) + return "http://{}:{}/v{}{}".format(self._host, self._port, version, path) - def _fetch(self, method, path, body=None, **kwargs): + def _fetch(self, method, path, body=None, api_version = 1, **kwargs): """Fetch an url, parse the JSON and return response Options: - example if True the session is included inside documentation - raw do not JSON encode the query + - api_version Version of API, None if no version """ if body is not None and not kwargs.get("raw", False): body = json.dumps(body) @asyncio.coroutine def go(future): - response = yield from aiohttp.request(method, self._get_url(path), data=body) + response = yield from aiohttp.request(method, self._get_url(path, api_version), data=body) future.set_result(response) future = asyncio.Future() asyncio.async(go(future)) @@ -79,12 +82,16 @@ class Query: response.route = x_route.replace("/v1", "") if response.body is not None: - try: - response.json = json.loads(response.body.decode("utf-8")) - except ValueError: - response.json = None + if response.headers.get("CONTENT-TYPE", "") == "application/json": + try: + response.json = json.loads(response.body.decode("utf-8")) + except ValueError: + response.json = None + else: + response.html = response.body.decode("utf-8") else: response.json = {} + response.html = "" if kwargs.get('example') and os.environ.get("PYTEST_BUILD_DOCUMENTATION") == "1": self._dump_example(method, response.route, body, response) return response diff --git a/tests/api/test_dynamips.py b/tests/handlers/api/test_dynamips.py similarity index 100% rename from tests/api/test_dynamips.py rename to tests/handlers/api/test_dynamips.py diff --git a/tests/api/test_iou.py b/tests/handlers/api/test_iou.py similarity index 100% rename from tests/api/test_iou.py rename to tests/handlers/api/test_iou.py diff --git a/tests/api/test_network.py b/tests/handlers/api/test_network.py similarity index 100% rename from tests/api/test_network.py rename to tests/handlers/api/test_network.py diff --git a/tests/api/test_project.py b/tests/handlers/api/test_project.py similarity index 100% rename from tests/api/test_project.py rename to tests/handlers/api/test_project.py diff --git a/tests/api/test_qemu.py b/tests/handlers/api/test_qemu.py similarity index 100% rename from tests/api/test_qemu.py rename to tests/handlers/api/test_qemu.py diff --git a/tests/api/test_version.py b/tests/handlers/api/test_version.py similarity index 100% rename from tests/api/test_version.py rename to tests/handlers/api/test_version.py diff --git a/tests/api/test_virtualbox.py b/tests/handlers/api/test_virtualbox.py similarity index 100% rename from tests/api/test_virtualbox.py rename to tests/handlers/api/test_virtualbox.py diff --git a/tests/api/test_vpcs.py b/tests/handlers/api/test_vpcs.py similarity index 100% rename from tests/api/test_vpcs.py rename to tests/handlers/api/test_vpcs.py diff --git a/tests/handlers/test_upload.py b/tests/handlers/test_upload.py new file mode 100644 index 00000000..d53b6302 --- /dev/null +++ b/tests/handlers/test_upload.py @@ -0,0 +1,31 @@ +# -*- 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 . + +""" +This test suite check /version endpoint +It's also used for unittest the HTTP implementation. +""" + +from gns3server.version import __version__ + + +def test_version_index_upload(server): + response = server.get('/upload', api_version=None) + assert response.status == 200 + html = response.html + assert "GNS3 Server" in html + assert "Select & Upload" in html From 259f6249e2341362770e5e619737b0b22f75103d Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 23 Feb 2015 16:09:52 +0100 Subject: [PATCH 293/485] Fix tests --- gns3server/handlers/api/iou_handler.py | 3 +-- tests/handlers/api/test_iou.py | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/gns3server/handlers/api/iou_handler.py b/gns3server/handlers/api/iou_handler.py index 30aada83..63a18a42 100644 --- a/gns3server/handlers/api/iou_handler.py +++ b/gns3server/handlers/api/iou_handler.py @@ -308,5 +308,4 @@ class IOUHandler: vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) response.set_status(200) - response.json({"content": vm.initial_config, - "path": vm.relative_initial_config_file}) + response.json({"content": vm.initial_config}) diff --git a/tests/handlers/api/test_iou.py b/tests/handlers/api/test_iou.py index e112bd94..5e1b29dc 100644 --- a/tests/handlers/api/test_iou.py +++ b/tests/handlers/api/test_iou.py @@ -234,7 +234,6 @@ def test_get_initial_config_without_config_file(server, vm): response = server.get("/projects/{project_id}/iou/vms/{vm_id}/initial_config".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert response.status == 200 assert response.json["content"] == None - assert response.json["path"] == None def test_get_initial_config_with_config_file(server, project, vm): @@ -246,4 +245,3 @@ def test_get_initial_config_with_config_file(server, project, vm): response = server.get("/projects/{project_id}/iou/vms/{vm_id}/initial_config".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert response.status == 200 assert response.json["content"] == "TEST" - assert response.json["path"] == "initial-config.cfg" From c9314ec5093deea6bdf254c0d6b261c1ba2bb49a Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 23 Feb 2015 17:21:39 +0100 Subject: [PATCH 294/485] autopep8 et upload files --- gns3server/handlers/__init__.py | 1 - .../handlers/api/dynamips_vm_handler.py | 2 +- gns3server/handlers/upload_handler.py | 39 ++++++++++++++++++- gns3server/modules/dynamips/__init__.py | 12 +++--- .../modules/dynamips/nodes/atm_switch.py | 2 +- gns3server/modules/dynamips/nodes/bridge.py | 1 + gns3server/modules/dynamips/nodes/device.py | 1 + .../modules/dynamips/nodes/ethernet_hub.py | 1 + .../modules/dynamips/nodes/ethernet_switch.py | 1 + .../dynamips/nodes/frame_relay_switch.py | 1 + gns3server/schemas/dynamips_device.py | 6 +-- gns3server/templates/upload.html | 8 ++-- gns3server/web/response.py | 10 ++++- gns3server/web/route.py | 10 +++++ tests/handlers/api/base.py | 2 +- tests/handlers/test_upload.py | 26 ++++++++++--- 16 files changed, 99 insertions(+), 24 deletions(-) diff --git a/gns3server/handlers/__init__.py b/gns3server/handlers/__init__.py index 9f824c16..8d2587c9 100644 --- a/gns3server/handlers/__init__.py +++ b/gns3server/handlers/__init__.py @@ -16,4 +16,3 @@ from gns3server.handlers.api import * from gns3server.handlers.upload_handler import UploadHandler - diff --git a/gns3server/handlers/api/dynamips_vm_handler.py b/gns3server/handlers/api/dynamips_vm_handler.py index c2855a29..79e7c4c0 100644 --- a/gns3server/handlers/api/dynamips_vm_handler.py +++ b/gns3server/handlers/api/dynamips_vm_handler.py @@ -30,6 +30,7 @@ from ...schemas.dynamips_vm import VM_CONFIGS_SCHEMA from ...modules.dynamips import Dynamips from ...modules.project_manager import ProjectManager + class DynamipsVMHandler: """ @@ -387,4 +388,3 @@ class DynamipsVMHandler: idlepc = yield from dynamips_manager.auto_idlepc(vm) response.set_status(200) response.json({"idlepc": idlepc}) - diff --git a/gns3server/handlers/upload_handler.py b/gns3server/handlers/upload_handler.py index 7bc6a5b7..2f5d9fd4 100644 --- a/gns3server/handlers/upload_handler.py +++ b/gns3server/handlers/upload_handler.py @@ -15,6 +15,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os +import stat + +from ..config import Config from ..web.route import Route from ..schemas.version import VERSION_SCHEMA from ..version import __version__ @@ -30,5 +34,38 @@ class UploadHandler: api_version=None ) def index(request, response): - response.template("upload.html") + files = [] + for filename in os.listdir(UploadHandler.image_directory()): + if os.path.isfile(os.path.join(UploadHandler.image_directory(), filename)): + if filename[0] != ".": + files.append(filename) + response.template("upload.html", files=files, image_path=UploadHandler.image_directory()) + + @classmethod + @Route.post( + r"/upload", + description="Manage upload of GNS3 images", + api_version=None + ) + def upload(request, response): + data = yield from request.post() + + destination_path = os.path.join(UploadHandler.image_directory(), data["file"].filename) + + try: + os.makedirs(UploadHandler.image_directory(), exist_ok=True) + with open(destination_path, "wb+") as f: + f.write(data["file"].file.read()) + print(destination_path) + st = os.stat(destination_path) + os.chmod(destination_path, st.st_mode | stat.S_IXUSR) + except OSError as e: + response.html("Could not upload file: {}".format(e)) + response.set_status(500) + return + response.redirect("/upload") + @staticmethod + def image_directory(): + server_config = Config.instance().get_section_config("Server") + return os.path.expanduser(server_config.get("image_directory", "~/GNS3/images")) diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index 5c388f14..d8f70ab6 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -357,10 +357,10 @@ class Dynamips(BaseManager): raise DynamipsError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e)) # check if we have an allocated NIO UDP auto #nio = node.hypervisor.get_nio_udp_auto(lport) - #if not nio: + # if not nio: # otherwise create an NIO UDP nio = NIOUDP(node.hypervisor, lport, rhost, rport) - #else: + # else: # nio.connect(rhost, rport) elif nio_settings["type"] == "nio_generic_ethernet": ethernet_device = nio_settings["ethernet_device"] @@ -462,9 +462,9 @@ class Dynamips(BaseManager): slot_id = int(name[-1]) adapter_name = value adapter = ADAPTER_MATRIX[adapter_name]() - if vm.slots[slot_id] and type(vm.slots[slot_id]) != type(adapter): + if vm.slots[slot_id] and not isinstance(vm.slots[slot_id], type(adapter)): yield from vm.slot_remove_binding(slot_id) - if type(vm.slots[slot_id]) != type(adapter): + if not isinstance(vm.slots[slot_id], type(adapter)): yield from vm.slot_add_binding(slot_id, adapter) elif name.startswith("slot") and value is None: slot_id = int(name[-1]) @@ -474,9 +474,9 @@ class Dynamips(BaseManager): wic_slot_id = int(name[-1]) wic_name = value wic = WIC_MATRIX[wic_name]() - if vm.slots[0].wics[wic_slot_id] and type(vm.slots[0].wics[wic_slot_id]) != type(wic): + if vm.slots[0].wics[wic_slot_id] and not isinstance(vm.slots[0].wics[wic_slot_id], type(wic)): yield from vm.uninstall_wic(wic_slot_id) - if type(vm.slots[0].wics[wic_slot_id]) != type(wic): + if not isinstance(vm.slots[0].wics[wic_slot_id], type(wic)): yield from vm.install_wic(wic_slot_id, wic) elif name.startswith("wic") and value is None: wic_slot_id = int(name[-1]) diff --git a/gns3server/modules/dynamips/nodes/atm_switch.py b/gns3server/modules/dynamips/nodes/atm_switch.py index 32867883..5c620221 100644 --- a/gns3server/modules/dynamips/nodes/atm_switch.py +++ b/gns3server/modules/dynamips/nodes/atm_switch.py @@ -31,6 +31,7 @@ log = logging.getLogger(__name__) class ATMSwitch(Device): + """ Dynamips ATM switch. @@ -79,7 +80,6 @@ class ATMSwitch(Device): new_name=new_name)) self._name = new_name - @property def nios(self): """ diff --git a/gns3server/modules/dynamips/nodes/bridge.py b/gns3server/modules/dynamips/nodes/bridge.py index 5f056101..174fbb86 100644 --- a/gns3server/modules/dynamips/nodes/bridge.py +++ b/gns3server/modules/dynamips/nodes/bridge.py @@ -25,6 +25,7 @@ from .device import Device class Bridge(Device): + """ Dynamips bridge. diff --git a/gns3server/modules/dynamips/nodes/device.py b/gns3server/modules/dynamips/nodes/device.py index cbf755bd..fb5b3595 100644 --- a/gns3server/modules/dynamips/nodes/device.py +++ b/gns3server/modules/dynamips/nodes/device.py @@ -17,6 +17,7 @@ class Device: + """ Base device for switches and hubs diff --git a/gns3server/modules/dynamips/nodes/ethernet_hub.py b/gns3server/modules/dynamips/nodes/ethernet_hub.py index 33c523ab..39a891ab 100644 --- a/gns3server/modules/dynamips/nodes/ethernet_hub.py +++ b/gns3server/modules/dynamips/nodes/ethernet_hub.py @@ -29,6 +29,7 @@ log = logging.getLogger(__name__) class EthernetHub(Bridge): + """ Dynamips Ethernet hub (based on Bridge) diff --git a/gns3server/modules/dynamips/nodes/ethernet_switch.py b/gns3server/modules/dynamips/nodes/ethernet_switch.py index b0e425b8..7a8d8abe 100644 --- a/gns3server/modules/dynamips/nodes/ethernet_switch.py +++ b/gns3server/modules/dynamips/nodes/ethernet_switch.py @@ -31,6 +31,7 @@ log = logging.getLogger(__name__) class EthernetSwitch(Device): + """ Dynamips Ethernet switch. diff --git a/gns3server/modules/dynamips/nodes/frame_relay_switch.py b/gns3server/modules/dynamips/nodes/frame_relay_switch.py index 16572871..74fdda1c 100644 --- a/gns3server/modules/dynamips/nodes/frame_relay_switch.py +++ b/gns3server/modules/dynamips/nodes/frame_relay_switch.py @@ -30,6 +30,7 @@ log = logging.getLogger(__name__) class FrameRelaySwitch(Device): + """ Dynamips Frame Relay switch. diff --git a/gns3server/schemas/dynamips_device.py b/gns3server/schemas/dynamips_device.py index 28ba32be..52a9ba53 100644 --- a/gns3server/schemas/dynamips_device.py +++ b/gns3server/schemas/dynamips_device.py @@ -63,7 +63,7 @@ DEVICE_UPDATE_SCHEMA = { "vlan": {"description": "VLAN number", "type": "integer", "minimum": 1 - }, + }, }, "required": ["port", "type", "vlan"], "additionalProperties": False @@ -108,7 +108,7 @@ DEVICE_OBJECT_SCHEMA = { "vlan": {"description": "VLAN number", "type": "integer", "minimum": 1 - }, + }, }, "required": ["port", "type", "vlan"], "additionalProperties": False @@ -306,7 +306,7 @@ DEVICE_NIO_SCHEMA = { "vlan": {"description": "VLAN number", "type": "integer", "minimum": 1 - }, + }, }, "required": ["type", "vlan"], "additionalProperties": False diff --git a/gns3server/templates/upload.html b/gns3server/templates/upload.html index c912e49d..e9ba8b0b 100644 --- a/gns3server/templates/upload.html +++ b/gns3server/templates/upload.html @@ -7,10 +7,10 @@
- {%if items%} -

Files on {{host}}

- {%for item in items%} -

{{path}}/{{item}}

+ {%if files%} +

Files on {{gns3_host}}

+ {%for file in files%} +

{{image_path}}/{{file}}

{%endfor%} {%endif%} {% endblock %} diff --git a/gns3server/web/response.py b/gns3server/web/response.py index 9bd453c6..42112fa9 100644 --- a/gns3server/web/response.py +++ b/gns3server/web/response.py @@ -46,7 +46,7 @@ class Response(aiohttp.web.Response): log.debug("%s", request.json) log.info("Response: %d %s", self.status, self.reason) log.debug(dict(self.headers)) - if hasattr(self, 'body') and self.body is not None: + if hasattr(self, 'body') and self.body is not None and self.headers["CONTENT-TYPE"] == "application/json": log.debug(json.loads(self.body.decode('utf-8'))) return super().start(request) @@ -90,3 +90,11 @@ class Response(aiohttp.web.Response): log.error("Invalid output query. JSON schema error: {}".format(e.message)) raise aiohttp.web.HTTPBadRequest(text="{}".format(e)) self.body = json.dumps(answer, indent=4, sort_keys=True).encode('utf-8') + + def redirect(self, url): + """ + Redirect to url + + :params url: Redirection URL + """ + raise aiohttp.web.HTTPFound(url) diff --git a/gns3server/web/route.py b/gns3server/web/route.py index 4ec4d2b8..93b10eed 100644 --- a/gns3server/web/route.py +++ b/gns3server/web/route.py @@ -82,6 +82,8 @@ class Route(object): output_schema = kw.get("output", {}) input_schema = kw.get("input", {}) api_version = kw.get("api_version", 1) + + # If it's a JSON api endpoint just register the endpoint an do nothing if api_version is None: cls._path = path else: @@ -107,6 +109,14 @@ class Route(object): @asyncio.coroutine def control_schema(request): # This block is executed at each method call + + # Non API call + if api_version is None: + response = Response(route=route, output_schema=output_schema) + yield from func(request, response) + return response + + # API call try: request = yield from parse_request(request, input_schema) response = Response(route=route, output_schema=output_schema) diff --git a/tests/handlers/api/base.py b/tests/handlers/api/base.py index d0f38878..d7c7f690 100644 --- a/tests/handlers/api/base.py +++ b/tests/handlers/api/base.py @@ -49,7 +49,7 @@ class Query: return "http://{}:{}{}".format(self._host, self._port, path) return "http://{}:{}/v{}{}".format(self._host, self._port, version, path) - def _fetch(self, method, path, body=None, api_version = 1, **kwargs): + def _fetch(self, method, path, body=None, api_version=1, **kwargs): """Fetch an url, parse the JSON and return response Options: diff --git a/tests/handlers/test_upload.py b/tests/handlers/test_upload.py index d53b6302..65869b4f 100644 --- a/tests/handlers/test_upload.py +++ b/tests/handlers/test_upload.py @@ -15,17 +15,33 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -""" -This test suite check /version endpoint -It's also used for unittest the HTTP implementation. -""" + +import aiohttp +import os +from unittest.mock import patch from gns3server.version import __version__ -def test_version_index_upload(server): +def test_index_upload(server): response = server.get('/upload', api_version=None) assert response.status == 200 html = response.html assert "GNS3 Server" in html assert "Select & Upload" in html + + +def test_upload(server, tmpdir): + + with open(str(tmpdir / "test"), "w+") as f: + f.write("TEST") + body = aiohttp.FormData() + body.add_field("file", open(str(tmpdir / "test"), "rb"), content_type="application/iou", filename="test2") + + with patch("gns3server.config.Config.get_section_config", return_value={"image_directory": str(tmpdir)}): + response = server.post('/upload', api_version=None, body=body, raw=True) + + with open(str(tmpdir / "test2")) as f: + assert f.read() == "TEST" + + assert "test2" in response.body.decode("utf-8") From 89b7d62ec6cd5523f8acefc9713bb44f2a4db947 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 23 Feb 2015 17:28:17 +0100 Subject: [PATCH 295/485] Some code cleanup --- gns3server/handlers/upload_handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gns3server/handlers/upload_handler.py b/gns3server/handlers/upload_handler.py index 2f5d9fd4..394450c8 100644 --- a/gns3server/handlers/upload_handler.py +++ b/gns3server/handlers/upload_handler.py @@ -55,8 +55,8 @@ class UploadHandler: try: os.makedirs(UploadHandler.image_directory(), exist_ok=True) with open(destination_path, "wb+") as f: - f.write(data["file"].file.read()) - print(destination_path) + chunk = data["file"].file.read() + f.write(chunk) st = os.stat(destination_path) os.chmod(destination_path, st.st_mode | stat.S_IXUSR) except OSError as e: From 4ffb2c8c2095d364fcbf8893bca3421509dd13f8 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 23 Feb 2015 17:32:55 +0100 Subject: [PATCH 296/485] Fix tests --- gns3server/handlers/upload_handler.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/gns3server/handlers/upload_handler.py b/gns3server/handlers/upload_handler.py index 394450c8..de6809be 100644 --- a/gns3server/handlers/upload_handler.py +++ b/gns3server/handlers/upload_handler.py @@ -35,10 +35,13 @@ class UploadHandler: ) def index(request, response): files = [] - for filename in os.listdir(UploadHandler.image_directory()): - if os.path.isfile(os.path.join(UploadHandler.image_directory(), filename)): - if filename[0] != ".": - files.append(filename) + try: + for filename in os.listdir(UploadHandler.image_directory()): + if os.path.isfile(os.path.join(UploadHandler.image_directory(), filename)): + if filename[0] != ".": + files.append(filename) + except OSError as e: + pass response.template("upload.html", files=files, image_path=UploadHandler.image_directory()) @classmethod From cad708f4ab6009e2eb21503cc25c223bcf21767b Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 23 Feb 2015 18:00:59 +0100 Subject: [PATCH 297/485] Add warning unstable at the beginning of the API --- ...ips_device.rst => api.dynamips_device.rst} | 2 +- .../v1projectsprojectiddynamipsdevices.rst | 0 ...ojectsprojectiddynamipsdevicesdeviceid.rst | 0 ...mipsdevicesdeviceidportsportnumberdnio.rst | 4 +-- ...esdeviceidportsportnumberdstartcapture.rst | 2 +- ...cesdeviceidportsportnumberdstopcapture.rst | 2 +- .../{dynamips_vm.rst => api.dynamips_vm.rst} | 2 +- .../v1projectsprojectiddynamipsvms.rst | 0 .../v1projectsprojectiddynamipsvmsvmid.rst | 2 ++ ...ptersadapternumberdportsportnumberdnio.rst | 4 +-- ...ternumberdportsportnumberdstartcapture.rst | 2 +- ...pternumberdportsportnumberdstopcapture.rst | 2 +- ...ectsprojectiddynamipsvmsvmidautoidlepc.rst | 15 +++++++++++ ...rojectsprojectiddynamipsvmsvmidconfigs.rst | 25 +++++++++++++++++++ ...rojectiddynamipsvmsvmididlepcproposals.rst | 15 +++++++++++ ...projectsprojectiddynamipsvmsvmidreload.rst | 0 ...projectsprojectiddynamipsvmsvmidresume.rst | 0 ...1projectsprojectiddynamipsvmsvmidstart.rst | 0 ...v1projectsprojectiddynamipsvmsvmidstop.rst | 0 ...rojectsprojectiddynamipsvmsvmidsuspend.rst | 0 docs/api/{iou.rst => api.iou.rst} | 2 +- .../v1projectsprojectidiouvms.rst | 0 .../v1projectsprojectidiouvmsvmid.rst | 0 ...ptersadapternumberdportsportnumberdnio.rst | 4 +-- ...ternumberdportsportnumberdstartcapture.rst | 2 +- ...pternumberdportsportnumberdstopcapture.rst | 2 +- ...ojectsprojectidiouvmsvmidinitialconfig.rst | 1 - .../v1projectsprojectidiouvmsvmidreload.rst | 0 .../v1projectsprojectidiouvmsvmidstart.rst | 0 .../v1projectsprojectidiouvmsvmidstop.rst | 0 docs/api/{network.rst => api.network.rst} | 2 +- .../{network => api.network}/v1interfaces.rst | 0 .../{network => api.network}/v1portsudp.rst | 0 docs/api/{project.rst => api.project.rst} | 2 +- .../{project => api.project}/v1projects.rst | 0 .../v1projectsprojectid.rst | 0 .../v1projectsprojectidclose.rst | 0 .../v1projectsprojectidcommit.rst | 0 docs/api/{qemu.rst => api.qemu.rst} | 2 +- .../v1projectsprojectidqemuvms.rst | 0 .../v1projectsprojectidqemuvmsvmid.rst | 0 ...ptersadapternumberdportsportnumberdnio.rst | 4 +-- .../v1projectsprojectidqemuvmsvmidreload.rst | 0 .../v1projectsprojectidqemuvmsvmidresume.rst | 20 +++++++++++++++ .../v1projectsprojectidqemuvmsvmidstart.rst | 0 .../v1projectsprojectidqemuvmsvmidstop.rst | 0 .../v1projectsprojectidqemuvmsvmidsuspend.rst | 2 +- docs/api/api.qemu/v1qemubinaries.rst | 15 +++++++++++ docs/api/{version.rst => api.version.rst} | 2 +- .../{version => api.version}/v1version.rst | 0 .../{virtualbox.rst => api.virtualbox.rst} | 2 +- .../v1projectsprojectidvirtualboxvms.rst | 0 .../v1projectsprojectidvirtualboxvmsvmid.rst | 0 ...ptersadapternumberdportsportnumberdnio.rst | 4 +-- ...ternumberdportsportnumberdstartcapture.rst | 2 +- ...pternumberdportsportnumberdstopcapture.rst | 2 +- ...ojectsprojectidvirtualboxvmsvmidreload.rst | 0 ...ojectsprojectidvirtualboxvmsvmidresume.rst | 0 ...rojectsprojectidvirtualboxvmsvmidstart.rst | 0 ...projectsprojectidvirtualboxvmsvmidstop.rst | 0 ...jectsprojectidvirtualboxvmsvmidsuspend.rst | 0 .../v1virtualboxvms.rst | 0 docs/api/{vpcs.rst => api.vpcs.rst} | 2 +- .../v1projectsprojectidvpcsvms.rst | 0 .../v1projectsprojectidvpcsvmsvmid.rst | 0 ...ptersadapternumberdportsportnumberdnio.rst | 4 +-- .../v1projectsprojectidvpcsvmsvmidreload.rst | 0 .../v1projectsprojectidvpcsvmsvmidstart.rst | 0 .../v1projectsprojectidvpcsvmsvmidstop.rst | 0 docs/api/upload.rst | 8 ++++++ docs/api/upload/upload.rst | 22 ++++++++++++++++ docs/index.rst | 3 +++ gns3server/web/documentation.py | 2 +- 73 files changed, 156 insertions(+), 32 deletions(-) rename docs/api/{dynamips_device.rst => api.dynamips_device.rst} (75%) rename docs/api/{dynamips_device => api.dynamips_device}/v1projectsprojectiddynamipsdevices.rst (100%) rename docs/api/{dynamips_device => api.dynamips_device}/v1projectsprojectiddynamipsdevicesdeviceid.rst (100%) rename docs/api/{dynamips_device => api.dynamips_device}/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst (100%) rename docs/api/{dynamips_device => api.dynamips_device}/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst (100%) rename docs/api/{dynamips_device => api.dynamips_device}/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst (100%) rename docs/api/{dynamips_vm.rst => api.dynamips_vm.rst} (78%) rename docs/api/{dynamips_vm => api.dynamips_vm}/v1projectsprojectiddynamipsvms.rst (100%) rename docs/api/{dynamips_vm => api.dynamips_vm}/v1projectsprojectiddynamipsvmsvmid.rst (98%) rename docs/api/{dynamips_vm => api.dynamips_vm}/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst (100%) rename docs/api/{dynamips_vm => api.dynamips_vm}/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst (100%) rename docs/api/{dynamips_vm => api.dynamips_vm}/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst (100%) create mode 100644 docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidautoidlepc.rst create mode 100644 docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidconfigs.rst create mode 100644 docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmididlepcproposals.rst rename docs/api/{dynamips_vm => api.dynamips_vm}/v1projectsprojectiddynamipsvmsvmidreload.rst (100%) rename docs/api/{dynamips_vm => api.dynamips_vm}/v1projectsprojectiddynamipsvmsvmidresume.rst (100%) rename docs/api/{dynamips_vm => api.dynamips_vm}/v1projectsprojectiddynamipsvmsvmidstart.rst (100%) rename docs/api/{dynamips_vm => api.dynamips_vm}/v1projectsprojectiddynamipsvmsvmidstop.rst (100%) rename docs/api/{dynamips_vm => api.dynamips_vm}/v1projectsprojectiddynamipsvmsvmidsuspend.rst (100%) rename docs/api/{iou.rst => api.iou.rst} (83%) rename docs/api/{iou => api.iou}/v1projectsprojectidiouvms.rst (100%) rename docs/api/{iou => api.iou}/v1projectsprojectidiouvmsvmid.rst (100%) rename docs/api/{iou => api.iou}/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst (100%) rename docs/api/{iou => api.iou}/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst (100%) rename docs/api/{iou => api.iou}/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst (100%) rename docs/api/{iou => api.iou}/v1projectsprojectidiouvmsvmidinitialconfig.rst (81%) rename docs/api/{iou => api.iou}/v1projectsprojectidiouvmsvmidreload.rst (100%) rename docs/api/{iou => api.iou}/v1projectsprojectidiouvmsvmidstart.rst (100%) rename docs/api/{iou => api.iou}/v1projectsprojectidiouvmsvmidstop.rst (100%) rename docs/api/{network.rst => api.network.rst} (80%) rename docs/api/{network => api.network}/v1interfaces.rst (100%) rename docs/api/{network => api.network}/v1portsudp.rst (100%) rename docs/api/{project.rst => api.project.rst} (80%) rename docs/api/{project => api.project}/v1projects.rst (100%) rename docs/api/{project => api.project}/v1projectsprojectid.rst (100%) rename docs/api/{project => api.project}/v1projectsprojectidclose.rst (100%) rename docs/api/{project => api.project}/v1projectsprojectidcommit.rst (100%) rename docs/api/{qemu.rst => api.qemu.rst} (82%) rename docs/api/{qemu => api.qemu}/v1projectsprojectidqemuvms.rst (100%) rename docs/api/{qemu => api.qemu}/v1projectsprojectidqemuvmsvmid.rst (100%) rename docs/api/{qemu => api.qemu}/v1projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst (100%) rename docs/api/{qemu => api.qemu}/v1projectsprojectidqemuvmsvmidreload.rst (100%) create mode 100644 docs/api/api.qemu/v1projectsprojectidqemuvmsvmidresume.rst rename docs/api/{qemu => api.qemu}/v1projectsprojectidqemuvmsvmidstart.rst (100%) rename docs/api/{qemu => api.qemu}/v1projectsprojectidqemuvmsvmidstop.rst (100%) rename docs/api/{qemu => api.qemu}/v1projectsprojectidqemuvmsvmidsuspend.rst (96%) create mode 100644 docs/api/api.qemu/v1qemubinaries.rst rename docs/api/{version.rst => api.version.rst} (80%) rename docs/api/{version => api.version}/v1version.rst (100%) rename docs/api/{virtualbox.rst => api.virtualbox.rst} (78%) rename docs/api/{virtualbox => api.virtualbox}/v1projectsprojectidvirtualboxvms.rst (100%) rename docs/api/{virtualbox => api.virtualbox}/v1projectsprojectidvirtualboxvmsvmid.rst (100%) rename docs/api/{virtualbox => api.virtualbox}/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst (100%) rename docs/api/{virtualbox => api.virtualbox}/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst (100%) rename docs/api/{virtualbox => api.virtualbox}/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst (100%) rename docs/api/{virtualbox => api.virtualbox}/v1projectsprojectidvirtualboxvmsvmidreload.rst (100%) rename docs/api/{virtualbox => api.virtualbox}/v1projectsprojectidvirtualboxvmsvmidresume.rst (100%) rename docs/api/{virtualbox => api.virtualbox}/v1projectsprojectidvirtualboxvmsvmidstart.rst (100%) rename docs/api/{virtualbox => api.virtualbox}/v1projectsprojectidvirtualboxvmsvmidstop.rst (100%) rename docs/api/{virtualbox => api.virtualbox}/v1projectsprojectidvirtualboxvmsvmidsuspend.rst (100%) rename docs/api/{virtualbox => api.virtualbox}/v1virtualboxvms.rst (100%) rename docs/api/{vpcs.rst => api.vpcs.rst} (82%) rename docs/api/{vpcs => api.vpcs}/v1projectsprojectidvpcsvms.rst (100%) rename docs/api/{vpcs => api.vpcs}/v1projectsprojectidvpcsvmsvmid.rst (100%) rename docs/api/{vpcs => api.vpcs}/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst (100%) rename docs/api/{vpcs => api.vpcs}/v1projectsprojectidvpcsvmsvmidreload.rst (100%) rename docs/api/{vpcs => api.vpcs}/v1projectsprojectidvpcsvmsvmidstart.rst (100%) rename docs/api/{vpcs => api.vpcs}/v1projectsprojectidvpcsvmsvmidstop.rst (100%) create mode 100644 docs/api/upload.rst create mode 100644 docs/api/upload/upload.rst diff --git a/docs/api/dynamips_device.rst b/docs/api/api.dynamips_device.rst similarity index 75% rename from docs/api/dynamips_device.rst rename to docs/api/api.dynamips_device.rst index 83c17b94..c5cd1ff6 100644 --- a/docs/api/dynamips_device.rst +++ b/docs/api/api.dynamips_device.rst @@ -5,4 +5,4 @@ Dynamips device :glob: :maxdepth: 2 - dynamips_device/* + api.dynamips_device/* diff --git a/docs/api/dynamips_device/v1projectsprojectiddynamipsdevices.rst b/docs/api/api.dynamips_device/v1projectsprojectiddynamipsdevices.rst similarity index 100% rename from docs/api/dynamips_device/v1projectsprojectiddynamipsdevices.rst rename to docs/api/api.dynamips_device/v1projectsprojectiddynamipsdevices.rst diff --git a/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceid.rst b/docs/api/api.dynamips_device/v1projectsprojectiddynamipsdevicesdeviceid.rst similarity index 100% rename from docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceid.rst rename to docs/api/api.dynamips_device/v1projectsprojectiddynamipsdevicesdeviceid.rst diff --git a/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst b/docs/api/api.dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst similarity index 100% rename from docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst rename to docs/api/api.dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst index 0533ad08..706b5c06 100644 --- a/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst +++ b/docs/api/api.dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst @@ -10,8 +10,8 @@ Add a NIO to a Dynamips device instance Parameters ********** - **project_id**: UUID for the project -- **port_number**: Port on the device - **device_id**: UUID for the instance +- **port_number**: Port on the device Response status codes ********************** @@ -129,8 +129,8 @@ Remove a NIO from a Dynamips device instance Parameters ********** - **project_id**: UUID for the project -- **port_number**: Port on the device - **device_id**: UUID for the instance +- **port_number**: Port on the device Response status codes ********************** diff --git a/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst b/docs/api/api.dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst similarity index 100% rename from docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst rename to docs/api/api.dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst index 117cd928..19852f49 100644 --- a/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst +++ b/docs/api/api.dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst @@ -10,8 +10,8 @@ Start a packet capture on a Dynamips device instance Parameters ********** - **project_id**: UUID for the project -- **port_number**: Port on the device - **device_id**: UUID for the instance +- **port_number**: Port on the device Response status codes ********************** diff --git a/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst b/docs/api/api.dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst similarity index 100% rename from docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst rename to docs/api/api.dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst index 9674ef65..cc312e43 100644 --- a/docs/api/dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst +++ b/docs/api/api.dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst @@ -10,8 +10,8 @@ Stop a packet capture on a Dynamips device instance Parameters ********** - **project_id**: UUID for the project -- **port_number**: Port on the device - **device_id**: UUID for the instance +- **port_number**: Port on the device Response status codes ********************** diff --git a/docs/api/dynamips_vm.rst b/docs/api/api.dynamips_vm.rst similarity index 78% rename from docs/api/dynamips_vm.rst rename to docs/api/api.dynamips_vm.rst index f32d26b7..c4851bfe 100644 --- a/docs/api/dynamips_vm.rst +++ b/docs/api/api.dynamips_vm.rst @@ -5,4 +5,4 @@ Dynamips vm :glob: :maxdepth: 2 - dynamips_vm/* + api.dynamips_vm/* diff --git a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvms.rst b/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvms.rst similarity index 100% rename from docs/api/dynamips_vm/v1projectsprojectiddynamipsvms.rst rename to docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvms.rst diff --git a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmid.rst b/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmid.rst similarity index 98% rename from docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmid.rst rename to docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmid.rst index 191d71a0..11b3c065 100644 --- a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmid.rst +++ b/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmid.rst @@ -114,6 +114,7 @@ Input power_supplies array Power supplies status private_config string path to the IOS private configuration file private_config_base64 string private configuration base64 encoded + private_config_content string Content of IOS private configuration file ram integer amount of RAM in MB sensors array Temperature sensors slot0 Network module slot 0 @@ -126,6 +127,7 @@ Input sparsemem boolean sparse memory feature startup_config string path to the IOS startup configuration file startup_config_base64 string startup configuration base64 encoded + startup_config_content string Content of IOS startup configuration file system_id string system ID wic0 Network module WIC slot 0 wic1 Network module WIC slot 0 diff --git a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst similarity index 100% rename from docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst rename to docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst index 25d0a246..c1c114ec 100644 --- a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -10,9 +10,9 @@ Add a NIO to a Dynamips VM instance Parameters ********** - **project_id**: UUID for the project -- **vm_id**: UUID for the instance - **adapter_number**: Adapter where the nio should be added - **port_number**: Port on the adapter +- **vm_id**: UUID for the instance Response status codes ********************** @@ -28,9 +28,9 @@ Remove a NIO from a Dynamips VM instance Parameters ********** - **project_id**: UUID for the project -- **vm_id**: UUID for the instance - **adapter_number**: Adapter from where the nio should be removed - **port_number**: Port on the adapter +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst b/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst similarity index 100% rename from docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst rename to docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst index 0d54a8c6..57271aac 100644 --- a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst +++ b/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst @@ -10,9 +10,9 @@ Start a packet capture on a Dynamips VM instance Parameters ********** - **project_id**: UUID for the project -- **vm_id**: UUID for the instance - **adapter_number**: Adapter to start a packet capture - **port_number**: Port on the adapter +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst b/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst similarity index 100% rename from docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst rename to docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst index f89a083c..c3b44232 100644 --- a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst +++ b/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst @@ -10,9 +10,9 @@ Stop a packet capture on a Dynamips VM instance Parameters ********** - **project_id**: UUID for the project -- **vm_id**: UUID for the instance - **adapter_number**: Adapter to stop a packet capture - **port_number**: Port on the adapter (always 0) +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidautoidlepc.rst b/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidautoidlepc.rst new file mode 100644 index 00000000..24c17864 --- /dev/null +++ b/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidautoidlepc.rst @@ -0,0 +1,15 @@ +/v1/projects/{project_id}/dynamips/vms/{vm_id}/auto_idlepc +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +GET /v1/projects/**{project_id}**/dynamips/vms/**{vm_id}**/auto_idlepc +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Retrieve the idlepc proposals + +Response status codes +********************** +- **200**: Best Idle-pc value found +- **400**: Invalid request +- **404**: Instance doesn't exist + diff --git a/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidconfigs.rst b/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidconfigs.rst new file mode 100644 index 00000000..547b9e25 --- /dev/null +++ b/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidconfigs.rst @@ -0,0 +1,25 @@ +/v1/projects/{project_id}/dynamips/vms/{vm_id}/configs +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +GET /v1/projects/**{project_id}**/dynamips/vms/**{vm_id}**/configs +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Retrieve the startup and private configs content + +Response status codes +********************** +- **200**: Configs retrieved +- **400**: Invalid request +- **404**: Instance doesn't exist + +Output +******* +.. raw:: html + + + + + +
Name Mandatory Type Description
private_config_content ['string', 'null'] Content of the private configuration file
startup_config_content ['string', 'null'] Content of the startup configuration file
+ diff --git a/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmididlepcproposals.rst b/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmididlepcproposals.rst new file mode 100644 index 00000000..18722f8a --- /dev/null +++ b/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmididlepcproposals.rst @@ -0,0 +1,15 @@ +/v1/projects/{project_id}/dynamips/vms/{vm_id}/idlepc_proposals +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +GET /v1/projects/**{project_id}**/dynamips/vms/**{vm_id}**/idlepc_proposals +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Retrieve the idlepc proposals + +Response status codes +********************** +- **200**: Idle-PCs retrieved +- **400**: Invalid request +- **404**: Instance doesn't exist + diff --git a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidreload.rst b/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidreload.rst similarity index 100% rename from docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidreload.rst rename to docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidreload.rst diff --git a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidresume.rst b/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidresume.rst similarity index 100% rename from docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidresume.rst rename to docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidresume.rst diff --git a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidstart.rst b/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidstart.rst similarity index 100% rename from docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidstart.rst rename to docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidstart.rst diff --git a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidstop.rst b/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidstop.rst similarity index 100% rename from docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidstop.rst rename to docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidstop.rst diff --git a/docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidsuspend.rst b/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidsuspend.rst similarity index 100% rename from docs/api/dynamips_vm/v1projectsprojectiddynamipsvmsvmidsuspend.rst rename to docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidsuspend.rst diff --git a/docs/api/iou.rst b/docs/api/api.iou.rst similarity index 83% rename from docs/api/iou.rst rename to docs/api/api.iou.rst index c2188031..a04ef778 100644 --- a/docs/api/iou.rst +++ b/docs/api/api.iou.rst @@ -5,4 +5,4 @@ Iou :glob: :maxdepth: 2 - iou/* + api.iou/* diff --git a/docs/api/iou/v1projectsprojectidiouvms.rst b/docs/api/api.iou/v1projectsprojectidiouvms.rst similarity index 100% rename from docs/api/iou/v1projectsprojectidiouvms.rst rename to docs/api/api.iou/v1projectsprojectidiouvms.rst diff --git a/docs/api/iou/v1projectsprojectidiouvmsvmid.rst b/docs/api/api.iou/v1projectsprojectidiouvmsvmid.rst similarity index 100% rename from docs/api/iou/v1projectsprojectidiouvmsvmid.rst rename to docs/api/api.iou/v1projectsprojectidiouvmsvmid.rst diff --git a/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/api.iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst similarity index 100% rename from docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst rename to docs/api/api.iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst index 890f73bc..643f5a12 100644 --- a/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/api.iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -10,9 +10,9 @@ Add a NIO to a IOU instance Parameters ********** - **project_id**: UUID for the project -- **vm_id**: UUID for the instance - **adapter_number**: Network adapter where the nio is located - **port_number**: Port where the nio should be added +- **vm_id**: UUID for the instance Response status codes ********************** @@ -28,9 +28,9 @@ Remove a NIO from a IOU instance Parameters ********** - **project_id**: UUID for the project -- **vm_id**: UUID for the instance - **adapter_number**: Network adapter where the nio is located - **port_number**: Port from where the nio should be removed +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst b/docs/api/api.iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst similarity index 100% rename from docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst rename to docs/api/api.iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst index 11c808bf..3c31f1ef 100644 --- a/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst +++ b/docs/api/api.iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst @@ -10,9 +10,9 @@ Start a packet capture on a IOU VM instance Parameters ********** - **project_id**: UUID for the project -- **vm_id**: UUID for the instance - **adapter_number**: Adapter to start a packet capture - **port_number**: Port on the adapter +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst b/docs/api/api.iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst similarity index 100% rename from docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst rename to docs/api/api.iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst index 51a784d6..e8da18d1 100644 --- a/docs/api/iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst +++ b/docs/api/api.iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst @@ -10,9 +10,9 @@ Stop a packet capture on a IOU VM instance Parameters ********** - **project_id**: UUID for the project -- **vm_id**: UUID for the instance - **adapter_number**: Adapter to stop a packet capture - **port_number**: Port on the adapter (always 0) +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/iou/v1projectsprojectidiouvmsvmidinitialconfig.rst b/docs/api/api.iou/v1projectsprojectidiouvmsvmidinitialconfig.rst similarity index 81% rename from docs/api/iou/v1projectsprojectidiouvmsvmidinitialconfig.rst rename to docs/api/api.iou/v1projectsprojectidiouvmsvmidinitialconfig.rst index f2cfdbfd..8e56bf25 100644 --- a/docs/api/iou/v1projectsprojectidiouvmsvmidinitialconfig.rst +++ b/docs/api/api.iou/v1projectsprojectidiouvmsvmidinitialconfig.rst @@ -20,6 +20,5 @@ Output -
Name Mandatory Type Description
content ['string', 'null'] Content of the initial configuration file
path ['string', 'null'] Relative path on the server of the initial configuration file
diff --git a/docs/api/iou/v1projectsprojectidiouvmsvmidreload.rst b/docs/api/api.iou/v1projectsprojectidiouvmsvmidreload.rst similarity index 100% rename from docs/api/iou/v1projectsprojectidiouvmsvmidreload.rst rename to docs/api/api.iou/v1projectsprojectidiouvmsvmidreload.rst diff --git a/docs/api/iou/v1projectsprojectidiouvmsvmidstart.rst b/docs/api/api.iou/v1projectsprojectidiouvmsvmidstart.rst similarity index 100% rename from docs/api/iou/v1projectsprojectidiouvmsvmidstart.rst rename to docs/api/api.iou/v1projectsprojectidiouvmsvmidstart.rst diff --git a/docs/api/iou/v1projectsprojectidiouvmsvmidstop.rst b/docs/api/api.iou/v1projectsprojectidiouvmsvmidstop.rst similarity index 100% rename from docs/api/iou/v1projectsprojectidiouvmsvmidstop.rst rename to docs/api/api.iou/v1projectsprojectidiouvmsvmidstop.rst diff --git a/docs/api/network.rst b/docs/api/api.network.rst similarity index 80% rename from docs/api/network.rst rename to docs/api/api.network.rst index 38366abe..886cd394 100644 --- a/docs/api/network.rst +++ b/docs/api/api.network.rst @@ -5,4 +5,4 @@ Network :glob: :maxdepth: 2 - network/* + api.network/* diff --git a/docs/api/network/v1interfaces.rst b/docs/api/api.network/v1interfaces.rst similarity index 100% rename from docs/api/network/v1interfaces.rst rename to docs/api/api.network/v1interfaces.rst diff --git a/docs/api/network/v1portsudp.rst b/docs/api/api.network/v1portsudp.rst similarity index 100% rename from docs/api/network/v1portsudp.rst rename to docs/api/api.network/v1portsudp.rst diff --git a/docs/api/project.rst b/docs/api/api.project.rst similarity index 80% rename from docs/api/project.rst rename to docs/api/api.project.rst index 95453d81..703ef19c 100644 --- a/docs/api/project.rst +++ b/docs/api/api.project.rst @@ -5,4 +5,4 @@ Project :glob: :maxdepth: 2 - project/* + api.project/* diff --git a/docs/api/project/v1projects.rst b/docs/api/api.project/v1projects.rst similarity index 100% rename from docs/api/project/v1projects.rst rename to docs/api/api.project/v1projects.rst diff --git a/docs/api/project/v1projectsprojectid.rst b/docs/api/api.project/v1projectsprojectid.rst similarity index 100% rename from docs/api/project/v1projectsprojectid.rst rename to docs/api/api.project/v1projectsprojectid.rst diff --git a/docs/api/project/v1projectsprojectidclose.rst b/docs/api/api.project/v1projectsprojectidclose.rst similarity index 100% rename from docs/api/project/v1projectsprojectidclose.rst rename to docs/api/api.project/v1projectsprojectidclose.rst diff --git a/docs/api/project/v1projectsprojectidcommit.rst b/docs/api/api.project/v1projectsprojectidcommit.rst similarity index 100% rename from docs/api/project/v1projectsprojectidcommit.rst rename to docs/api/api.project/v1projectsprojectidcommit.rst diff --git a/docs/api/qemu.rst b/docs/api/api.qemu.rst similarity index 82% rename from docs/api/qemu.rst rename to docs/api/api.qemu.rst index 70fd8fc2..553c3953 100644 --- a/docs/api/qemu.rst +++ b/docs/api/api.qemu.rst @@ -5,4 +5,4 @@ Qemu :glob: :maxdepth: 2 - qemu/* + api.qemu/* diff --git a/docs/api/qemu/v1projectsprojectidqemuvms.rst b/docs/api/api.qemu/v1projectsprojectidqemuvms.rst similarity index 100% rename from docs/api/qemu/v1projectsprojectidqemuvms.rst rename to docs/api/api.qemu/v1projectsprojectidqemuvms.rst diff --git a/docs/api/qemu/v1projectsprojectidqemuvmsvmid.rst b/docs/api/api.qemu/v1projectsprojectidqemuvmsvmid.rst similarity index 100% rename from docs/api/qemu/v1projectsprojectidqemuvmsvmid.rst rename to docs/api/api.qemu/v1projectsprojectidqemuvmsvmid.rst diff --git a/docs/api/qemu/v1projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/api.qemu/v1projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst similarity index 100% rename from docs/api/qemu/v1projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst rename to docs/api/api.qemu/v1projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst index 8d0b3733..a5515da9 100644 --- a/docs/api/qemu/v1projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/api.qemu/v1projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -10,9 +10,9 @@ Add a NIO to a Qemu.instance Parameters ********** - **project_id**: UUID for the project -- **vm_id**: UUID for the instance - **adapter_number**: Network adapter where the nio is located - **port_number**: Port where the nio should be added +- **vm_id**: UUID for the instance Response status codes ********************** @@ -28,9 +28,9 @@ Remove a NIO from a Qemu.instance Parameters ********** - **project_id**: UUID for the project -- **vm_id**: UUID for the instance - **adapter_number**: Network adapter where the nio is located - **port_number**: Port from where the nio should be removed +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/qemu/v1projectsprojectidqemuvmsvmidreload.rst b/docs/api/api.qemu/v1projectsprojectidqemuvmsvmidreload.rst similarity index 100% rename from docs/api/qemu/v1projectsprojectidqemuvmsvmidreload.rst rename to docs/api/api.qemu/v1projectsprojectidqemuvmsvmidreload.rst diff --git a/docs/api/api.qemu/v1projectsprojectidqemuvmsvmidresume.rst b/docs/api/api.qemu/v1projectsprojectidqemuvmsvmidresume.rst new file mode 100644 index 00000000..a06a45c2 --- /dev/null +++ b/docs/api/api.qemu/v1projectsprojectidqemuvmsvmidresume.rst @@ -0,0 +1,20 @@ +/v1/projects/{project_id}/qemu/vms/{vm_id}/resume +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/qemu/vms/**{vm_id}**/resume +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Resume a Qemu.instance + +Parameters +********** +- **project_id**: UUID for the project +- **vm_id**: UUID for the instance + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance resumed + diff --git a/docs/api/qemu/v1projectsprojectidqemuvmsvmidstart.rst b/docs/api/api.qemu/v1projectsprojectidqemuvmsvmidstart.rst similarity index 100% rename from docs/api/qemu/v1projectsprojectidqemuvmsvmidstart.rst rename to docs/api/api.qemu/v1projectsprojectidqemuvmsvmidstart.rst diff --git a/docs/api/qemu/v1projectsprojectidqemuvmsvmidstop.rst b/docs/api/api.qemu/v1projectsprojectidqemuvmsvmidstop.rst similarity index 100% rename from docs/api/qemu/v1projectsprojectidqemuvmsvmidstop.rst rename to docs/api/api.qemu/v1projectsprojectidqemuvmsvmidstop.rst diff --git a/docs/api/qemu/v1projectsprojectidqemuvmsvmidsuspend.rst b/docs/api/api.qemu/v1projectsprojectidqemuvmsvmidsuspend.rst similarity index 96% rename from docs/api/qemu/v1projectsprojectidqemuvmsvmidsuspend.rst rename to docs/api/api.qemu/v1projectsprojectidqemuvmsvmidsuspend.rst index c9da38a2..26f4216b 100644 --- a/docs/api/qemu/v1projectsprojectidqemuvmsvmidsuspend.rst +++ b/docs/api/api.qemu/v1projectsprojectidqemuvmsvmidsuspend.rst @@ -5,7 +5,7 @@ POST /v1/projects/**{project_id}**/qemu/vms/**{vm_id}**/suspend ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Reload a Qemu.instance +Suspend a Qemu.instance Parameters ********** diff --git a/docs/api/api.qemu/v1qemubinaries.rst b/docs/api/api.qemu/v1qemubinaries.rst new file mode 100644 index 00000000..4d9494bb --- /dev/null +++ b/docs/api/api.qemu/v1qemubinaries.rst @@ -0,0 +1,15 @@ +/v1/qemu/binaries +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +GET /v1/qemu/binaries +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Get a list of available Qemu binaries + +Response status codes +********************** +- **200**: Success +- **400**: Invalid request +- **404**: Instance doesn't exist + diff --git a/docs/api/version.rst b/docs/api/api.version.rst similarity index 80% rename from docs/api/version.rst rename to docs/api/api.version.rst index adc4c1f0..62427503 100644 --- a/docs/api/version.rst +++ b/docs/api/api.version.rst @@ -5,4 +5,4 @@ Version :glob: :maxdepth: 2 - version/* + api.version/* diff --git a/docs/api/version/v1version.rst b/docs/api/api.version/v1version.rst similarity index 100% rename from docs/api/version/v1version.rst rename to docs/api/api.version/v1version.rst diff --git a/docs/api/virtualbox.rst b/docs/api/api.virtualbox.rst similarity index 78% rename from docs/api/virtualbox.rst rename to docs/api/api.virtualbox.rst index 517624b2..e1700535 100644 --- a/docs/api/virtualbox.rst +++ b/docs/api/api.virtualbox.rst @@ -5,4 +5,4 @@ Virtualbox :glob: :maxdepth: 2 - virtualbox/* + api.virtualbox/* diff --git a/docs/api/virtualbox/v1projectsprojectidvirtualboxvms.rst b/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvms.rst similarity index 100% rename from docs/api/virtualbox/v1projectsprojectidvirtualboxvms.rst rename to docs/api/api.virtualbox/v1projectsprojectidvirtualboxvms.rst diff --git a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmid.rst b/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmid.rst similarity index 100% rename from docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmid.rst rename to docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmid.rst diff --git a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst similarity index 100% rename from docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst rename to docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst index 7150561f..e670257d 100644 --- a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -10,9 +10,9 @@ Add a NIO to a VirtualBox VM instance Parameters ********** - **project_id**: UUID for the project -- **vm_id**: UUID for the instance - **adapter_number**: Adapter where the nio should be added - **port_number**: Port on the adapter (always 0) +- **vm_id**: UUID for the instance Response status codes ********************** @@ -28,9 +28,9 @@ Remove a NIO from a VirtualBox VM instance Parameters ********** - **project_id**: UUID for the project -- **vm_id**: UUID for the instance - **adapter_number**: Adapter from where the nio should be removed - **port_number**: Port on the adapter (always) +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst b/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst similarity index 100% rename from docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst rename to docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst index 402ccc5e..47b4fdca 100644 --- a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst +++ b/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst @@ -10,9 +10,9 @@ Start a packet capture on a VirtualBox VM instance Parameters ********** - **project_id**: UUID for the project -- **vm_id**: UUID for the instance - **adapter_number**: Adapter to start a packet capture - **port_number**: Port on the adapter (always 0) +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst b/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst similarity index 100% rename from docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst rename to docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst index 63e1f22d..057170b0 100644 --- a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst +++ b/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst @@ -10,9 +10,9 @@ Stop a packet capture on a VirtualBox VM instance Parameters ********** - **project_id**: UUID for the project -- **vm_id**: UUID for the instance - **adapter_number**: Adapter to stop a packet capture - **port_number**: Port on the adapter (always 0) +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidreload.rst b/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidreload.rst similarity index 100% rename from docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidreload.rst rename to docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidreload.rst diff --git a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidresume.rst b/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidresume.rst similarity index 100% rename from docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidresume.rst rename to docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidresume.rst diff --git a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidstart.rst b/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidstart.rst similarity index 100% rename from docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidstart.rst rename to docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidstart.rst diff --git a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidstop.rst b/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidstop.rst similarity index 100% rename from docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidstop.rst rename to docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidstop.rst diff --git a/docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidsuspend.rst b/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidsuspend.rst similarity index 100% rename from docs/api/virtualbox/v1projectsprojectidvirtualboxvmsvmidsuspend.rst rename to docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidsuspend.rst diff --git a/docs/api/virtualbox/v1virtualboxvms.rst b/docs/api/api.virtualbox/v1virtualboxvms.rst similarity index 100% rename from docs/api/virtualbox/v1virtualboxvms.rst rename to docs/api/api.virtualbox/v1virtualboxvms.rst diff --git a/docs/api/vpcs.rst b/docs/api/api.vpcs.rst similarity index 82% rename from docs/api/vpcs.rst rename to docs/api/api.vpcs.rst index ab00c921..e4b06735 100644 --- a/docs/api/vpcs.rst +++ b/docs/api/api.vpcs.rst @@ -5,4 +5,4 @@ Vpcs :glob: :maxdepth: 2 - vpcs/* + api.vpcs/* diff --git a/docs/api/vpcs/v1projectsprojectidvpcsvms.rst b/docs/api/api.vpcs/v1projectsprojectidvpcsvms.rst similarity index 100% rename from docs/api/vpcs/v1projectsprojectidvpcsvms.rst rename to docs/api/api.vpcs/v1projectsprojectidvpcsvms.rst diff --git a/docs/api/vpcs/v1projectsprojectidvpcsvmsvmid.rst b/docs/api/api.vpcs/v1projectsprojectidvpcsvmsvmid.rst similarity index 100% rename from docs/api/vpcs/v1projectsprojectidvpcsvmsvmid.rst rename to docs/api/api.vpcs/v1projectsprojectidvpcsvmsvmid.rst diff --git a/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/api.vpcs/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst similarity index 100% rename from docs/api/vpcs/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst rename to docs/api/api.vpcs/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst index 4b84af47..ce72f5d7 100644 --- a/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/api.vpcs/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -10,9 +10,9 @@ Add a NIO to a VPCS instance Parameters ********** - **project_id**: UUID for the project -- **vm_id**: UUID for the instance - **adapter_number**: Network adapter where the nio is located - **port_number**: Port where the nio should be added +- **vm_id**: UUID for the instance Response status codes ********************** @@ -28,9 +28,9 @@ Remove a NIO from a VPCS instance Parameters ********** - **project_id**: UUID for the project -- **vm_id**: UUID for the instance - **adapter_number**: Network adapter where the nio is located - **port_number**: Port from where the nio should be removed +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidreload.rst b/docs/api/api.vpcs/v1projectsprojectidvpcsvmsvmidreload.rst similarity index 100% rename from docs/api/vpcs/v1projectsprojectidvpcsvmsvmidreload.rst rename to docs/api/api.vpcs/v1projectsprojectidvpcsvmsvmidreload.rst diff --git a/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidstart.rst b/docs/api/api.vpcs/v1projectsprojectidvpcsvmsvmidstart.rst similarity index 100% rename from docs/api/vpcs/v1projectsprojectidvpcsvmsvmidstart.rst rename to docs/api/api.vpcs/v1projectsprojectidvpcsvmsvmidstart.rst diff --git a/docs/api/vpcs/v1projectsprojectidvpcsvmsvmidstop.rst b/docs/api/api.vpcs/v1projectsprojectidvpcsvmsvmidstop.rst similarity index 100% rename from docs/api/vpcs/v1projectsprojectidvpcsvmsvmidstop.rst rename to docs/api/api.vpcs/v1projectsprojectidvpcsvmsvmidstop.rst diff --git a/docs/api/upload.rst b/docs/api/upload.rst new file mode 100644 index 00000000..51e901b0 --- /dev/null +++ b/docs/api/upload.rst @@ -0,0 +1,8 @@ +Upload +--------------------- + +.. toctree:: + :glob: + :maxdepth: 2 + + upload/* diff --git a/docs/api/upload/upload.rst b/docs/api/upload/upload.rst new file mode 100644 index 00000000..f716f9fe --- /dev/null +++ b/docs/api/upload/upload.rst @@ -0,0 +1,22 @@ +/upload +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +GET /upload +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Manage upload of GNS3 images + +Response status codes +********************** +- **200**: OK + + +POST /upload +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Manage upload of GNS3 images + +Response status codes +********************** +- **200**: OK + diff --git a/docs/index.rst b/docs/index.rst index 74292e8a..35f652b5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,6 +1,9 @@ Welcome to API documentation! ====================================== +.. WARNING:: + The API is not stable, feel free to send comment on GNS3 Jungle + https://community.gns3.com/ .. toctree:: general diff --git a/gns3server/web/documentation.py b/gns3server/web/documentation.py index f2032c9b..fd972529 100644 --- a/gns3server/web/documentation.py +++ b/gns3server/web/documentation.py @@ -77,7 +77,7 @@ class Documentation(object): os.makedirs(directory, exist_ok=True) with open("docs/api/{}.rst".format(handler_name), "w+") as f: - f.write(handler_name.replace("_", " ", ).capitalize()) + f.write(handler_name.replace("api.", "").replace("_", " ", ).capitalize()) f.write("\n---------------------\n\n") f.write(".. toctree::\n :glob:\n :maxdepth: 2\n\n {}/*\n".format(handler_name)) From 4d1f08c96e9e7d8b30519fa449c470ff827a7b4d Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 23 Feb 2015 20:21:00 +0100 Subject: [PATCH 298/485] Turn off Qemu graphics if no display is available Fixes #66 --- gns3server/modules/qemu/qemu_vm.py | 17 ++++++++++++++- tests/modules/qemu/test_qemu_vm.py | 33 ++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index eb4ab118..829bbfe7 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -915,13 +915,17 @@ class QemuVM(BaseVM): return options + def _get_random_mac(self, adapter_id): + # TODO: let users specify a base mac address + return "00:00:ab:%02x:%02x:%02d" % (random.randint(0x00, 0xff), random.randint(0x00, 0xff), adapter_id) + def _network_options(self): network_options = [] adapter_id = 0 for adapter in self._ethernet_adapters: # TODO: let users specify a base mac address - mac = "00:00:ab:%02x:%02x:%02d" % (random.randint(0x00, 0xff), random.randint(0x00, 0xff), adapter_id) + mac = self._get_random_mac(adapter_id) network_options.extend(["-net", "nic,vlan={},macaddr={},model={}".format(adapter_id, mac, self._adapter_type)]) nio = adapter.get_nio(0) if nio and isinstance(nio, NIO_UDP): @@ -944,6 +948,16 @@ class QemuVM(BaseVM): return network_options + def _graphic(self): + """ + Add the correct graphic options depending of the OS + """ + if sys.platform.startswith("win"): + return [] + if len(os.environ.get("DISPLAY", "")) > 0: + return [] + return ["-nographic"] + @asyncio.coroutine def _build_command(self): """ @@ -963,6 +977,7 @@ class QemuVM(BaseVM): if additional_options: command.extend(shlex.split(additional_options)) command.extend(self._network_options()) + command.extend(self._graphic()) return command def __json__(self): diff --git a/tests/modules/qemu/test_qemu_vm.py b/tests/modules/qemu/test_qemu_vm.py index a174f7fd..fe0f8cd4 100644 --- a/tests/modules/qemu/test_qemu_vm.py +++ b/tests/modules/qemu/test_qemu_vm.py @@ -233,3 +233,36 @@ def test_control_vm_expect_text(vm, loop): assert writer.write.called_with("test") assert res == "epic product" + + +def test_build_command(vm, loop, fake_qemu_binary): + + os.environ["DISPLAY"] = "0:0" + with patch("gns3server.modules.qemu.qemu_vm.QemuVM._get_random_mac", return_value="00:00:ab:7e:b5:00"): + with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()) as process: + cmd = loop.run_until_complete(asyncio.async(vm._build_command())) + assert cmd == [ + fake_qemu_binary, + "-name", + "test", + "-m", + "256", + "-hda", + os.path.join(vm.working_dir, "flash.qcow2"), + "-serial", + "telnet:0.0.0.0:{},server,nowait".format(vm.console), + "-monitor", + "telnet:0.0.0.0:{},server,nowait".format(vm.monitor), + "-net", + "nic,vlan=0,macaddr=00:00:ab:7e:b5:00,model=e1000", + "-net", + "user,vlan=0,name=gns3-0" + ] + + +def test_build_command_without_display(vm, loop, fake_qemu_binary): + + os.environ["DISPLAY"] = "" + with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()) as process: + cmd = loop.run_until_complete(asyncio.async(vm._build_command())) + assert "-nographic" in cmd From 0e8c1849870898a2a108e3ee2d3688acebc745cd Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 23 Feb 2015 15:49:05 -0700 Subject: [PATCH 299/485] Recursive listing of the images directory & fixes bug when uploading no files. --- gns3server/handlers/upload_handler.py | 21 ++++++++++----------- gns3server/templates/upload.html | 2 +- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/gns3server/handlers/upload_handler.py b/gns3server/handlers/upload_handler.py index de6809be..1d904d65 100644 --- a/gns3server/handlers/upload_handler.py +++ b/gns3server/handlers/upload_handler.py @@ -20,9 +20,6 @@ import stat from ..config import Config from ..web.route import Route -from ..schemas.version import VERSION_SCHEMA -from ..version import __version__ -from aiohttp.web import HTTPConflict class UploadHandler: @@ -34,15 +31,14 @@ class UploadHandler: api_version=None ) def index(request, response): - files = [] + image_files = [] try: - for filename in os.listdir(UploadHandler.image_directory()): - if os.path.isfile(os.path.join(UploadHandler.image_directory(), filename)): - if filename[0] != ".": - files.append(filename) - except OSError as e: + for root, _, files in os.walk(UploadHandler.image_directory()): + for filename in files: + image_files.append(os.path.join(root, filename)) + except OSError: pass - response.template("upload.html", files=files, image_path=UploadHandler.image_directory()) + response.template("upload.html", files=image_files) @classmethod @Route.post( @@ -53,8 +49,11 @@ class UploadHandler: def upload(request, response): data = yield from request.post() - destination_path = os.path.join(UploadHandler.image_directory(), data["file"].filename) + if not data["file"]: + response.redirect("/upload") + return + destination_path = os.path.join(UploadHandler.image_directory(), data["file"].filename) try: os.makedirs(UploadHandler.image_directory(), exist_ok=True) with open(destination_path, "wb+") as f: diff --git a/gns3server/templates/upload.html b/gns3server/templates/upload.html index e9ba8b0b..0cd9fbcc 100644 --- a/gns3server/templates/upload.html +++ b/gns3server/templates/upload.html @@ -10,7 +10,7 @@ {%if files%}

Files on {{gns3_host}}

{%for file in files%} -

{{image_path}}/{{file}}

+

{{file}}

{%endfor%} {%endif%} {% endblock %} From 8b19029d9740c04e0d93ca26bbf08b883fc958da Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 23 Feb 2015 15:56:10 -0700 Subject: [PATCH 300/485] List only executable files in upload handler. --- gns3server/handlers/upload_handler.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gns3server/handlers/upload_handler.py b/gns3server/handlers/upload_handler.py index 1d904d65..2d89e081 100644 --- a/gns3server/handlers/upload_handler.py +++ b/gns3server/handlers/upload_handler.py @@ -35,7 +35,9 @@ class UploadHandler: try: for root, _, files in os.walk(UploadHandler.image_directory()): for filename in files: - image_files.append(os.path.join(root, filename)) + image_file = os.path.join(root, filename) + if os.access(image_file, os.X_OK): + image_files.append(os.path.join(root, filename)) except OSError: pass response.template("upload.html", files=image_files) From fb9f5d3c1416093d8ba351e4b8963d38874d4f28 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 23 Feb 2015 15:56:40 -0700 Subject: [PATCH 301/485] List only executable files in upload handler. --- gns3server/handlers/upload_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/handlers/upload_handler.py b/gns3server/handlers/upload_handler.py index 2d89e081..429963a6 100644 --- a/gns3server/handlers/upload_handler.py +++ b/gns3server/handlers/upload_handler.py @@ -37,7 +37,7 @@ class UploadHandler: for filename in files: image_file = os.path.join(root, filename) if os.access(image_file, os.X_OK): - image_files.append(os.path.join(root, filename)) + image_files.append(image_file) except OSError: pass response.template("upload.html", files=image_files) From 182d2e465e05ec60c657748d24baf422b25624f8 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 23 Feb 2015 17:08:34 -0700 Subject: [PATCH 302/485] Use projects_path & images_path. --- gns3server/handlers/upload_handler.py | 2 +- gns3server/modules/project.py | 3 ++- gns3server/server.py | 8 +++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/gns3server/handlers/upload_handler.py b/gns3server/handlers/upload_handler.py index 429963a6..0df82392 100644 --- a/gns3server/handlers/upload_handler.py +++ b/gns3server/handlers/upload_handler.py @@ -72,4 +72,4 @@ class UploadHandler: @staticmethod def image_directory(): server_config = Config.instance().get_section_config("Server") - return os.path.expanduser(server_config.get("image_directory", "~/GNS3/images")) + return os.path.expanduser(server_config.get("images_path", "~/GNS3/images")) diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index 6aa4bf6f..11e20421 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -91,7 +91,8 @@ class Project: depending of the operating system """ - path = os.path.normpath(os.path.expanduser("~/GNS3/projects")) + server_config = Config.instance().get_section_config("Server") + path = os.path.expanduser(server_config.get("projects_path", "~/GNS3/projects")) try: os.makedirs(path, exist_ok=True) except OSError as e: diff --git a/gns3server/server.py b/gns3server/server.py index 7e47a307..cb8d5b7f 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -142,9 +142,11 @@ class Server: @asyncio.coroutine def start_shell(self): - from ptpython.repl import embed - from gns3server.modules import Qemu - + try: + from ptpython.repl import embed + except ImportError: + log.error("Unable to start a shell: the ptpython module must be installed!") + return yield from embed(globals(), locals(), return_asyncio_coroutine=True, patch_stdout=True) def run(self): From 3d3300e83a9c2b5c3ed7e2193ad28875abd90d2c Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 23 Feb 2015 17:42:55 -0700 Subject: [PATCH 303/485] Rename console methods in port manager to use the generic tcp term in the name. Fixes bug when a console port is allocated to a Ghost VM instance and not released. Warnings at exit when TCP/UDP ports are still allocated. --- gns3server/modules/base_vm.py | 8 +++--- gns3server/modules/dynamips/nodes/router.py | 20 +++++++------ gns3server/modules/iou/iou_vm.py | 2 +- gns3server/modules/port_manager.py | 28 +++++++++++++++---- gns3server/modules/qemu/qemu_vm.py | 8 +++--- .../modules/virtualbox/virtualbox_vm.py | 2 +- gns3server/modules/vpcs/vpcs_vm.py | 2 +- gns3server/server.py | 7 +++++ tests/conftest.py | 4 +-- tests/modules/iou/test_iou_vm.py | 2 +- tests/modules/qemu/test_qemu_vm.py | 4 +-- tests/modules/test_port_manager.py | 6 ++-- tests/modules/vpcs/test_vpcs_vm.py | 12 ++++---- 13 files changed, 66 insertions(+), 39 deletions(-) diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index 6a3f29c1..67db308a 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -47,9 +47,9 @@ class BaseVM: self._console = console if self._console is not None: - self._console = self._manager.port_manager.reserve_console_port(self._console) + self._console = self._manager.port_manager.reserve_tcp_port(self._console) else: - self._console = self._manager.port_manager.get_free_console_port() + self._console = self._manager.port_manager.get_free_tcp_port() log.debug("{module}: {name} [{id}] initialized. Console port {console}".format( module=self.manager.module_name, @@ -188,8 +188,8 @@ class BaseVM: if console == self._console: return if self._console: - self._manager.port_manager.release_console_port(self._console) - self._console = self._manager.port_manager.reserve_console_port(console) + self._manager.port_manager.release_tcp_port(self._console) + self._console = self._manager.port_manager.reserve_tcp_port(console) log.info("{module}: '{name}' [{id}]: console port set to {port}".format( module=self.manager.module_name, name=self.name, diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index a94f4af5..eb16c275 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -106,11 +106,15 @@ class Router(BaseVM): self._dynamips_ids[project.id].append(self._dynamips_id) if self._aux is not None: - self._aux = self._manager.port_manager.reserve_console_port(self._aux) + self._aux = self._manager.port_manager.reserve_tcp_port(self._aux) else: - self._aux = self._manager.port_manager.get_free_console_port() + self._aux = self._manager.port_manager.get_free_tcp_port() else: log.info("Creating a new ghost IOS instance") + if self._console: + # Ghost VMs do not need a console port. + self._manager.port_manager.release_tcp_port(self._console) + self._console = None self._dynamips_id = 0 self._name = "Ghost" @@ -320,11 +324,11 @@ class Router(BaseVM): yield from self.hypervisor.stop() if self._console: - self._manager.port_manager.release_console_port(self._console) + self._manager.port_manager.release_tcp_port(self._console) self._console = None if self._aux: - self._manager.port_manager.release_console_port(self._aux) + self._manager.port_manager.release_tcp_port(self._aux) self._aux = None self._closed = True @@ -873,8 +877,8 @@ class Router(BaseVM): old_console=self._console, new_console=console)) - self._manager.port_manager.release_console_port(self._console) - self._console = self._manager.port_manager.reserve_console_port(console) + self._manager.port_manager.release_tcp_port(self._console) + self._console = self._manager.port_manager.reserve_tcp_port(console) @property def aux(self): @@ -901,8 +905,8 @@ class Router(BaseVM): old_aux=self._aux, new_aux=aux)) - self._manager.port_manager.release_console_port(self._aux) - self._aux = self._manager.port_manager.reserve_console_port(aux) + self._manager.port_manager.release_tcp_port(self._aux) + self._aux = self._manager.port_manager.reserve_tcp_port(aux) @asyncio.coroutine def get_cpu_usage(self, cpu_id=0): diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 7751cd3e..1ab6302d 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -106,7 +106,7 @@ class IOUVM(BaseVM): yield from self.stop() if self._console: - self._manager.port_manager.release_console_port(self._console) + self._manager.port_manager.release_tcp_port(self._console) self._console = None @property diff --git a/gns3server/modules/port_manager.py b/gns3server/modules/port_manager.py index da448cd6..79b8b6eb 100644 --- a/gns3server/modules/port_manager.py +++ b/gns3server/modules/port_manager.py @@ -116,6 +116,16 @@ class PortManager: assert isinstance(new_range, tuple) self._udp_port_range = new_range + @property + def tcp_ports(self): + + return self._used_tcp_ports + + @property + def udp_ports(self): + + return self._used_udp_ports + @staticmethod def find_unused_port(start_port, end_port, host="127.0.0.1", socket_type="TCP", ignore_ports=[]): """ @@ -163,9 +173,9 @@ class PortManager: host, last_exception)) - def get_free_console_port(self): + def get_free_tcp_port(self): """ - Get an available TCP console port and reserve it + Get an available TCP port and reserve it """ port = self.find_unused_port(self._console_port_range[0], @@ -175,11 +185,12 @@ class PortManager: ignore_ports=self._used_tcp_ports) self._used_tcp_ports.add(port) + log.debug("TCP port {} has been allocated".format(port)) return port - def reserve_console_port(self, port): + def reserve_tcp_port(self, port): """ - Reserve a specific TCP console port number + Reserve a specific TCP port number :param port: TCP port number """ @@ -187,17 +198,19 @@ class PortManager: if port in self._used_tcp_ports: raise HTTPConflict(text="TCP port {} already in use on host".format(port, self._console_host)) self._used_tcp_ports.add(port) + log.debug("TCP port {} has been reserved".format(port)) return port - def release_console_port(self, port): + def release_tcp_port(self, port): """ - Release a specific TCP console port number + Release a specific TCP port number :param port: TCP port number """ if port in self._used_tcp_ports: self._used_tcp_ports.remove(port) + log.debug("TCP port {} has been released".format(port)) def get_free_udp_port(self): """ @@ -211,6 +224,7 @@ class PortManager: ignore_ports=self._used_udp_ports) self._used_udp_ports.add(port) + log.debug("UDP port {} has been allocated".format(port)) return port def reserve_udp_port(self, port): @@ -223,6 +237,7 @@ class PortManager: if port in self._used_udp_ports: raise HTTPConflict(text="UDP port {} already in use on host".format(port, self._console_host)) self._used_udp_ports.add(port) + log.debug("UDP port {} has been reserved".format(port)) def release_udp_port(self, port): """ @@ -233,3 +248,4 @@ class PortManager: if port in self._used_udp_ports: self._used_udp_ports.remove(port) + log.debug("UDP port {} has been released".format(port)) diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index 829bbfe7..0e89a4ef 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -97,9 +97,9 @@ class QemuVM(BaseVM): self._process_priority = "low" if self._monitor is not None: - self._monitor = self._manager.port_manager.reserve_console_port(self._monitor) + self._monitor = self._manager.port_manager.reserve_tcp_port(self._monitor) else: - self._monitor = self._manager.port_manager.get_free_console_port() + self._monitor = self._manager.port_manager.get_free_tcp_port() self.adapters = 1 # creates 1 adapter by default log.info("QEMU VM {name} [id={id}] has been created".format(name=self._name, @@ -649,10 +649,10 @@ class QemuVM(BaseVM): yield from self.stop() if self._console: - self._manager.port_manager.release_console_port(self._console) + self._manager.port_manager.release_tcp_port(self._console) self._console = None if self._monitor: - self._manager.port_manager.release_console_port(self._monitor) + self._manager.port_manager.release_tcp_port(self._monitor) self._monitor = None @asyncio.coroutine diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 708aa6ec..4b77b090 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -299,7 +299,7 @@ class VirtualBoxVM(BaseVM): yield from self.stop() if self._console: - self._manager.port_manager.release_console_port(self._console) + self._manager.port_manager.release_tcp_port(self._console) self._console = None if self._linked_clone: diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index 6bd5a4ba..52c1736e 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -72,7 +72,7 @@ class VPCSVM(BaseVM): self._terminate_process() if self._console: - self._manager.port_manager.release_console_port(self._console) + self._manager.port_manager.release_tcp_port(self._console) self._console = None @asyncio.coroutine diff --git a/gns3server/server.py b/gns3server/server.py index cb8d5b7f..13b76f3d 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -72,6 +72,13 @@ class Server: log.debug("Unloading module {}".format(module.__name__)) m = module.instance() yield from m.unload() + + if self._port_manager.tcp_ports: + log.warning("TCP ports are still used {}".format(self._port_manager.tcp_ports)) + + if self._port_manager.udp_ports: + log.warning("UDP ports are still used {}".format(self._port_manager.udp_ports)) + self._loop.stop() def _signal_handling(self, handler): diff --git a/tests/conftest.py b/tests/conftest.py index 51b5cee6..a64d9535 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -116,10 +116,10 @@ def free_console_port(request, port_manager): """Get a free TCP port""" # In case of already use ports we will raise an exception - port = port_manager.get_free_console_port() + port = port_manager.get_free_tcp_port() # We release the port immediately in order to allow # the test do whatever the test want - port_manager.release_console_port(port) + port_manager.release_tcp_port(port) return port diff --git a/tests/modules/iou/test_iou_vm.py b/tests/modules/iou/test_iou_vm.py index 6b1f976f..4ee16327 100644 --- a/tests/modules/iou/test_iou_vm.py +++ b/tests/modules/iou/test_iou_vm.py @@ -165,7 +165,7 @@ def test_close(vm, port_manager, loop): port = vm.console loop.run_until_complete(asyncio.async(vm.close())) # Raise an exception if the port is not free - port_manager.reserve_console_port(port) + port_manager.reserve_tcp_port(port) assert vm.is_running() is False diff --git a/tests/modules/qemu/test_qemu_vm.py b/tests/modules/qemu/test_qemu_vm.py index fe0f8cd4..1cc9596e 100644 --- a/tests/modules/qemu/test_qemu_vm.py +++ b/tests/modules/qemu/test_qemu_vm.py @@ -138,9 +138,9 @@ def test_close(vm, port_manager, loop): loop.run_until_complete(asyncio.async(vm.close())) # Raise an exception if the port is not free - port_manager.reserve_console_port(console_port) + port_manager.reserve_tcp_port(console_port) # Raise an exception if the port is not free - port_manager.reserve_console_port(monitor_port) + port_manager.reserve_tcp_port(monitor_port) assert vm.is_running() is False diff --git a/tests/modules/test_port_manager.py b/tests/modules/test_port_manager.py index 06735272..7b2f9193 100644 --- a/tests/modules/test_port_manager.py +++ b/tests/modules/test_port_manager.py @@ -20,8 +20,8 @@ import pytest from gns3server.modules.port_manager import PortManager -def test_reserve_console_port(): +def test_reserve_tcp_port(): pm = PortManager() - pm.reserve_console_port(4242) + pm.reserve_tcp_port(4242) with pytest.raises(aiohttp.web.HTTPConflict): - pm.reserve_console_port(4242) + pm.reserve_tcp_port(4242) diff --git a/tests/modules/vpcs/test_vpcs_vm.py b/tests/modules/vpcs/test_vpcs_vm.py index 990a386e..0ac186f7 100644 --- a/tests/modules/vpcs/test_vpcs_vm.py +++ b/tests/modules/vpcs/test_vpcs_vm.py @@ -191,14 +191,14 @@ def test_get_startup_script_using_default_script(vm): def test_change_console_port(vm, port_manager): - port1 = port_manager.get_free_console_port() - port2 = port_manager.get_free_console_port() - port_manager.release_console_port(port1) - port_manager.release_console_port(port2) + port1 = port_manager.get_free_tcp_port() + port2 = port_manager.get_free_tcp_port() + port_manager.release_tcp_port(port1) + port_manager.release_tcp_port(port2) vm.console = port1 vm.console = port2 assert vm.console == port2 - port_manager.reserve_console_port(port1) + port_manager.reserve_tcp_port(port1) def test_change_name(vm, tmpdir): @@ -219,5 +219,5 @@ def test_close(vm, port_manager, loop): port = vm.console loop.run_until_complete(asyncio.async(vm.close())) # Raise an exception if the port is not free - port_manager.reserve_console_port(port) + port_manager.reserve_tcp_port(port) assert vm.is_running() is False From 42c07cee1ab9617553ada73832990cad46b1a436 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 23 Feb 2015 19:00:34 -0700 Subject: [PATCH 304/485] Properly release UDP ports when closing a project or deleting a link. --- gns3server/modules/base_manager.py | 12 +- gns3server/modules/dynamips/__init__.py | 7 - .../modules/dynamips/dynamips_hypervisor.py | 29 ---- .../modules/dynamips/nios/nio_udp_auto.py | 140 ------------------ .../modules/dynamips/nodes/atm_switch.py | 7 + .../modules/dynamips/nodes/ethernet_hub.py | 7 + .../modules/dynamips/nodes/ethernet_switch.py | 7 + .../dynamips/nodes/frame_relay_switch.py | 8 + gns3server/modules/dynamips/nodes/router.py | 25 +++- gns3server/modules/iou/iou_vm.py | 24 ++- .../modules/nios/nio_generic_ethernet.py | 2 +- gns3server/modules/nios/nio_tap.py | 2 +- gns3server/modules/nios/nio_udp.py | 2 +- gns3server/modules/qemu/qemu_vm.py | 6 +- .../modules/virtualbox/virtualbox_vm.py | 13 +- gns3server/modules/vpcs/vpcs_vm.py | 15 +- 16 files changed, 96 insertions(+), 210 deletions(-) delete mode 100644 gns3server/modules/dynamips/nios/nio_udp_auto.py diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index d379a59b..acf9c1ab 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -32,9 +32,9 @@ from ..config import Config from ..utils.asyncio import wait_run_in_executor from .project_manager import ProjectManager -from .nios.nio_udp import NIO_UDP -from .nios.nio_tap import NIO_TAP -from .nios.nio_generic_ethernet import NIO_GenericEthernet +from .nios.nio_udp import NIOUDP +from .nios.nio_tap import NIOTAP +from .nios.nio_generic_ethernet import NIOGenericEthernet class BaseManager: @@ -283,13 +283,13 @@ class BaseManager: sock.connect((rhost, rport)) except OSError as e: raise aiohttp.web.HTTPInternalServerError(text="Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e)) - nio = NIO_UDP(lport, rhost, rport) + nio = NIOUDP(lport, rhost, rport) elif nio_settings["type"] == "nio_tap": tap_device = nio_settings["tap_device"] if not self._has_privileged_access(executable): raise aiohttp.web.HTTPForbidden(text="{} has no privileged access to {}.".format(executable, tap_device)) - nio = NIO_TAP(tap_device) + nio = NIOTAP(tap_device) elif nio_settings["type"] == "nio_generic_ethernet": - nio = NIO_GenericEthernet(nio_settings["ethernet_device"]) + nio = NIOGenericEthernet(nio_settings["ethernet_device"]) assert nio is not None return nio diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index d8f70ab6..e01fc388 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -46,7 +46,6 @@ from .dynamips_device import DynamipsDevice # NIOs from .nios.nio_udp import NIOUDP -from .nios.nio_udp_auto import NIOUDPAuto from .nios.nio_unix import NIOUNIX from .nios.nio_vde import NIOVDE from .nios.nio_tap import NIOTAP @@ -355,13 +354,7 @@ class Dynamips(BaseManager): sock.connect((rhost, rport)) except OSError as e: raise DynamipsError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e)) - # check if we have an allocated NIO UDP auto - #nio = node.hypervisor.get_nio_udp_auto(lport) - # if not nio: - # otherwise create an NIO UDP nio = NIOUDP(node.hypervisor, lport, rhost, rport) - # else: - # nio.connect(rhost, rport) elif nio_settings["type"] == "nio_generic_ethernet": ethernet_device = nio_settings["ethernet_device"] if sys.platform.startswith("win"): diff --git a/gns3server/modules/dynamips/dynamips_hypervisor.py b/gns3server/modules/dynamips/dynamips_hypervisor.py index b6c9304f..4895b9fe 100644 --- a/gns3server/modules/dynamips/dynamips_hypervisor.py +++ b/gns3server/modules/dynamips/dynamips_hypervisor.py @@ -25,7 +25,6 @@ import logging import asyncio from .dynamips_error import DynamipsError -from .nios.nio_udp_auto import NIOUDPAuto log = logging.getLogger(__name__) @@ -53,7 +52,6 @@ class DynamipsHypervisor: self._devices = [] self._working_dir = working_dir - self._nio_udp_auto_instances = {} self._version = "N/A" self._timeout = timeout self._uuid = None @@ -130,7 +128,6 @@ class DynamipsHypervisor: except OSError as e: log.debug("Stopping hypervisor {}:{} {}".format(self._host, self._port, e)) self._reader = self._writer = None - self._nio_udp_auto_instances.clear() @asyncio.coroutine def reset(self): @@ -139,7 +136,6 @@ class DynamipsHypervisor: """ yield from self.send("hypervisor reset") - self._nio_udp_auto_instances.clear() @asyncio.coroutine def set_working_dir(self, working_dir): @@ -224,31 +220,6 @@ class DynamipsHypervisor: self._host = host - def get_nio_udp_auto(self, port): - """ - Returns an allocated NIO UDP auto instance. - - :returns: NIO UDP auto instance - """ - - if port in self._nio_udp_auto_instances: - return self._nio_udp_auto_instances.pop(port) - else: - return None - - def allocate_udp_port(self): - """ - Allocates a new UDP port for creating an UDP NIO Auto. - - :returns: port number (integer) - """ - - # use Dynamips's NIO UDP auto back-end. - nio = NIOUDPAuto(self, self._host, self._udp_start_port_range, self._udp_end_port_range) - self._nio_udp_auto_instances[nio.lport] = nio - allocated_port = nio.lport - return allocated_port - @asyncio.coroutine def send(self, command): """ diff --git a/gns3server/modules/dynamips/nios/nio_udp_auto.py b/gns3server/modules/dynamips/nios/nio_udp_auto.py deleted file mode 100644 index a7757199..00000000 --- a/gns3server/modules/dynamips/nios/nio_udp_auto.py +++ /dev/null @@ -1,140 +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 . - -""" -Interface for automatic UDP NIOs. -""" - -import asyncio -from .nio import NIO - -import logging -log = logging.getLogger(__name__) - - -class NIOUDPAuto(NIO): - - """ - Dynamips auto UDP NIO. - - :param hypervisor: Dynamips hypervisor instance - :param laddr: local address - :param lport_start: start local port range - :param lport_end: end local port range - """ - - _instance_count = 0 - - def __init__(self, hypervisor, laddr, lport_start, lport_end): - - # create an unique ID and name - nio_id = NIOUDPAuto._instance_count - NIOUDPAuto._instance_count += 1 - name = 'nio_udp_auto' + str(nio_id) - self._laddr = laddr - self._lport = None - self._raddr = None - self._rport = None - NIO.__init__(self, name, hypervisor) - - @classmethod - def reset(cls): - """ - Reset the instance count. - """ - - cls._instance_count = 0 - - @asyncio.coroutine - def create(self): - - port = yield from self._hypervisor.send("nio create_udp_auto {name} {laddr} {lport_start} {lport_end}".format(name=self._name, - laddr=self._laddr, - lport_start=self._lport_start, - lport_end=self._lport_end)) - self._lport = int(port[0]) - - log.info("NIO UDP AUTO {name} created with laddr={laddr}, lport_start={start}, lport_end={end}".format(name=self._name, - laddr=self._laddr, - start=self._lport_start, - end=self._lport_end)) - - @property - def laddr(self): - """ - Returns the local address - - :returns: local address - """ - - return self._laddr - - @property - def lport(self): - """ - Returns the local port - - :returns: local port number - """ - - return self._lport - - @property - def raddr(self): - """ - Returns the remote address - - :returns: remote address - """ - - return self._raddr - - @property - def rport(self): - """ - Returns the remote port - - :returns: remote port number - """ - - return self._rport - - @asyncio.coroutine - def connect(self, raddr, rport): - """ - Connects this NIO to a remote socket - - :param raddr: remote address - :param rport: remote port number - """ - - yield from self._hypervisor.send("nio connect_udp_auto {name} {raddr} {rport}".format(name=self._name, - raddr=raddr, - rport=rport)) - self._raddr = raddr - self._rport = rport - - log.info("NIO UDP AUTO {name} connected to {raddr}:{rport}".format(name=self._name, - raddr=raddr, - rport=rport)) - - def __json__(self): - - return {"type": "nio_udp_auto", - "lport": self._lport, - "rport": self._rport, - "raddr": self._raddr} diff --git a/gns3server/modules/dynamips/nodes/atm_switch.py b/gns3server/modules/dynamips/nodes/atm_switch.py index 5c620221..36b8f343 100644 --- a/gns3server/modules/dynamips/nodes/atm_switch.py +++ b/gns3server/modules/dynamips/nodes/atm_switch.py @@ -24,6 +24,7 @@ import asyncio import re from .device import Device +from ..nios.nio_udp import NIOUDP from ..dynamips_error import DynamipsError import logging @@ -106,6 +107,10 @@ class ATMSwitch(Device): Deletes this ATM switch. """ + for nio in self._nios.values(): + if nio and isinstance(nio, NIOUDP): + self.manager.port_manager.release_udp_port(nio.lport) + try: yield from self._hypervisor.send('atmsw delete "{}"'.format(self._name)) log.info('ATM switch "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id)) @@ -155,6 +160,8 @@ class ATMSwitch(Device): raise DynamipsError("Port {} is not allocated".format(port_number)) nio = self._nios[port_number] + if isinstance(nio, NIOUDP): + self.manager.port_manager.release_udp_port(nio.lport) log.info('ATM switch "{name}" [{id}]: NIO {nio} removed from port {port}'.format(name=self._name, id=self._id, nio=nio, diff --git a/gns3server/modules/dynamips/nodes/ethernet_hub.py b/gns3server/modules/dynamips/nodes/ethernet_hub.py index 39a891ab..33807c97 100644 --- a/gns3server/modules/dynamips/nodes/ethernet_hub.py +++ b/gns3server/modules/dynamips/nodes/ethernet_hub.py @@ -22,6 +22,7 @@ Hub object that uses the Bridge interface to create a hub with ports. import asyncio from .bridge import Bridge +from ..nios.nio_udp import NIOUDP from ..dynamips_error import DynamipsError import logging @@ -73,6 +74,10 @@ class EthernetHub(Bridge): Deletes this hub. """ + for nio in self._nios.values(): + if nio and isinstance(nio, NIOUDP): + self.manager.port_manager.release_udp_port(nio.lport) + try: yield from Bridge.delete(self) log.info('Ethernet hub "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id)) @@ -115,6 +120,8 @@ class EthernetHub(Bridge): raise DynamipsError("Port {} is not allocated".format(port_number)) nio = self._mappings[port_number] + if isinstance(nio, NIOUDP): + self.manager.port_manager.release_udp_port(nio.lport) yield from Bridge.remove_nio(self, nio) log.info('Ethernet switch "{name}" [{id}]: NIO {nio} removed from port {port}'.format(name=self._name, diff --git a/gns3server/modules/dynamips/nodes/ethernet_switch.py b/gns3server/modules/dynamips/nodes/ethernet_switch.py index 7a8d8abe..1f3abdbe 100644 --- a/gns3server/modules/dynamips/nodes/ethernet_switch.py +++ b/gns3server/modules/dynamips/nodes/ethernet_switch.py @@ -23,6 +23,7 @@ http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L558 import asyncio from .device import Device +from ..nios.nio_udp import NIOUDP from ..dynamips_error import DynamipsError @@ -114,6 +115,10 @@ class EthernetSwitch(Device): Deletes this Ethernet switch. """ + for nio in self._nios.values(): + if nio and isinstance(nio, NIOUDP): + self.manager.port_manager.release_udp_port(nio.lport) + try: yield from self._hypervisor.send('ethsw delete "{}"'.format(self._name)) log.info('Ethernet switch "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id)) @@ -157,6 +162,8 @@ class EthernetSwitch(Device): raise DynamipsError("Port {} is not allocated".format(port_number)) nio = self._nios[port_number] + if isinstance(nio, NIOUDP): + self.manager.port_manager.release_udp_port(nio.lport) yield from self._hypervisor.send('ethsw remove_nio "{name}" {nio}'.format(name=self._name, nio=nio)) log.info('Ethernet switch "{name}" [{id}]: NIO {nio} removed from port {port}'.format(name=self._name, diff --git a/gns3server/modules/dynamips/nodes/frame_relay_switch.py b/gns3server/modules/dynamips/nodes/frame_relay_switch.py index 74fdda1c..d30578be 100644 --- a/gns3server/modules/dynamips/nodes/frame_relay_switch.py +++ b/gns3server/modules/dynamips/nodes/frame_relay_switch.py @@ -23,6 +23,7 @@ http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L642 import asyncio from .device import Device +from ..nios.nio_udp import NIOUDP from ..dynamips_error import DynamipsError import logging @@ -105,6 +106,10 @@ class FrameRelaySwitch(Device): Deletes this Frame Relay switch. """ + for nio in self._nios.values(): + if nio and isinstance(nio, NIOUDP): + self.manager.port_manager.release_udp_port(nio.lport) + try: yield from self._hypervisor.send('frsw delete "{}"'.format(self._name)) log.info('Frame Relay switch "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id)) @@ -156,6 +161,9 @@ class FrameRelaySwitch(Device): raise DynamipsError("Port {} is not allocated".format(port_number)) nio = self._nios[port_number] + if isinstance(nio, NIOUDP): + self.manager.port_manager.release_udp_port(nio.lport) + log.info('Frame Relay switch "{name}" [{id}]: NIO {nio} removed from port {port}'.format(name=self._name, id=self._id, nio=nio, diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index eb16c275..7b456fed 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -32,6 +32,7 @@ log = logging.getLogger(__name__) from ...base_vm import BaseVM from ..dynamips_error import DynamipsError +from ..nios.nio_udp import NIOUDP from gns3server.utils.asyncio import wait_run_in_executor @@ -312,6 +313,20 @@ class Router(BaseVM): if self._dynamips_id in self._dynamips_ids[self._project.id]: self._dynamips_ids[self._project.id].remove(self._dynamips_id) + if self._console: + self._manager.port_manager.release_tcp_port(self._console) + self._console = None + + if self._aux: + self._manager.port_manager.release_tcp_port(self._aux) + self._aux = None + + for adapter in self._slots: + if adapter is not None: + for nio in adapter.ports.values(): + if nio and isinstance(nio, NIOUDP): + self.manager.port_manager.release_udp_port(nio.lport) + if self in self._hypervisor.devices: self._hypervisor.devices.remove(self) if self._hypervisor and not self._hypervisor.devices: @@ -323,14 +338,6 @@ class Router(BaseVM): pass yield from self.hypervisor.stop() - if self._console: - self._manager.port_manager.release_tcp_port(self._console) - self._console = None - - if self._aux: - self._manager.port_manager.release_tcp_port(self._aux) - self._aux = None - self._closed = True @property @@ -1226,6 +1233,8 @@ class Router(BaseVM): port_number=port_number)) nio = adapter.get_nio(port_number) + if isinstance(nio, NIOUDP): + self.manager.port_manager.release_udp_port(nio.lport) adapter.remove_nio(port_number) log.info('Router "{name}" [{id}]: NIO {nio_name} removed from port {slot_number}/{port_number}'.format(name=self._name, diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 1ab6302d..79682479 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -34,9 +34,9 @@ import glob from .iou_error import IOUError from ..adapters.ethernet_adapter import EthernetAdapter from ..adapters.serial_adapter import SerialAdapter -from ..nios.nio_udp import NIO_UDP -from ..nios.nio_tap import NIO_TAP -from ..nios.nio_generic_ethernet import NIO_GenericEthernet +from ..nios.nio_udp import NIOUDP +from ..nios.nio_tap import NIOTAP +from ..nios.nio_generic_ethernet import NIOGenericEthernet from ..base_vm import BaseVM from .ioucon import start_ioucon import gns3server.utils.asyncio @@ -104,11 +104,19 @@ class IOUVM(BaseVM): @asyncio.coroutine def close(self): - yield from self.stop() if self._console: self._manager.port_manager.release_tcp_port(self._console) self._console = None + adapters = self._ethernet_adapters + self._serial_adapters + for adapter in adapters: + if adapter is not None: + for nio in adapter.ports.values(): + if nio and isinstance(nio, NIOUDP): + self.manager.port_manager.release_udp_port(nio.lport) + + yield from self.stop() + @property def path(self): """Path of the iou binary""" @@ -410,16 +418,16 @@ class IOUVM(BaseVM): nio = adapter.get_nio(unit) if nio: connection = None - if isinstance(nio, NIO_UDP): + if isinstance(nio, NIOUDP): # UDP tunnel connection = {"tunnel_udp": "{lport}:{rhost}:{rport}".format(lport=nio.lport, rhost=nio.rhost, rport=nio.rport)} - elif isinstance(nio, NIO_TAP): + elif isinstance(nio, NIOTAP): # TAP interface connection = {"tap_dev": "{tap_device}".format(tap_device=nio.tap_device)} - elif isinstance(nio, NIO_GenericEthernet): + elif isinstance(nio, NIOGenericEthernet): # Ethernet interface connection = {"eth_dev": "{ethernet_device}".format(ethernet_device=nio.ethernet_device)} @@ -750,6 +758,8 @@ class IOUVM(BaseVM): port_number=port_number)) nio = adapter.get_nio(port_number) + if isinstance(nio, NIOUDP): + self.manager.port_manager.release_udp_port(nio.lport) adapter.remove_nio(port_number) log.info("IOU {name} [id={id}]: {nio} removed from {adapter_number}/{port_number}".format(name=self._name, id=self._id, diff --git a/gns3server/modules/nios/nio_generic_ethernet.py b/gns3server/modules/nios/nio_generic_ethernet.py index 98dc91ca..da729565 100644 --- a/gns3server/modules/nios/nio_generic_ethernet.py +++ b/gns3server/modules/nios/nio_generic_ethernet.py @@ -22,7 +22,7 @@ Interface for generic Ethernet NIOs (PCAP library). from .nio import NIO -class NIO_GenericEthernet(NIO): +class NIOGenericEthernet(NIO): """ Generic Ethernet NIO. diff --git a/gns3server/modules/nios/nio_tap.py b/gns3server/modules/nios/nio_tap.py index a63a72c3..9f51ce13 100644 --- a/gns3server/modules/nios/nio_tap.py +++ b/gns3server/modules/nios/nio_tap.py @@ -22,7 +22,7 @@ Interface for TAP NIOs (UNIX based OSes only). from .nio import NIO -class NIO_TAP(NIO): +class NIOTAP(NIO): """ TAP NIO. diff --git a/gns3server/modules/nios/nio_udp.py b/gns3server/modules/nios/nio_udp.py index 4af43cd6..a87875fe 100644 --- a/gns3server/modules/nios/nio_udp.py +++ b/gns3server/modules/nios/nio_udp.py @@ -22,7 +22,7 @@ Interface for UDP NIOs. from .nio import NIO -class NIO_UDP(NIO): +class NIOUDP(NIO): """ UDP NIO. diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index 0e89a4ef..54ae3e4c 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -29,7 +29,7 @@ import asyncio from .qemu_error import QemuError from ..adapters.ethernet_adapter import EthernetAdapter -from ..nios.nio_udp import NIO_UDP +from ..nios.nio_udp import NIOUDP from ..base_vm import BaseVM from ...schemas.qemu import QEMU_OBJECT_SCHEMA @@ -719,7 +719,7 @@ class QemuVM(BaseVM): if self.is_running(): # dynamically configure an UDP tunnel on the QEMU VM adapter - if nio and isinstance(nio, NIO_UDP): + if nio and isinstance(nio, NIOUDP): if self._legacy_networking: yield from self._control_vm("host_net_remove {} gns3-{}".format(adapter_id, adapter_id)) yield from self._control_vm("host_net_add udp vlan={},name=gns3-{},sport={},dport={},daddr={}".format(adapter_id, @@ -928,7 +928,7 @@ class QemuVM(BaseVM): mac = self._get_random_mac(adapter_id) network_options.extend(["-net", "nic,vlan={},macaddr={},model={}".format(adapter_id, mac, self._adapter_type)]) nio = adapter.get_nio(0) - if nio and isinstance(nio, NIO_UDP): + if nio and isinstance(nio, NIOUDP): if self._legacy_networking: network_options.extend(["-net", "udp,vlan={},name=gns3-{},sport={},dport={},daddr={}".format(adapter_id, adapter_id, diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 4b77b090..a7728f94 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -30,6 +30,7 @@ import asyncio from pkg_resources import parse_version from .virtualbox_error import VirtualBoxError +from ..nios.nio_udp import NIOUDP from ..adapters.ethernet_adapter import EthernetAdapter from .telnet_server import TelnetServer # TODO: port TelnetServer to asyncio from ..base_vm import BaseVM @@ -296,12 +297,18 @@ class VirtualBoxVM(BaseVM): # VM is already closed return - yield from self.stop() - if self._console: self._manager.port_manager.release_tcp_port(self._console) self._console = None + for adapter in self._ethernet_adapters: + if adapter is not None: + for nio in adapter.ports.values(): + if nio and isinstance(nio, NIOUDP): + self.manager.port_manager.release_udp_port(nio.lport) + + yield from self.stop() + if self._linked_clone: hdd_table = [] if os.path.exists(self.working_dir): @@ -781,7 +788,7 @@ class VirtualBoxVM(BaseVM): yield from self._control_vm("nic{} null".format(adapter_number + 1)) nio = adapter.get_nio(0) - if str(nio) == "NIO UDP": + if isinstance(nio, NIOUDP): self.manager.port_manager.release_udp_port(nio.lport) adapter.remove_nio(0) diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index 52c1736e..6549ebbc 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -31,6 +31,8 @@ import shutil from pkg_resources import parse_version from .vpcs_error import VPCSError from ..adapters.ethernet_adapter import EthernetAdapter +from ..nios.nio_udp import NIOUDP +from ..nios.nio_tap import NIOTAP from ..base_vm import BaseVM from ...utils.asyncio import subprocess_check_output @@ -70,11 +72,16 @@ class VPCSVM(BaseVM): @asyncio.coroutine def close(self): - self._terminate_process() if self._console: self._manager.port_manager.release_tcp_port(self._console) self._console = None + nio = self._ethernet_adapter.get_nio(0) + if isinstance(nio, NIOUDP): + self.manager.port_manager.release_udp_port(nio.lport) + + self._terminate_process() + @asyncio.coroutine def _check_requirements(self): """ @@ -310,7 +317,7 @@ class VPCSVM(BaseVM): port_number=port_number)) nio = self._ethernet_adapter.get_nio(port_number) - if str(nio) == "NIO UDP": + if isinstance(nio, NIOUDP): self.manager.port_manager.release_udp_port(nio.lport) self._ethernet_adapter.remove_nio(port_number) @@ -361,13 +368,13 @@ class VPCSVM(BaseVM): nio = self._ethernet_adapter.get_nio(0) if nio: - if str(nio) == "NIO UDP": + if isinstance(nio, NIOUDP): # UDP tunnel command.extend(["-s", str(nio.lport)]) # source UDP port command.extend(["-c", str(nio.rport)]) # destination UDP port command.extend(["-t", nio.rhost]) # destination host - elif str(nio) == "NIO TAP": + elif isinstance(nio, NIOTAP): # TAP interface command.extend(["-e"]) command.extend(["-d", nio.tap_vm]) From 49f3c9295fda394f615832736e53bd8e17d9b0eb Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 23 Feb 2015 19:59:19 -0700 Subject: [PATCH 305/485] Some debug messages to help with port allocation debugging. --- gns3server/modules/project.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index 11e20421..31c4f088 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -21,6 +21,7 @@ import shutil import asyncio from uuid import UUID, uuid4 +from .port_manager import PortManager from ..config import Config from ..utils.asyncio import wait_run_in_executor @@ -280,6 +281,13 @@ class Project: except OSError as e: raise aiohttp.web.HTTPInternalServerError(text="Could not delete the project directory: {}".format(e)) + port_manager = PortManager.instance() + if port_manager.tcp_ports: + log.debug("TCP ports still in use: {}".format(port_manager.tcp_ports)) + + if port_manager.udp_ports: + log.warning("UDP ports still in use: {}".format(port_manager.udp_ports)) + @asyncio.coroutine def commit(self): """Write project changes on disk""" From e910167a85e8db4f1f8f2713a0656c1991e6ab1a Mon Sep 17 00:00:00 2001 From: Jeremy Grossmann Date: Mon, 23 Feb 2015 22:19:03 -0700 Subject: [PATCH 306/485] Quick change warning -> debug --- gns3server/modules/project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index 31c4f088..52aea2db 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -286,7 +286,7 @@ class Project: log.debug("TCP ports still in use: {}".format(port_manager.tcp_ports)) if port_manager.udp_ports: - log.warning("UDP ports still in use: {}".format(port_manager.udp_ports)) + log.debug("UDP ports still in use: {}".format(port_manager.udp_ports)) @asyncio.coroutine def commit(self): From 1ca445d5f53360e0cd2ee39ede9c8472df23b297 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 24 Feb 2015 10:02:06 +0100 Subject: [PATCH 307/485] Fix dynampis resume API --- gns3server/handlers/api/dynamips_vm_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/handlers/api/dynamips_vm_handler.py b/gns3server/handlers/api/dynamips_vm_handler.py index 79e7c4c0..c17ceb71 100644 --- a/gns3server/handlers/api/dynamips_vm_handler.py +++ b/gns3server/handlers/api/dynamips_vm_handler.py @@ -204,7 +204,7 @@ class DynamipsVMHandler: 404: "Instance doesn't exist" }, description="Resume a suspended Dynamips VM instance") - def suspend(request, response): + def resume(request, response): dynamips_manager = Dynamips.instance() vm = dynamips_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) From fd03b36258f287923690ea94f5dbd8faec244043 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 24 Feb 2015 10:07:22 +0100 Subject: [PATCH 308/485] Fix tests --- tests/handlers/test_upload.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/handlers/test_upload.py b/tests/handlers/test_upload.py index 65869b4f..cec27948 100644 --- a/tests/handlers/test_upload.py +++ b/tests/handlers/test_upload.py @@ -38,7 +38,7 @@ def test_upload(server, tmpdir): body = aiohttp.FormData() body.add_field("file", open(str(tmpdir / "test"), "rb"), content_type="application/iou", filename="test2") - with patch("gns3server.config.Config.get_section_config", return_value={"image_directory": str(tmpdir)}): + with patch("gns3server.config.Config.get_section_config", return_value={"images_path": str(tmpdir)}): response = server.post('/upload', api_version=None, body=body, raw=True) with open(str(tmpdir / "test2")) as f: From 67be24a4126233cb15ca6be6e77c5bed5d9b0667 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 24 Feb 2015 11:38:57 +0100 Subject: [PATCH 309/485] Fix doc generation --- .../api/examples/delete_projectsprojectid.txt | 13 +++++ .../delete_projectsprojectidiouvmsvmid.txt | 13 +++++ ...ptersadapternumberdportsportnumberdnio.txt | 13 +++++ .../delete_projectsprojectidqemuvmsvmid.txt | 13 +++++ ...ptersadapternumberdportsportnumberdnio.txt | 13 +++++ ...ptersadapternumberdportsportnumberdnio.txt | 13 +++++ .../delete_projectsprojectidvpcsvmsvmid.txt | 13 +++++ ...ptersadapternumberdportsportnumberdnio.txt | 13 +++++ docs/api/examples/get_interfaces.txt | 52 +++++++++++++++++++ docs/api/examples/get_projectsprojectid.txt | 20 +++++++ .../get_projectsprojectidiouvmsvmid.txt | 27 ++++++++++ ...ojectsprojectidiouvmsvmidinitialconfig.txt | 17 ++++++ .../get_projectsprojectidqemuvmsvmid.txt | 34 ++++++++++++ ...get_projectsprojectidvirtualboxvmsvmid.txt | 26 ++++++++++ .../get_projectsprojectidvpcsvmsvmid.txt | 21 ++++++++ docs/api/examples/get_qemubinaries.txt | 24 +++++++++ docs/api/examples/get_version.txt | 17 ++++++ docs/api/examples/post_portsudp.txt | 17 ++++++ docs/api/examples/post_projects.txt | 20 +++++++ .../examples/post_projectsprojectidclose.txt | 13 +++++ .../examples/post_projectsprojectidcommit.txt | 13 +++++ .../examples/post_projectsprojectidiouvms.txt | 36 +++++++++++++ ...ptersadapternumberdportsportnumberdnio.txt | 21 ++++++++ ...ternumberdportsportnumberdstartcapture.txt | 20 +++++++ ...pternumberdportsportnumberdstopcapture.txt | 13 +++++ ...post_projectsprojectidiouvmsvmidreload.txt | 13 +++++ .../post_projectsprojectidiouvmsvmidstart.txt | 13 +++++ .../post_projectsprojectidiouvmsvmidstop.txt | 13 +++++ .../post_projectsprojectidqemuvms.txt | 39 ++++++++++++++ ...ptersadapternumberdportsportnumberdnio.txt | 21 ++++++++ ...ost_projectsprojectidqemuvmsvmidreload.txt | 13 +++++ ...ost_projectsprojectidqemuvmsvmidresume.txt | 13 +++++ ...post_projectsprojectidqemuvmsvmidstart.txt | 13 +++++ .../post_projectsprojectidqemuvmsvmidstop.txt | 13 +++++ ...st_projectsprojectidqemuvmsvmidsuspend.txt | 13 +++++ .../post_projectsprojectidvirtualboxvms.txt | 30 +++++++++++ ...ptersadapternumberdportsportnumberdnio.txt | 25 +++++++++ ...ojectsprojectidvirtualboxvmsvmidreload.txt | 13 +++++ ...ojectsprojectidvirtualboxvmsvmidresume.txt | 13 +++++ ...rojectsprojectidvirtualboxvmsvmidstart.txt | 13 +++++ ...projectsprojectidvirtualboxvmsvmidstop.txt | 13 +++++ ...jectsprojectidvirtualboxvmsvmidsuspend.txt | 13 +++++ .../post_projectsprojectidvpcsvms.txt | 23 ++++++++ ...ptersadapternumberdportsportnumberdnio.txt | 25 +++++++++ ...ost_projectsprojectidvpcsvmsvmidreload.txt | 13 +++++ ...post_projectsprojectidvpcsvmsvmidstart.txt | 13 +++++ .../post_projectsprojectidvpcsvmsvmidstop.txt | 13 +++++ docs/api/examples/post_version.txt | 19 +++++++ docs/api/examples/put_projectsprojectid.txt | 20 +++++++ .../put_projectsprojectidiouvmsvmid.txt | 36 +++++++++++++ .../put_projectsprojectidqemuvmsvmid.txt | 39 ++++++++++++++ ...put_projectsprojectidvirtualboxvmsvmid.txt | 29 +++++++++++ .../put_projectsprojectidvpcsvmsvmid.txt | 25 +++++++++ docs/api/upload.rst | 8 --- docs/api/upload/upload.rst | 22 -------- .../dynamips_device.rst} | 2 +- .../projectsprojectiddynamipsdevices.rst} | 0 ...jectsprojectiddynamipsdevicesdeviceid.rst} | 0 ...ipsdevicesdeviceidportsportnumberdnio.rst} | 4 +- ...sdeviceidportsportnumberdstartcapture.rst} | 2 +- ...esdeviceidportsportnumberdstopcapture.rst} | 2 +- .../dynamips_vm.rst} | 2 +- .../projectsprojectiddynamipsvms.rst} | 0 .../projectsprojectiddynamipsvmsvmid.rst} | 0 ...tersadapternumberdportsportnumberdnio.rst} | 4 +- ...ernumberdportsportnumberdstartcapture.rst} | 2 +- ...ternumberdportsportnumberdstopcapture.rst} | 2 +- ...ctsprojectiddynamipsvmsvmidautoidlepc.rst} | 0 ...ojectsprojectiddynamipsvmsvmidconfigs.rst} | 0 ...ojectiddynamipsvmsvmididlepcproposals.rst} | 0 ...rojectsprojectiddynamipsvmsvmidreload.rst} | 0 ...rojectsprojectiddynamipsvmsvmidresume.rst} | 0 ...projectsprojectiddynamipsvmsvmidstart.rst} | 0 .../projectsprojectiddynamipsvmsvmidstop.rst} | 0 ...ojectsprojectiddynamipsvmsvmidsuspend.rst} | 0 docs/api/{api.iou.rst => v1/iou.rst} | 2 +- .../iou/projectsprojectidiouvms.rst} | 6 +++ .../iou/projectsprojectidiouvmsvmid.rst} | 18 +++++++ ...tersadapternumberdportsportnumberdnio.rst} | 16 +++++- ...ernumberdportsportnumberdstartcapture.rst} | 8 ++- ...ternumberdportsportnumberdstopcapture.rst} | 8 ++- ...jectsprojectidiouvmsvmidinitialconfig.rst} | 6 +++ .../projectsprojectidiouvmsvmidreload.rst} | 6 +++ .../iou/projectsprojectidiouvmsvmidstart.rst} | 6 +++ .../iou/projectsprojectidiouvmsvmidstop.rst} | 6 +++ docs/api/{api.network.rst => v1/network.rst} | 2 +- .../network/interfaces.rst} | 6 +++ .../network/portsudp.rst} | 6 +++ docs/api/{api.project.rst => v1/project.rst} | 2 +- .../project/projects.rst} | 6 +++ .../project/projectsprojectid.rst} | 18 +++++++ .../project/projectsprojectidclose.rst} | 6 +++ .../project/projectsprojectidcommit.rst} | 6 +++ docs/api/{api.qemu.rst => v1/qemu.rst} | 2 +- .../qemu/projectsprojectidqemuvms.rst} | 6 +++ .../qemu/projectsprojectidqemuvmsvmid.rst} | 18 +++++++ ...tersadapternumberdportsportnumberdnio.rst} | 16 +++++- .../projectsprojectidqemuvmsvmidreload.rst} | 6 +++ .../projectsprojectidqemuvmsvmidresume.rst} | 6 +++ .../projectsprojectidqemuvmsvmidstart.rst} | 6 +++ .../projectsprojectidqemuvmsvmidstop.rst} | 6 +++ .../projectsprojectidqemuvmsvmidsuspend.rst} | 6 +++ .../qemu/qemubinaries.rst} | 6 +++ docs/api/{api.version.rst => v1/version.rst} | 2 +- .../v1version.rst => v1/version/version.rst} | 12 +++++ .../{api.virtualbox.rst => v1/virtualbox.rst} | 2 +- .../projectsprojectidvirtualboxvms.rst} | 6 +++ .../projectsprojectidvirtualboxvmsvmid.rst} | 12 +++++ ...tersadapternumberdportsportnumberdnio.rst} | 16 +++++- ...ernumberdportsportnumberdstartcapture.rst} | 2 +- ...ternumberdportsportnumberdstopcapture.rst} | 2 +- ...jectsprojectidvirtualboxvmsvmidreload.rst} | 6 +++ ...jectsprojectidvirtualboxvmsvmidresume.rst} | 6 +++ ...ojectsprojectidvirtualboxvmsvmidstart.rst} | 6 +++ ...rojectsprojectidvirtualboxvmsvmidstop.rst} | 6 +++ ...ectsprojectidvirtualboxvmsvmidsuspend.rst} | 6 +++ .../virtualbox/virtualboxvms.rst} | 0 docs/api/{api.vpcs.rst => v1/vpcs.rst} | 2 +- .../vpcs/projectsprojectidvpcsvms.rst} | 6 +++ .../vpcs/projectsprojectidvpcsvmsvmid.rst} | 18 +++++++ ...tersadapternumberdportsportnumberdnio.rst} | 16 +++++- .../projectsprojectidvpcsvmsvmidreload.rst} | 6 +++ .../projectsprojectidvpcsvmsvmidstart.rst} | 6 +++ .../projectsprojectidvpcsvmsvmidstop.rst} | 6 +++ docs/index.rst | 2 +- gns3server/web/documentation.py | 38 +++++++++----- gns3server/web/route.py | 5 +- scripts/documentation.sh | 2 +- tests/handlers/api/test_iou.py | 10 ++-- tests/handlers/api/test_project.py | 2 +- tests/handlers/api/test_qemu.py | 16 +++--- tests/handlers/api/test_virtualbox.py | 13 ++--- tests/handlers/api/test_vpcs.py | 11 ++-- tests/web/test_documentation.py | 40 ++++++++++++++ 134 files changed, 1477 insertions(+), 102 deletions(-) create mode 100644 docs/api/examples/delete_projectsprojectid.txt create mode 100644 docs/api/examples/delete_projectsprojectidiouvmsvmid.txt create mode 100644 docs/api/examples/delete_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt create mode 100644 docs/api/examples/delete_projectsprojectidqemuvmsvmid.txt create mode 100644 docs/api/examples/delete_projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.txt create mode 100644 docs/api/examples/delete_projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.txt create mode 100644 docs/api/examples/delete_projectsprojectidvpcsvmsvmid.txt create mode 100644 docs/api/examples/delete_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt create mode 100644 docs/api/examples/get_interfaces.txt create mode 100644 docs/api/examples/get_projectsprojectid.txt create mode 100644 docs/api/examples/get_projectsprojectidiouvmsvmid.txt create mode 100644 docs/api/examples/get_projectsprojectidiouvmsvmidinitialconfig.txt create mode 100644 docs/api/examples/get_projectsprojectidqemuvmsvmid.txt create mode 100644 docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt create mode 100644 docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt create mode 100644 docs/api/examples/get_qemubinaries.txt create mode 100644 docs/api/examples/get_version.txt create mode 100644 docs/api/examples/post_portsudp.txt create mode 100644 docs/api/examples/post_projects.txt create mode 100644 docs/api/examples/post_projectsprojectidclose.txt create mode 100644 docs/api/examples/post_projectsprojectidcommit.txt create mode 100644 docs/api/examples/post_projectsprojectidiouvms.txt create mode 100644 docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt create mode 100644 docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.txt create mode 100644 docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.txt create mode 100644 docs/api/examples/post_projectsprojectidiouvmsvmidreload.txt create mode 100644 docs/api/examples/post_projectsprojectidiouvmsvmidstart.txt create mode 100644 docs/api/examples/post_projectsprojectidiouvmsvmidstop.txt create mode 100644 docs/api/examples/post_projectsprojectidqemuvms.txt create mode 100644 docs/api/examples/post_projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.txt create mode 100644 docs/api/examples/post_projectsprojectidqemuvmsvmidreload.txt create mode 100644 docs/api/examples/post_projectsprojectidqemuvmsvmidresume.txt create mode 100644 docs/api/examples/post_projectsprojectidqemuvmsvmidstart.txt create mode 100644 docs/api/examples/post_projectsprojectidqemuvmsvmidstop.txt create mode 100644 docs/api/examples/post_projectsprojectidqemuvmsvmidsuspend.txt create mode 100644 docs/api/examples/post_projectsprojectidvirtualboxvms.txt create mode 100644 docs/api/examples/post_projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.txt create mode 100644 docs/api/examples/post_projectsprojectidvirtualboxvmsvmidreload.txt create mode 100644 docs/api/examples/post_projectsprojectidvirtualboxvmsvmidresume.txt create mode 100644 docs/api/examples/post_projectsprojectidvirtualboxvmsvmidstart.txt create mode 100644 docs/api/examples/post_projectsprojectidvirtualboxvmsvmidstop.txt create mode 100644 docs/api/examples/post_projectsprojectidvirtualboxvmsvmidsuspend.txt create mode 100644 docs/api/examples/post_projectsprojectidvpcsvms.txt create mode 100644 docs/api/examples/post_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt create mode 100644 docs/api/examples/post_projectsprojectidvpcsvmsvmidreload.txt create mode 100644 docs/api/examples/post_projectsprojectidvpcsvmsvmidstart.txt create mode 100644 docs/api/examples/post_projectsprojectidvpcsvmsvmidstop.txt create mode 100644 docs/api/examples/post_version.txt create mode 100644 docs/api/examples/put_projectsprojectid.txt create mode 100644 docs/api/examples/put_projectsprojectidiouvmsvmid.txt create mode 100644 docs/api/examples/put_projectsprojectidqemuvmsvmid.txt create mode 100644 docs/api/examples/put_projectsprojectidvirtualboxvmsvmid.txt create mode 100644 docs/api/examples/put_projectsprojectidvpcsvmsvmid.txt delete mode 100644 docs/api/upload.rst delete mode 100644 docs/api/upload/upload.rst rename docs/api/{api.dynamips_device.rst => v1/dynamips_device.rst} (75%) rename docs/api/{api.dynamips_device/v1projectsprojectiddynamipsdevices.rst => v1/dynamips_device/projectsprojectiddynamipsdevices.rst} (100%) rename docs/api/{api.dynamips_device/v1projectsprojectiddynamipsdevicesdeviceid.rst => v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceid.rst} (100%) rename docs/api/{api.dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst => v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst} (100%) rename docs/api/{api.dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst => v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst} (100%) rename docs/api/{api.dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst => v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst} (100%) rename docs/api/{api.dynamips_vm.rst => v1/dynamips_vm.rst} (78%) rename docs/api/{api.dynamips_vm/v1projectsprojectiddynamipsvms.rst => v1/dynamips_vm/projectsprojectiddynamipsvms.rst} (100%) rename docs/api/{api.dynamips_vm/v1projectsprojectiddynamipsvmsvmid.rst => v1/dynamips_vm/projectsprojectiddynamipsvmsvmid.rst} (100%) rename docs/api/{api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst => v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst} (100%) rename docs/api/{api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst => v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst} (100%) rename docs/api/{api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst => v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst} (100%) rename docs/api/{api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidautoidlepc.rst => v1/dynamips_vm/projectsprojectiddynamipsvmsvmidautoidlepc.rst} (100%) rename docs/api/{api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidconfigs.rst => v1/dynamips_vm/projectsprojectiddynamipsvmsvmidconfigs.rst} (100%) rename docs/api/{api.dynamips_vm/v1projectsprojectiddynamipsvmsvmididlepcproposals.rst => v1/dynamips_vm/projectsprojectiddynamipsvmsvmididlepcproposals.rst} (100%) rename docs/api/{api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidreload.rst => v1/dynamips_vm/projectsprojectiddynamipsvmsvmidreload.rst} (100%) rename docs/api/{api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidresume.rst => v1/dynamips_vm/projectsprojectiddynamipsvmsvmidresume.rst} (100%) rename docs/api/{api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidstart.rst => v1/dynamips_vm/projectsprojectiddynamipsvmsvmidstart.rst} (100%) rename docs/api/{api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidstop.rst => v1/dynamips_vm/projectsprojectiddynamipsvmsvmidstop.rst} (100%) rename docs/api/{api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidsuspend.rst => v1/dynamips_vm/projectsprojectiddynamipsvmsvmidsuspend.rst} (100%) rename docs/api/{api.iou.rst => v1/iou.rst} (83%) rename docs/api/{api.iou/v1projectsprojectidiouvms.rst => v1/iou/projectsprojectidiouvms.rst} (97%) rename docs/api/{api.iou/v1projectsprojectidiouvmsvmid.rst => v1/iou/projectsprojectidiouvmsvmid.rst} (96%) rename docs/api/{api.iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst => v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst} (83%) rename docs/api/{api.iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst => v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst} (90%) rename docs/api/{api.iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst => v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst} (85%) rename docs/api/{api.iou/v1projectsprojectidiouvmsvmidinitialconfig.rst => v1/iou/projectsprojectidiouvmsvmidinitialconfig.rst} (89%) rename docs/api/{api.iou/v1projectsprojectidiouvmsvmidreload.rst => v1/iou/projectsprojectidiouvmsvmidreload.rst} (85%) rename docs/api/{api.iou/v1projectsprojectidiouvmsvmidstart.rst => v1/iou/projectsprojectidiouvmsvmidstart.rst} (85%) rename docs/api/{api.iou/v1projectsprojectidiouvmsvmidstop.rst => v1/iou/projectsprojectidiouvmsvmidstop.rst} (85%) rename docs/api/{api.network.rst => v1/network.rst} (80%) rename docs/api/{api.network/v1interfaces.rst => v1/network/interfaces.rst} (82%) rename docs/api/{api.network/v1portsudp.rst => v1/network/portsudp.rst} (82%) rename docs/api/{api.project.rst => v1/project.rst} (80%) rename docs/api/{api.project/v1projects.rst => v1/project/projects.rst} (96%) rename docs/api/{api.project/v1projectsprojectid.rst => v1/project/projectsprojectid.rst} (93%) rename docs/api/{api.project/v1projectsprojectidclose.rst => v1/project/projectsprojectidclose.rst} (84%) rename docs/api/{api.project/v1projectsprojectidcommit.rst => v1/project/projectsprojectidcommit.rst} (84%) rename docs/api/{api.qemu.rst => v1/qemu.rst} (82%) rename docs/api/{api.qemu/v1projectsprojectidqemuvms.rst => v1/qemu/projectsprojectidqemuvms.rst} (98%) rename docs/api/{api.qemu/v1projectsprojectidqemuvmsvmid.rst => v1/qemu/projectsprojectidqemuvmsvmid.rst} (97%) rename docs/api/{api.qemu/v1projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst => v1/qemu/projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst} (83%) rename docs/api/{api.qemu/v1projectsprojectidqemuvmsvmidreload.rst => v1/qemu/projectsprojectidqemuvmsvmidreload.rst} (85%) rename docs/api/{api.qemu/v1projectsprojectidqemuvmsvmidresume.rst => v1/qemu/projectsprojectidqemuvmsvmidresume.rst} (85%) rename docs/api/{api.qemu/v1projectsprojectidqemuvmsvmidstart.rst => v1/qemu/projectsprojectidqemuvmsvmidstart.rst} (85%) rename docs/api/{api.qemu/v1projectsprojectidqemuvmsvmidstop.rst => v1/qemu/projectsprojectidqemuvmsvmidstop.rst} (85%) rename docs/api/{api.qemu/v1projectsprojectidqemuvmsvmidsuspend.rst => v1/qemu/projectsprojectidqemuvmsvmidsuspend.rst} (85%) rename docs/api/{api.qemu/v1qemubinaries.rst => v1/qemu/qemubinaries.rst} (84%) rename docs/api/{api.version.rst => v1/version.rst} (80%) rename docs/api/{api.version/v1version.rst => v1/version/version.rst} (91%) rename docs/api/{api.virtualbox.rst => v1/virtualbox.rst} (78%) rename docs/api/{api.virtualbox/v1projectsprojectidvirtualboxvms.rst => v1/virtualbox/projectsprojectidvirtualboxvms.rst} (97%) rename docs/api/{api.virtualbox/v1projectsprojectidvirtualboxvmsvmid.rst => v1/virtualbox/projectsprojectidvirtualboxvmsvmid.rst} (96%) rename docs/api/{api.virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst => v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst} (83%) rename docs/api/{api.virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst => v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst} (100%) rename docs/api/{api.virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst => v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst} (100%) rename docs/api/{api.virtualbox/v1projectsprojectidvirtualboxvmsvmidreload.rst => v1/virtualbox/projectsprojectidvirtualboxvmsvmidreload.rst} (84%) rename docs/api/{api.virtualbox/v1projectsprojectidvirtualboxvmsvmidresume.rst => v1/virtualbox/projectsprojectidvirtualboxvmsvmidresume.rst} (85%) rename docs/api/{api.virtualbox/v1projectsprojectidvirtualboxvmsvmidstart.rst => v1/virtualbox/projectsprojectidvirtualboxvmsvmidstart.rst} (84%) rename docs/api/{api.virtualbox/v1projectsprojectidvirtualboxvmsvmidstop.rst => v1/virtualbox/projectsprojectidvirtualboxvmsvmidstop.rst} (84%) rename docs/api/{api.virtualbox/v1projectsprojectidvirtualboxvmsvmidsuspend.rst => v1/virtualbox/projectsprojectidvirtualboxvmsvmidsuspend.rst} (84%) rename docs/api/{api.virtualbox/v1virtualboxvms.rst => v1/virtualbox/virtualboxvms.rst} (100%) rename docs/api/{api.vpcs.rst => v1/vpcs.rst} (82%) rename docs/api/{api.vpcs/v1projectsprojectidvpcsvms.rst => v1/vpcs/projectsprojectidvpcsvms.rst} (96%) rename docs/api/{api.vpcs/v1projectsprojectidvpcsvmsvmid.rst => v1/vpcs/projectsprojectidvpcsvmsvmid.rst} (93%) rename docs/api/{api.vpcs/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst => v1/vpcs/projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst} (83%) rename docs/api/{api.vpcs/v1projectsprojectidvpcsvmsvmidreload.rst => v1/vpcs/projectsprojectidvpcsvmsvmidreload.rst} (85%) rename docs/api/{api.vpcs/v1projectsprojectidvpcsvmsvmidstart.rst => v1/vpcs/projectsprojectidvpcsvmsvmidstart.rst} (85%) rename docs/api/{api.vpcs/v1projectsprojectidvpcsvmsvmidstop.rst => v1/vpcs/projectsprojectidvpcsvmsvmidstop.rst} (85%) create mode 100644 tests/web/test_documentation.py diff --git a/docs/api/examples/delete_projectsprojectid.txt b/docs/api/examples/delete_projectsprojectid.txt new file mode 100644 index 00000000..45efff6c --- /dev/null +++ b/docs/api/examples/delete_projectsprojectid.txt @@ -0,0 +1,13 @@ +curl -i -X DELETE 'http://localhost:8000/projects/{project_id}' + +DELETE /projects/{project_id} HTTP/1.1 + + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id} + diff --git a/docs/api/examples/delete_projectsprojectidiouvmsvmid.txt b/docs/api/examples/delete_projectsprojectidiouvmsvmid.txt new file mode 100644 index 00000000..ae225fce --- /dev/null +++ b/docs/api/examples/delete_projectsprojectidiouvmsvmid.txt @@ -0,0 +1,13 @@ +curl -i -X DELETE 'http://localhost:8000/projects/{project_id}/iou/vms/{vm_id}' + +DELETE /projects/{project_id}/iou/vms/{vm_id} HTTP/1.1 + + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/iou/vms/{vm_id} + diff --git a/docs/api/examples/delete_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt b/docs/api/examples/delete_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt new file mode 100644 index 00000000..f8aa407f --- /dev/null +++ b/docs/api/examples/delete_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -0,0 +1,13 @@ +curl -i -X DELETE 'http://localhost:8000/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio' + +DELETE /projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio HTTP/1.1 + + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio + diff --git a/docs/api/examples/delete_projectsprojectidqemuvmsvmid.txt b/docs/api/examples/delete_projectsprojectidqemuvmsvmid.txt new file mode 100644 index 00000000..8b47125e --- /dev/null +++ b/docs/api/examples/delete_projectsprojectidqemuvmsvmid.txt @@ -0,0 +1,13 @@ +curl -i -X DELETE 'http://localhost:8000/projects/{project_id}/qemu/vms/{vm_id}' + +DELETE /projects/{project_id}/qemu/vms/{vm_id} HTTP/1.1 + + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/qemu/vms/{vm_id} + diff --git a/docs/api/examples/delete_projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.txt b/docs/api/examples/delete_projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.txt new file mode 100644 index 00000000..ebd74cfd --- /dev/null +++ b/docs/api/examples/delete_projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -0,0 +1,13 @@ +curl -i -X DELETE 'http://localhost:8000/projects/{project_id}/qemu/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio' + +DELETE /projects/{project_id}/qemu/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio HTTP/1.1 + + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/qemu/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio + diff --git a/docs/api/examples/delete_projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.txt b/docs/api/examples/delete_projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.txt new file mode 100644 index 00000000..d40be628 --- /dev/null +++ b/docs/api/examples/delete_projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -0,0 +1,13 @@ +curl -i -X DELETE 'http://localhost:8000/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio' + +DELETE /projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio HTTP/1.1 + + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio + diff --git a/docs/api/examples/delete_projectsprojectidvpcsvmsvmid.txt b/docs/api/examples/delete_projectsprojectidvpcsvmsvmid.txt new file mode 100644 index 00000000..e9b9677c --- /dev/null +++ b/docs/api/examples/delete_projectsprojectidvpcsvmsvmid.txt @@ -0,0 +1,13 @@ +curl -i -X DELETE 'http://localhost:8000/projects/{project_id}/vpcs/vms/{vm_id}' + +DELETE /projects/{project_id}/vpcs/vms/{vm_id} HTTP/1.1 + + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/vpcs/vms/{vm_id} + diff --git a/docs/api/examples/delete_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt b/docs/api/examples/delete_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt new file mode 100644 index 00000000..6842905a --- /dev/null +++ b/docs/api/examples/delete_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -0,0 +1,13 @@ +curl -i -X DELETE 'http://localhost:8000/projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio' + +DELETE /projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio HTTP/1.1 + + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio + diff --git a/docs/api/examples/get_interfaces.txt b/docs/api/examples/get_interfaces.txt new file mode 100644 index 00000000..a34c7bc5 --- /dev/null +++ b/docs/api/examples/get_interfaces.txt @@ -0,0 +1,52 @@ +curl -i -X GET 'http://localhost:8000/interfaces' + +GET /interfaces HTTP/1.1 + + + +HTTP/1.1 200 +CONNECTION: keep-alive +CONTENT-LENGTH: 520 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/interfaces + +[ + { + "id": "lo0", + "name": "lo0" + }, + { + "id": "gif0", + "name": "gif0" + }, + { + "id": "stf0", + "name": "stf0" + }, + { + "id": "en0", + "name": "en0" + }, + { + "id": "en1", + "name": "en1" + }, + { + "id": "fw0", + "name": "fw0" + }, + { + "id": "en2", + "name": "en2" + }, + { + "id": "p2p0", + "name": "p2p0" + }, + { + "id": "bridge0", + "name": "bridge0" + } +] diff --git a/docs/api/examples/get_projectsprojectid.txt b/docs/api/examples/get_projectsprojectid.txt new file mode 100644 index 00000000..559a0388 --- /dev/null +++ b/docs/api/examples/get_projectsprojectid.txt @@ -0,0 +1,20 @@ +curl -i -X GET 'http://localhost:8000/projects/{project_id}' + +GET /projects/{project_id} HTTP/1.1 + + + +HTTP/1.1 200 +CONNECTION: keep-alive +CONTENT-LENGTH: 277 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id} + +{ + "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpjlh4s0j0", + "path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpjlh4s0j0/00010203-0405-0607-0809-0a0b0c0d0e0f", + "project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f", + "temporary": false +} diff --git a/docs/api/examples/get_projectsprojectidiouvmsvmid.txt b/docs/api/examples/get_projectsprojectidiouvmsvmid.txt new file mode 100644 index 00000000..84850ddb --- /dev/null +++ b/docs/api/examples/get_projectsprojectidiouvmsvmid.txt @@ -0,0 +1,27 @@ +curl -i -X GET 'http://localhost:8000/projects/{project_id}/iou/vms/{vm_id}' + +GET /projects/{project_id}/iou/vms/{vm_id} HTTP/1.1 + + + +HTTP/1.1 200 +CONNECTION: keep-alive +CONTENT-LENGTH: 409 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/iou/vms/{vm_id} + +{ + "console": 2000, + "ethernet_adapters": 2, + "initial_config": null, + "l1_keepalives": false, + "name": "PC TEST 1", + "nvram": 128, + "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3621/test_iou_get0/iou.bin", + "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", + "ram": 256, + "serial_adapters": 2, + "vm_id": "f75ff9e7-e658-45f7-9021-1651cfed1194" +} diff --git a/docs/api/examples/get_projectsprojectidiouvmsvmidinitialconfig.txt b/docs/api/examples/get_projectsprojectidiouvmsvmidinitialconfig.txt new file mode 100644 index 00000000..99bb7273 --- /dev/null +++ b/docs/api/examples/get_projectsprojectidiouvmsvmidinitialconfig.txt @@ -0,0 +1,17 @@ +curl -i -X GET 'http://localhost:8000/projects/{project_id}/iou/vms/{vm_id}/initial_config' + +GET /projects/{project_id}/iou/vms/{vm_id}/initial_config HTTP/1.1 + + + +HTTP/1.1 200 +CONNECTION: keep-alive +CONTENT-LENGTH: 25 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/iou/vms/{vm_id}/initial_config + +{ + "content": "TEST" +} diff --git a/docs/api/examples/get_projectsprojectidqemuvmsvmid.txt b/docs/api/examples/get_projectsprojectidqemuvmsvmid.txt new file mode 100644 index 00000000..47159a3c --- /dev/null +++ b/docs/api/examples/get_projectsprojectidqemuvmsvmid.txt @@ -0,0 +1,34 @@ +curl -i -X GET 'http://localhost:8000/projects/{project_id}/qemu/vms/{vm_id}' + +GET /projects/{project_id}/qemu/vms/{vm_id} HTTP/1.1 + + + +HTTP/1.1 200 +CONNECTION: keep-alive +CONTENT-LENGTH: 566 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/qemu/vms/{vm_id} + +{ + "adapter_type": "e1000", + "adapters": 1, + "console": 2000, + "cpu_throttling": 0, + "hda_disk_image": "", + "hdb_disk_image": "", + "initrd": "", + "kernel_command_line": "", + "kernel_image": "", + "legacy_networking": false, + "monitor": 2001, + "name": "PC TEST 1", + "options": "", + "process_priority": "low", + "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", + "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpu7smjb0q/qemu_x42", + "ram": 256, + "vm_id": "b41caecc-86fc-4986-a0b2-36892ac8baba" +} diff --git a/docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt b/docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt new file mode 100644 index 00000000..9ddcdd09 --- /dev/null +++ b/docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt @@ -0,0 +1,26 @@ +curl -i -X GET 'http://localhost:8000/projects/{project_id}/virtualbox/vms/{vm_id}' + +GET /projects/{project_id}/virtualbox/vms/{vm_id} HTTP/1.1 + + + +HTTP/1.1 200 +CONNECTION: keep-alive +CONTENT-LENGTH: 347 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/virtualbox/vms/{vm_id} + +{ + "adapter_type": "Intel PRO/1000 MT Desktop (82540EM)", + "adapters": 0, + "console": 2001, + "enable_remote_console": false, + "headless": false, + "name": "VMTEST", + "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", + "use_any_adapter": false, + "vm_id": "bb20d4fa-f233-400d-af07-2fbdcb337022", + "vmname": "VMTEST" +} diff --git a/docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt b/docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt new file mode 100644 index 00000000..56cdbf0d --- /dev/null +++ b/docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt @@ -0,0 +1,21 @@ +curl -i -X GET 'http://localhost:8000/projects/{project_id}/vpcs/vms/{vm_id}' + +GET /projects/{project_id}/vpcs/vms/{vm_id} HTTP/1.1 + + + +HTTP/1.1 200 +CONNECTION: keep-alive +CONTENT-LENGTH: 187 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/vpcs/vms/{vm_id} + +{ + "console": 2009, + "name": "PC TEST 1", + "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", + "startup_script": null, + "vm_id": "1b0843ea-4fcf-4d2a-94e8-bc3b7a92be88" +} diff --git a/docs/api/examples/get_qemubinaries.txt b/docs/api/examples/get_qemubinaries.txt new file mode 100644 index 00000000..d65c7681 --- /dev/null +++ b/docs/api/examples/get_qemubinaries.txt @@ -0,0 +1,24 @@ +curl -i -X GET 'http://localhost:8000/qemu/binaries' + +GET /qemu/binaries HTTP/1.1 + + + +HTTP/1.1 200 +CONNECTION: keep-alive +CONTENT-LENGTH: 134 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/qemu/binaries + +[ + { + "path": "/tmp/1", + "version": "2.2.0" + }, + { + "path": "/tmp/2", + "version": "2.1.0" + } +] diff --git a/docs/api/examples/get_version.txt b/docs/api/examples/get_version.txt new file mode 100644 index 00000000..88017034 --- /dev/null +++ b/docs/api/examples/get_version.txt @@ -0,0 +1,17 @@ +curl -i -X GET 'http://localhost:8000/version' + +GET /version HTTP/1.1 + + + +HTTP/1.1 200 +CONNECTION: keep-alive +CONTENT-LENGTH: 29 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/version + +{ + "version": "1.3.dev1" +} diff --git a/docs/api/examples/post_portsudp.txt b/docs/api/examples/post_portsudp.txt new file mode 100644 index 00000000..3be4b74c --- /dev/null +++ b/docs/api/examples/post_portsudp.txt @@ -0,0 +1,17 @@ +curl -i -X POST 'http://localhost:8000/ports/udp' -d '{}' + +POST /ports/udp HTTP/1.1 +{} + + +HTTP/1.1 201 +CONNECTION: keep-alive +CONTENT-LENGTH: 25 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/ports/udp + +{ + "udp_port": 10000 +} diff --git a/docs/api/examples/post_projects.txt b/docs/api/examples/post_projects.txt new file mode 100644 index 00000000..610adf2d --- /dev/null +++ b/docs/api/examples/post_projects.txt @@ -0,0 +1,20 @@ +curl -i -X POST 'http://localhost:8000/projects' -d '{}' + +POST /projects HTTP/1.1 +{} + + +HTTP/1.1 200 +CONNECTION: keep-alive +CONTENT-LENGTH: 277 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects + +{ + "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpanmwxfqf", + "path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpanmwxfqf/f0f4987c-b1d3-432f-a354-1179d1c727f9", + "project_id": "f0f4987c-b1d3-432f-a354-1179d1c727f9", + "temporary": false +} diff --git a/docs/api/examples/post_projectsprojectidclose.txt b/docs/api/examples/post_projectsprojectidclose.txt new file mode 100644 index 00000000..bcc429c9 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidclose.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/close' -d '{}' + +POST /projects/{project_id}/close HTTP/1.1 +{} + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/close + diff --git a/docs/api/examples/post_projectsprojectidcommit.txt b/docs/api/examples/post_projectsprojectidcommit.txt new file mode 100644 index 00000000..0b36f05d --- /dev/null +++ b/docs/api/examples/post_projectsprojectidcommit.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/commit' -d '{}' + +POST /projects/{project_id}/commit HTTP/1.1 +{} + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/commit + diff --git a/docs/api/examples/post_projectsprojectidiouvms.txt b/docs/api/examples/post_projectsprojectidiouvms.txt new file mode 100644 index 00000000..65f5b1ce --- /dev/null +++ b/docs/api/examples/post_projectsprojectidiouvms.txt @@ -0,0 +1,36 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/iou/vms' -d '{"ethernet_adapters": 0, "initial_config_content": "hostname test", "l1_keepalives": true, "name": "PC TEST 1", "nvram": 512, "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3621/test_iou_create_with_params0/iou.bin", "ram": 1024, "serial_adapters": 4}' + +POST /projects/{project_id}/iou/vms HTTP/1.1 +{ + "ethernet_adapters": 0, + "initial_config_content": "hostname test", + "l1_keepalives": true, + "name": "PC TEST 1", + "nvram": 512, + "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3621/test_iou_create_with_params0/iou.bin", + "ram": 1024, + "serial_adapters": 4 +} + + +HTTP/1.1 201 +CONNECTION: keep-alive +CONTENT-LENGTH: 440 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/iou/vms + +{ + "console": 2000, + "ethernet_adapters": 0, + "initial_config": "initial-config.cfg", + "l1_keepalives": true, + "name": "PC TEST 1", + "nvram": 512, + "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3621/test_iou_create_with_params0/iou.bin", + "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", + "ram": 1024, + "serial_adapters": 4, + "vm_id": "20e66cd4-52ef-4ad2-a44e-16bce06fd6f2" +} diff --git a/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt b/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt new file mode 100644 index 00000000..5940e201 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -0,0 +1,21 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio' -d '{"ethernet_device": "eth0", "type": "nio_generic_ethernet"}' + +POST /projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio HTTP/1.1 +{ + "ethernet_device": "eth0", + "type": "nio_generic_ethernet" +} + + +HTTP/1.1 201 +CONNECTION: keep-alive +CONTENT-LENGTH: 69 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio + +{ + "ethernet_device": "eth0", + "type": "nio_generic_ethernet" +} diff --git a/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.txt b/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.txt new file mode 100644 index 00000000..a43ed225 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.txt @@ -0,0 +1,20 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/start_capture' -d '{"capture_file_name": "test.pcap", "data_link_type": "DLT_EN10MB"}' + +POST /projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/start_capture HTTP/1.1 +{ + "capture_file_name": "test.pcap", + "data_link_type": "DLT_EN10MB" +} + + +HTTP/1.1 200 +CONNECTION: keep-alive +CONTENT-LENGTH: 158 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/start_capture + +{ + "pcap_file_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpd25gn8du/a1e920ca-338a-4e9f-b363-aa607b09dd80/project-files/captures/test.pcap" +} diff --git a/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.txt b/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.txt new file mode 100644 index 00000000..3b79347f --- /dev/null +++ b/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/stop_capture' -d '{}' + +POST /projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/stop_capture HTTP/1.1 +{} + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/stop_capture + diff --git a/docs/api/examples/post_projectsprojectidiouvmsvmidreload.txt b/docs/api/examples/post_projectsprojectidiouvmsvmidreload.txt new file mode 100644 index 00000000..f464b22e --- /dev/null +++ b/docs/api/examples/post_projectsprojectidiouvmsvmidreload.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/iou/vms/{vm_id}/reload' -d '{}' + +POST /projects/{project_id}/iou/vms/{vm_id}/reload HTTP/1.1 +{} + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/iou/vms/{vm_id}/reload + diff --git a/docs/api/examples/post_projectsprojectidiouvmsvmidstart.txt b/docs/api/examples/post_projectsprojectidiouvmsvmidstart.txt new file mode 100644 index 00000000..a82a5504 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidiouvmsvmidstart.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/iou/vms/{vm_id}/start' -d '{}' + +POST /projects/{project_id}/iou/vms/{vm_id}/start HTTP/1.1 +{} + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/iou/vms/{vm_id}/start + diff --git a/docs/api/examples/post_projectsprojectidiouvmsvmidstop.txt b/docs/api/examples/post_projectsprojectidiouvmsvmidstop.txt new file mode 100644 index 00000000..281869a6 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidiouvmsvmidstop.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/iou/vms/{vm_id}/stop' -d '{}' + +POST /projects/{project_id}/iou/vms/{vm_id}/stop HTTP/1.1 +{} + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/iou/vms/{vm_id}/stop + diff --git a/docs/api/examples/post_projectsprojectidqemuvms.txt b/docs/api/examples/post_projectsprojectidqemuvms.txt new file mode 100644 index 00000000..c301f2da --- /dev/null +++ b/docs/api/examples/post_projectsprojectidqemuvms.txt @@ -0,0 +1,39 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/qemu/vms' -d '{"hda_disk_image": "hda", "name": "PC TEST 1", "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpu7smjb0q/qemu_x42", "ram": 1024}' + +POST /projects/{project_id}/qemu/vms HTTP/1.1 +{ + "hda_disk_image": "hda", + "name": "PC TEST 1", + "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpu7smjb0q/qemu_x42", + "ram": 1024 +} + + +HTTP/1.1 201 +CONNECTION: keep-alive +CONTENT-LENGTH: 570 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/qemu/vms + +{ + "adapter_type": "e1000", + "adapters": 1, + "console": 2000, + "cpu_throttling": 0, + "hda_disk_image": "hda", + "hdb_disk_image": "", + "initrd": "", + "kernel_command_line": "", + "kernel_image": "", + "legacy_networking": false, + "monitor": 2001, + "name": "PC TEST 1", + "options": "", + "process_priority": "low", + "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", + "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpu7smjb0q/qemu_x42", + "ram": 1024, + "vm_id": "3176897a-996a-4020-86b8-3cd7a0031cbc" +} diff --git a/docs/api/examples/post_projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.txt b/docs/api/examples/post_projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.txt new file mode 100644 index 00000000..4419ebca --- /dev/null +++ b/docs/api/examples/post_projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -0,0 +1,21 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/qemu/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio' -d '{"ethernet_device": "eth0", "type": "nio_generic_ethernet"}' + +POST /projects/{project_id}/qemu/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio HTTP/1.1 +{ + "ethernet_device": "eth0", + "type": "nio_generic_ethernet" +} + + +HTTP/1.1 201 +CONNECTION: keep-alive +CONTENT-LENGTH: 69 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/qemu/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio + +{ + "ethernet_device": "eth0", + "type": "nio_generic_ethernet" +} diff --git a/docs/api/examples/post_projectsprojectidqemuvmsvmidreload.txt b/docs/api/examples/post_projectsprojectidqemuvmsvmidreload.txt new file mode 100644 index 00000000..8c4e6d14 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidqemuvmsvmidreload.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/qemu/vms/{vm_id}/reload' -d '{}' + +POST /projects/{project_id}/qemu/vms/{vm_id}/reload HTTP/1.1 +{} + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/qemu/vms/{vm_id}/reload + diff --git a/docs/api/examples/post_projectsprojectidqemuvmsvmidresume.txt b/docs/api/examples/post_projectsprojectidqemuvmsvmidresume.txt new file mode 100644 index 00000000..86c13afa --- /dev/null +++ b/docs/api/examples/post_projectsprojectidqemuvmsvmidresume.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/qemu/vms/{vm_id}/resume' -d '{}' + +POST /projects/{project_id}/qemu/vms/{vm_id}/resume HTTP/1.1 +{} + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/qemu/vms/{vm_id}/resume + diff --git a/docs/api/examples/post_projectsprojectidqemuvmsvmidstart.txt b/docs/api/examples/post_projectsprojectidqemuvmsvmidstart.txt new file mode 100644 index 00000000..c2365ff2 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidqemuvmsvmidstart.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/qemu/vms/{vm_id}/start' -d '{}' + +POST /projects/{project_id}/qemu/vms/{vm_id}/start HTTP/1.1 +{} + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/qemu/vms/{vm_id}/start + diff --git a/docs/api/examples/post_projectsprojectidqemuvmsvmidstop.txt b/docs/api/examples/post_projectsprojectidqemuvmsvmidstop.txt new file mode 100644 index 00000000..4c85508e --- /dev/null +++ b/docs/api/examples/post_projectsprojectidqemuvmsvmidstop.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/qemu/vms/{vm_id}/stop' -d '{}' + +POST /projects/{project_id}/qemu/vms/{vm_id}/stop HTTP/1.1 +{} + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/qemu/vms/{vm_id}/stop + diff --git a/docs/api/examples/post_projectsprojectidqemuvmsvmidsuspend.txt b/docs/api/examples/post_projectsprojectidqemuvmsvmidsuspend.txt new file mode 100644 index 00000000..5a412c66 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidqemuvmsvmidsuspend.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/qemu/vms/{vm_id}/suspend' -d '{}' + +POST /projects/{project_id}/qemu/vms/{vm_id}/suspend HTTP/1.1 +{} + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/qemu/vms/{vm_id}/suspend + diff --git a/docs/api/examples/post_projectsprojectidvirtualboxvms.txt b/docs/api/examples/post_projectsprojectidvirtualboxvms.txt new file mode 100644 index 00000000..7f6b4a53 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidvirtualboxvms.txt @@ -0,0 +1,30 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/virtualbox/vms' -d '{"linked_clone": false, "name": "VM1", "vmname": "VM1"}' + +POST /projects/{project_id}/virtualbox/vms HTTP/1.1 +{ + "linked_clone": false, + "name": "VM1", + "vmname": "VM1" +} + + +HTTP/1.1 201 +CONNECTION: keep-alive +CONTENT-LENGTH: 341 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/virtualbox/vms + +{ + "adapter_type": "Intel PRO/1000 MT Desktop (82540EM)", + "adapters": 0, + "console": 2000, + "enable_remote_console": false, + "headless": false, + "name": "VM1", + "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", + "use_any_adapter": false, + "vm_id": "8f384969-8478-4e8a-a6cd-c91376ccc89b", + "vmname": "VM1" +} diff --git a/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.txt b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.txt new file mode 100644 index 00000000..25100dbb --- /dev/null +++ b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -0,0 +1,25 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio' -d '{"lport": 4242, "rhost": "127.0.0.1", "rport": 4343, "type": "nio_udp"}' + +POST /projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio HTTP/1.1 +{ + "lport": 4242, + "rhost": "127.0.0.1", + "rport": 4343, + "type": "nio_udp" +} + + +HTTP/1.1 201 +CONNECTION: keep-alive +CONTENT-LENGTH: 89 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio + +{ + "lport": 4242, + "rhost": "127.0.0.1", + "rport": 4343, + "type": "nio_udp" +} diff --git a/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidreload.txt b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidreload.txt new file mode 100644 index 00000000..50e6dc77 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidreload.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/virtualbox/vms/{vm_id}/reload' -d '{}' + +POST /projects/{project_id}/virtualbox/vms/{vm_id}/reload HTTP/1.1 +{} + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/virtualbox/vms/{vm_id}/reload + diff --git a/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidresume.txt b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidresume.txt new file mode 100644 index 00000000..7d27abb9 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidresume.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/virtualbox/vms/{vm_id}/resume' -d '{}' + +POST /projects/{project_id}/virtualbox/vms/{vm_id}/resume HTTP/1.1 +{} + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/virtualbox/vms/{vm_id}/resume + diff --git a/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidstart.txt b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidstart.txt new file mode 100644 index 00000000..d533f5f4 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidstart.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/virtualbox/vms/{vm_id}/start' -d '{}' + +POST /projects/{project_id}/virtualbox/vms/{vm_id}/start HTTP/1.1 +{} + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/virtualbox/vms/{vm_id}/start + diff --git a/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidstop.txt b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidstop.txt new file mode 100644 index 00000000..500f5c02 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidstop.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/virtualbox/vms/{vm_id}/stop' -d '{}' + +POST /projects/{project_id}/virtualbox/vms/{vm_id}/stop HTTP/1.1 +{} + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/virtualbox/vms/{vm_id}/stop + diff --git a/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidsuspend.txt b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidsuspend.txt new file mode 100644 index 00000000..77945fc1 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidsuspend.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/virtualbox/vms/{vm_id}/suspend' -d '{}' + +POST /projects/{project_id}/virtualbox/vms/{vm_id}/suspend HTTP/1.1 +{} + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/virtualbox/vms/{vm_id}/suspend + diff --git a/docs/api/examples/post_projectsprojectidvpcsvms.txt b/docs/api/examples/post_projectsprojectidvpcsvms.txt new file mode 100644 index 00000000..8f7c689f --- /dev/null +++ b/docs/api/examples/post_projectsprojectidvpcsvms.txt @@ -0,0 +1,23 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/vpcs/vms' -d '{"name": "PC TEST 1"}' + +POST /projects/{project_id}/vpcs/vms HTTP/1.1 +{ + "name": "PC TEST 1" +} + + +HTTP/1.1 201 +CONNECTION: keep-alive +CONTENT-LENGTH: 187 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/vpcs/vms + +{ + "console": 2009, + "name": "PC TEST 1", + "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", + "startup_script": null, + "vm_id": "90e4cc09-4013-4e94-97ce-3ca48676de22" +} diff --git a/docs/api/examples/post_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt b/docs/api/examples/post_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt new file mode 100644 index 00000000..e55c4ace --- /dev/null +++ b/docs/api/examples/post_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -0,0 +1,25 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio' -d '{"lport": 4242, "rhost": "127.0.0.1", "rport": 4343, "type": "nio_udp"}' + +POST /projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio HTTP/1.1 +{ + "lport": 4242, + "rhost": "127.0.0.1", + "rport": 4343, + "type": "nio_udp" +} + + +HTTP/1.1 201 +CONNECTION: keep-alive +CONTENT-LENGTH: 89 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio + +{ + "lport": 4242, + "rhost": "127.0.0.1", + "rport": 4343, + "type": "nio_udp" +} diff --git a/docs/api/examples/post_projectsprojectidvpcsvmsvmidreload.txt b/docs/api/examples/post_projectsprojectidvpcsvmsvmidreload.txt new file mode 100644 index 00000000..ad8d4282 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidvpcsvmsvmidreload.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/vpcs/vms/{vm_id}/reload' -d '{}' + +POST /projects/{project_id}/vpcs/vms/{vm_id}/reload HTTP/1.1 +{} + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/vpcs/vms/{vm_id}/reload + diff --git a/docs/api/examples/post_projectsprojectidvpcsvmsvmidstart.txt b/docs/api/examples/post_projectsprojectidvpcsvmsvmidstart.txt new file mode 100644 index 00000000..cd597019 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidvpcsvmsvmidstart.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/vpcs/vms/{vm_id}/start' -d '{}' + +POST /projects/{project_id}/vpcs/vms/{vm_id}/start HTTP/1.1 +{} + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/vpcs/vms/{vm_id}/start + diff --git a/docs/api/examples/post_projectsprojectidvpcsvmsvmidstop.txt b/docs/api/examples/post_projectsprojectidvpcsvmsvmidstop.txt new file mode 100644 index 00000000..8d4f0127 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidvpcsvmsvmidstop.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/{project_id}/vpcs/vms/{vm_id}/stop' -d '{}' + +POST /projects/{project_id}/vpcs/vms/{vm_id}/stop HTTP/1.1 +{} + + +HTTP/1.1 204 +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/vpcs/vms/{vm_id}/stop + diff --git a/docs/api/examples/post_version.txt b/docs/api/examples/post_version.txt new file mode 100644 index 00000000..2f6c1452 --- /dev/null +++ b/docs/api/examples/post_version.txt @@ -0,0 +1,19 @@ +curl -i -X POST 'http://localhost:8000/version' -d '{"version": "1.3.dev1"}' + +POST /version HTTP/1.1 +{ + "version": "1.3.dev1" +} + + +HTTP/1.1 200 +CONNECTION: keep-alive +CONTENT-LENGTH: 29 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/version + +{ + "version": "1.3.dev1" +} diff --git a/docs/api/examples/put_projectsprojectid.txt b/docs/api/examples/put_projectsprojectid.txt new file mode 100644 index 00000000..26774c07 --- /dev/null +++ b/docs/api/examples/put_projectsprojectid.txt @@ -0,0 +1,20 @@ +curl -i -X PUT 'http://localhost:8000/projects/{project_id}' -d '{"path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3621/test_update_path_project_non_l0"}' + +PUT /projects/{project_id} HTTP/1.1 +{ + "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3621/test_update_path_project_non_l0" +} + + +HTTP/1.1 403 +CONNECTION: keep-alive +CONTENT-LENGTH: 100 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id} + +{ + "message": "You are not allowed to modify the project directory location", + "status": 403 +} diff --git a/docs/api/examples/put_projectsprojectidiouvmsvmid.txt b/docs/api/examples/put_projectsprojectidiouvmsvmid.txt new file mode 100644 index 00000000..7c4c1058 --- /dev/null +++ b/docs/api/examples/put_projectsprojectidiouvmsvmid.txt @@ -0,0 +1,36 @@ +curl -i -X PUT 'http://localhost:8000/projects/{project_id}/iou/vms/{vm_id}' -d '{"console": 2001, "ethernet_adapters": 4, "initial_config_content": "hostname test", "l1_keepalives": true, "name": "test", "nvram": 2048, "ram": 512, "serial_adapters": 0}' + +PUT /projects/{project_id}/iou/vms/{vm_id} HTTP/1.1 +{ + "console": 2001, + "ethernet_adapters": 4, + "initial_config_content": "hostname test", + "l1_keepalives": true, + "name": "test", + "nvram": 2048, + "ram": 512, + "serial_adapters": 0 +} + + +HTTP/1.1 200 +CONNECTION: keep-alive +CONTENT-LENGTH: 423 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/iou/vms/{vm_id} + +{ + "console": 2001, + "ethernet_adapters": 4, + "initial_config": "initial-config.cfg", + "l1_keepalives": true, + "name": "test", + "nvram": 2048, + "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3621/test_iou_update0/iou.bin", + "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", + "ram": 512, + "serial_adapters": 0, + "vm_id": "e867af73-aaf1-4770-a935-78a357ea5db3" +} diff --git a/docs/api/examples/put_projectsprojectidqemuvmsvmid.txt b/docs/api/examples/put_projectsprojectidqemuvmsvmid.txt new file mode 100644 index 00000000..b52c7a2d --- /dev/null +++ b/docs/api/examples/put_projectsprojectidqemuvmsvmid.txt @@ -0,0 +1,39 @@ +curl -i -X PUT 'http://localhost:8000/projects/{project_id}/qemu/vms/{vm_id}' -d '{"console": 2002, "hdb_disk_image": "hdb", "name": "test", "ram": 1024}' + +PUT /projects/{project_id}/qemu/vms/{vm_id} HTTP/1.1 +{ + "console": 2002, + "hdb_disk_image": "hdb", + "name": "test", + "ram": 1024 +} + + +HTTP/1.1 200 +CONNECTION: keep-alive +CONTENT-LENGTH: 565 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/qemu/vms/{vm_id} + +{ + "adapter_type": "e1000", + "adapters": 1, + "console": 2002, + "cpu_throttling": 0, + "hda_disk_image": "", + "hdb_disk_image": "hdb", + "initrd": "", + "kernel_command_line": "", + "kernel_image": "", + "legacy_networking": false, + "monitor": 2001, + "name": "test", + "options": "", + "process_priority": "low", + "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", + "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpu7smjb0q/qemu_x42", + "ram": 1024, + "vm_id": "8a0d735b-d485-4a2e-bae1-be54f426fbeb" +} diff --git a/docs/api/examples/put_projectsprojectidvirtualboxvmsvmid.txt b/docs/api/examples/put_projectsprojectidvirtualboxvmsvmid.txt new file mode 100644 index 00000000..8f37d59d --- /dev/null +++ b/docs/api/examples/put_projectsprojectidvirtualboxvmsvmid.txt @@ -0,0 +1,29 @@ +curl -i -X PUT 'http://localhost:8000/projects/{project_id}/virtualbox/vms/{vm_id}' -d '{"console": 2010, "name": "test"}' + +PUT /projects/{project_id}/virtualbox/vms/{vm_id} HTTP/1.1 +{ + "console": 2010, + "name": "test" +} + + +HTTP/1.1 200 +CONNECTION: keep-alive +CONTENT-LENGTH: 345 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/virtualbox/vms/{vm_id} + +{ + "adapter_type": "Intel PRO/1000 MT Desktop (82540EM)", + "adapters": 0, + "console": 2010, + "enable_remote_console": false, + "headless": false, + "name": "test", + "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", + "use_any_adapter": false, + "vm_id": "dcf2c555-703d-47cb-9dd2-8ee6c11b9f6c", + "vmname": "VMTEST" +} diff --git a/docs/api/examples/put_projectsprojectidvpcsvmsvmid.txt b/docs/api/examples/put_projectsprojectidvpcsvmsvmid.txt new file mode 100644 index 00000000..532b274b --- /dev/null +++ b/docs/api/examples/put_projectsprojectidvpcsvmsvmid.txt @@ -0,0 +1,25 @@ +curl -i -X PUT 'http://localhost:8000/projects/{project_id}/vpcs/vms/{vm_id}' -d '{"console": 2011, "name": "test", "startup_script": "ip 192.168.1.1"}' + +PUT /projects/{project_id}/vpcs/vms/{vm_id} HTTP/1.1 +{ + "console": 2011, + "name": "test", + "startup_script": "ip 192.168.1.1" +} + + +HTTP/1.1 200 +CONNECTION: keep-alive +CONTENT-LENGTH: 194 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 GNS3/1.3.dev1 +X-ROUTE: /v1/projects/{project_id}/vpcs/vms/{vm_id} + +{ + "console": 2011, + "name": "test", + "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", + "startup_script": "ip 192.168.1.1", + "vm_id": "f27aa7da-e267-483a-adc6-2587e1377e8c" +} diff --git a/docs/api/upload.rst b/docs/api/upload.rst deleted file mode 100644 index 51e901b0..00000000 --- a/docs/api/upload.rst +++ /dev/null @@ -1,8 +0,0 @@ -Upload ---------------------- - -.. toctree:: - :glob: - :maxdepth: 2 - - upload/* diff --git a/docs/api/upload/upload.rst b/docs/api/upload/upload.rst deleted file mode 100644 index f716f9fe..00000000 --- a/docs/api/upload/upload.rst +++ /dev/null @@ -1,22 +0,0 @@ -/upload ----------------------------------------------------------------------------------------------------------------------- - -.. contents:: - -GET /upload -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Manage upload of GNS3 images - -Response status codes -********************** -- **200**: OK - - -POST /upload -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Manage upload of GNS3 images - -Response status codes -********************** -- **200**: OK - diff --git a/docs/api/api.dynamips_device.rst b/docs/api/v1/dynamips_device.rst similarity index 75% rename from docs/api/api.dynamips_device.rst rename to docs/api/v1/dynamips_device.rst index c5cd1ff6..83c17b94 100644 --- a/docs/api/api.dynamips_device.rst +++ b/docs/api/v1/dynamips_device.rst @@ -5,4 +5,4 @@ Dynamips device :glob: :maxdepth: 2 - api.dynamips_device/* + dynamips_device/* diff --git a/docs/api/api.dynamips_device/v1projectsprojectiddynamipsdevices.rst b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevices.rst similarity index 100% rename from docs/api/api.dynamips_device/v1projectsprojectiddynamipsdevices.rst rename to docs/api/v1/dynamips_device/projectsprojectiddynamipsdevices.rst diff --git a/docs/api/api.dynamips_device/v1projectsprojectiddynamipsdevicesdeviceid.rst b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceid.rst similarity index 100% rename from docs/api/api.dynamips_device/v1projectsprojectiddynamipsdevicesdeviceid.rst rename to docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceid.rst diff --git a/docs/api/api.dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst similarity index 100% rename from docs/api/api.dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst rename to docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst index 706b5c06..0533ad08 100644 --- a/docs/api/api.dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst +++ b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst @@ -10,8 +10,8 @@ Add a NIO to a Dynamips device instance Parameters ********** - **project_id**: UUID for the project -- **device_id**: UUID for the instance - **port_number**: Port on the device +- **device_id**: UUID for the instance Response status codes ********************** @@ -129,8 +129,8 @@ Remove a NIO from a Dynamips device instance Parameters ********** - **project_id**: UUID for the project -- **device_id**: UUID for the instance - **port_number**: Port on the device +- **device_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/api.dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst similarity index 100% rename from docs/api/api.dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst rename to docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst index 19852f49..117cd928 100644 --- a/docs/api/api.dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst +++ b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst @@ -10,8 +10,8 @@ Start a packet capture on a Dynamips device instance Parameters ********** - **project_id**: UUID for the project -- **device_id**: UUID for the instance - **port_number**: Port on the device +- **device_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/api.dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst similarity index 100% rename from docs/api/api.dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst rename to docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst index cc312e43..9674ef65 100644 --- a/docs/api/api.dynamips_device/v1projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst +++ b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst @@ -10,8 +10,8 @@ Stop a packet capture on a Dynamips device instance Parameters ********** - **project_id**: UUID for the project -- **device_id**: UUID for the instance - **port_number**: Port on the device +- **device_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/api.dynamips_vm.rst b/docs/api/v1/dynamips_vm.rst similarity index 78% rename from docs/api/api.dynamips_vm.rst rename to docs/api/v1/dynamips_vm.rst index c4851bfe..f32d26b7 100644 --- a/docs/api/api.dynamips_vm.rst +++ b/docs/api/v1/dynamips_vm.rst @@ -5,4 +5,4 @@ Dynamips vm :glob: :maxdepth: 2 - api.dynamips_vm/* + dynamips_vm/* diff --git a/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvms.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvms.rst similarity index 100% rename from docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvms.rst rename to docs/api/v1/dynamips_vm/projectsprojectiddynamipsvms.rst diff --git a/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmid.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmid.rst similarity index 100% rename from docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmid.rst rename to docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmid.rst diff --git a/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst similarity index 100% rename from docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst rename to docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst index c1c114ec..13c726c7 100644 --- a/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -11,8 +11,8 @@ Parameters ********** - **project_id**: UUID for the project - **adapter_number**: Adapter where the nio should be added -- **port_number**: Port on the adapter - **vm_id**: UUID for the instance +- **port_number**: Port on the adapter Response status codes ********************** @@ -29,8 +29,8 @@ Parameters ********** - **project_id**: UUID for the project - **adapter_number**: Adapter from where the nio should be removed -- **port_number**: Port on the adapter - **vm_id**: UUID for the instance +- **port_number**: Port on the adapter Response status codes ********************** diff --git a/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst similarity index 100% rename from docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst rename to docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst index 57271aac..16b5b121 100644 --- a/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst @@ -11,8 +11,8 @@ Parameters ********** - **project_id**: UUID for the project - **adapter_number**: Adapter to start a packet capture -- **port_number**: Port on the adapter - **vm_id**: UUID for the instance +- **port_number**: Port on the adapter Response status codes ********************** diff --git a/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst similarity index 100% rename from docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst rename to docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst index c3b44232..5522c169 100644 --- a/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst @@ -11,8 +11,8 @@ Parameters ********** - **project_id**: UUID for the project - **adapter_number**: Adapter to stop a packet capture -- **port_number**: Port on the adapter (always 0) - **vm_id**: UUID for the instance +- **port_number**: Port on the adapter (always 0) Response status codes ********************** diff --git a/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidautoidlepc.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidautoidlepc.rst similarity index 100% rename from docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidautoidlepc.rst rename to docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidautoidlepc.rst diff --git a/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidconfigs.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidconfigs.rst similarity index 100% rename from docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidconfigs.rst rename to docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidconfigs.rst diff --git a/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmididlepcproposals.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmididlepcproposals.rst similarity index 100% rename from docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmididlepcproposals.rst rename to docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmididlepcproposals.rst diff --git a/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidreload.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidreload.rst similarity index 100% rename from docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidreload.rst rename to docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidreload.rst diff --git a/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidresume.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidresume.rst similarity index 100% rename from docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidresume.rst rename to docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidresume.rst diff --git a/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidstart.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidstart.rst similarity index 100% rename from docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidstart.rst rename to docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidstart.rst diff --git a/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidstop.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidstop.rst similarity index 100% rename from docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidstop.rst rename to docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidstop.rst diff --git a/docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidsuspend.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidsuspend.rst similarity index 100% rename from docs/api/api.dynamips_vm/v1projectsprojectiddynamipsvmsvmidsuspend.rst rename to docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidsuspend.rst diff --git a/docs/api/api.iou.rst b/docs/api/v1/iou.rst similarity index 83% rename from docs/api/api.iou.rst rename to docs/api/v1/iou.rst index a04ef778..c2188031 100644 --- a/docs/api/api.iou.rst +++ b/docs/api/v1/iou.rst @@ -5,4 +5,4 @@ Iou :glob: :maxdepth: 2 - api.iou/* + iou/* diff --git a/docs/api/api.iou/v1projectsprojectidiouvms.rst b/docs/api/v1/iou/projectsprojectidiouvms.rst similarity index 97% rename from docs/api/api.iou/v1projectsprojectidiouvms.rst rename to docs/api/v1/iou/projectsprojectidiouvms.rst index 281eccd2..5944bf4d 100644 --- a/docs/api/api.iou/v1projectsprojectidiouvms.rst +++ b/docs/api/v1/iou/projectsprojectidiouvms.rst @@ -54,3 +54,9 @@ Output vm_id ✔ string IOU VM UUID +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidiouvms.txt + diff --git a/docs/api/api.iou/v1projectsprojectidiouvmsvmid.rst b/docs/api/v1/iou/projectsprojectidiouvmsvmid.rst similarity index 96% rename from docs/api/api.iou/v1projectsprojectidiouvmsvmid.rst rename to docs/api/v1/iou/projectsprojectidiouvmsvmid.rst index 4ba73e3a..b4bb82f6 100644 --- a/docs/api/api.iou/v1projectsprojectidiouvmsvmid.rst +++ b/docs/api/v1/iou/projectsprojectidiouvmsvmid.rst @@ -37,6 +37,12 @@ Output vm_id ✔ string IOU VM UUID +Sample session +*************** + + +.. literalinclude:: ../../examples/get_projectsprojectidiouvmsvmid.txt + PUT /v1/projects/**{project_id}**/iou/vms/**{vm_id}** ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -90,6 +96,12 @@ Output vm_id ✔ string IOU VM UUID +Sample session +*************** + + +.. literalinclude:: ../../examples/put_projectsprojectidiouvmsvmid.txt + DELETE /v1/projects/**{project_id}**/iou/vms/**{vm_id}** ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -106,3 +118,9 @@ Response status codes - **404**: Instance doesn't exist - **204**: Instance deleted +Sample session +*************** + + +.. literalinclude:: ../../examples/delete_projectsprojectidiouvmsvmid.txt + diff --git a/docs/api/api.iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst similarity index 83% rename from docs/api/api.iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst rename to docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst index 643f5a12..6b9abacb 100644 --- a/docs/api/api.iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -11,8 +11,8 @@ Parameters ********** - **project_id**: UUID for the project - **adapter_number**: Network adapter where the nio is located -- **port_number**: Port where the nio should be added - **vm_id**: UUID for the instance +- **port_number**: Port where the nio should be added Response status codes ********************** @@ -20,6 +20,12 @@ Response status codes - **201**: NIO created - **404**: Instance doesn't exist +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt + DELETE /v1/projects/**{project_id}**/iou/vms/**{vm_id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/nio ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -29,8 +35,8 @@ Parameters ********** - **project_id**: UUID for the project - **adapter_number**: Network adapter where the nio is located -- **port_number**: Port from where the nio should be removed - **vm_id**: UUID for the instance +- **port_number**: Port from where the nio should be removed Response status codes ********************** @@ -38,3 +44,9 @@ Response status codes - **404**: Instance doesn't exist - **204**: NIO deleted +Sample session +*************** + + +.. literalinclude:: ../../examples/delete_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt + diff --git a/docs/api/api.iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst b/docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst similarity index 90% rename from docs/api/api.iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst rename to docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst index 3c31f1ef..31e72cd4 100644 --- a/docs/api/api.iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst +++ b/docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst @@ -11,8 +11,8 @@ Parameters ********** - **project_id**: UUID for the project - **adapter_number**: Adapter to start a packet capture -- **port_number**: Port on the adapter - **vm_id**: UUID for the instance +- **port_number**: Port on the adapter Response status codes ********************** @@ -30,3 +30,9 @@ Input data_link_type ✔ string PCAP data link type +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.txt + diff --git a/docs/api/api.iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst b/docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst similarity index 85% rename from docs/api/api.iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst rename to docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst index e8da18d1..ca5e1abc 100644 --- a/docs/api/api.iou/v1projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst +++ b/docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst @@ -11,8 +11,8 @@ Parameters ********** - **project_id**: UUID for the project - **adapter_number**: Adapter to stop a packet capture -- **port_number**: Port on the adapter (always 0) - **vm_id**: UUID for the instance +- **port_number**: Port on the adapter (always 0) Response status codes ********************** @@ -20,3 +20,9 @@ Response status codes - **404**: Instance doesn't exist - **204**: Capture stopped +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.txt + diff --git a/docs/api/api.iou/v1projectsprojectidiouvmsvmidinitialconfig.rst b/docs/api/v1/iou/projectsprojectidiouvmsvmidinitialconfig.rst similarity index 89% rename from docs/api/api.iou/v1projectsprojectidiouvmsvmidinitialconfig.rst rename to docs/api/v1/iou/projectsprojectidiouvmsvmidinitialconfig.rst index 8e56bf25..6159e783 100644 --- a/docs/api/api.iou/v1projectsprojectidiouvmsvmidinitialconfig.rst +++ b/docs/api/v1/iou/projectsprojectidiouvmsvmidinitialconfig.rst @@ -22,3 +22,9 @@ Output content ✔ ['string', 'null'] Content of the initial configuration file +Sample session +*************** + + +.. literalinclude:: ../../examples/get_projectsprojectidiouvmsvmidinitialconfig.txt + diff --git a/docs/api/api.iou/v1projectsprojectidiouvmsvmidreload.rst b/docs/api/v1/iou/projectsprojectidiouvmsvmidreload.rst similarity index 85% rename from docs/api/api.iou/v1projectsprojectidiouvmsvmidreload.rst rename to docs/api/v1/iou/projectsprojectidiouvmsvmidreload.rst index ffdf82b3..49be3d31 100644 --- a/docs/api/api.iou/v1projectsprojectidiouvmsvmidreload.rst +++ b/docs/api/v1/iou/projectsprojectidiouvmsvmidreload.rst @@ -18,3 +18,9 @@ Response status codes - **404**: Instance doesn't exist - **204**: Instance reloaded +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidiouvmsvmidreload.txt + diff --git a/docs/api/api.iou/v1projectsprojectidiouvmsvmidstart.rst b/docs/api/v1/iou/projectsprojectidiouvmsvmidstart.rst similarity index 85% rename from docs/api/api.iou/v1projectsprojectidiouvmsvmidstart.rst rename to docs/api/v1/iou/projectsprojectidiouvmsvmidstart.rst index 7ef3f580..25a9e236 100644 --- a/docs/api/api.iou/v1projectsprojectidiouvmsvmidstart.rst +++ b/docs/api/v1/iou/projectsprojectidiouvmsvmidstart.rst @@ -18,3 +18,9 @@ Response status codes - **404**: Instance doesn't exist - **204**: Instance started +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidiouvmsvmidstart.txt + diff --git a/docs/api/api.iou/v1projectsprojectidiouvmsvmidstop.rst b/docs/api/v1/iou/projectsprojectidiouvmsvmidstop.rst similarity index 85% rename from docs/api/api.iou/v1projectsprojectidiouvmsvmidstop.rst rename to docs/api/v1/iou/projectsprojectidiouvmsvmidstop.rst index dd2e81f5..220c81ad 100644 --- a/docs/api/api.iou/v1projectsprojectidiouvmsvmidstop.rst +++ b/docs/api/v1/iou/projectsprojectidiouvmsvmidstop.rst @@ -18,3 +18,9 @@ Response status codes - **404**: Instance doesn't exist - **204**: Instance stopped +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidiouvmsvmidstop.txt + diff --git a/docs/api/api.network.rst b/docs/api/v1/network.rst similarity index 80% rename from docs/api/api.network.rst rename to docs/api/v1/network.rst index 886cd394..38366abe 100644 --- a/docs/api/api.network.rst +++ b/docs/api/v1/network.rst @@ -5,4 +5,4 @@ Network :glob: :maxdepth: 2 - api.network/* + network/* diff --git a/docs/api/api.network/v1interfaces.rst b/docs/api/v1/network/interfaces.rst similarity index 82% rename from docs/api/api.network/v1interfaces.rst rename to docs/api/v1/network/interfaces.rst index 2a1071f3..35036d57 100644 --- a/docs/api/api.network/v1interfaces.rst +++ b/docs/api/v1/network/interfaces.rst @@ -11,3 +11,9 @@ Response status codes ********************** - **200**: OK +Sample session +*************** + + +.. literalinclude:: ../../examples/get_interfaces.txt + diff --git a/docs/api/api.network/v1portsudp.rst b/docs/api/v1/network/portsudp.rst similarity index 82% rename from docs/api/api.network/v1portsudp.rst rename to docs/api/v1/network/portsudp.rst index 0d6f7975..c37318ed 100644 --- a/docs/api/api.network/v1portsudp.rst +++ b/docs/api/v1/network/portsudp.rst @@ -11,3 +11,9 @@ Response status codes ********************** - **201**: UDP port allocated +Sample session +*************** + + +.. literalinclude:: ../../examples/post_portsudp.txt + diff --git a/docs/api/api.project.rst b/docs/api/v1/project.rst similarity index 80% rename from docs/api/api.project.rst rename to docs/api/v1/project.rst index 703ef19c..95453d81 100644 --- a/docs/api/api.project.rst +++ b/docs/api/v1/project.rst @@ -5,4 +5,4 @@ Project :glob: :maxdepth: 2 - api.project/* + project/* diff --git a/docs/api/api.project/v1projects.rst b/docs/api/v1/project/projects.rst similarity index 96% rename from docs/api/api.project/v1projects.rst rename to docs/api/v1/project/projects.rst index 9ace36fe..2cc133fb 100644 --- a/docs/api/api.project/v1projects.rst +++ b/docs/api/v1/project/projects.rst @@ -34,3 +34,9 @@ Output temporary ✔ boolean If project is a temporary project +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projects.txt + diff --git a/docs/api/api.project/v1projectsprojectid.rst b/docs/api/v1/project/projectsprojectid.rst similarity index 93% rename from docs/api/api.project/v1projectsprojectid.rst rename to docs/api/v1/project/projectsprojectid.rst index c1a70376..bed089de 100644 --- a/docs/api/api.project/v1projectsprojectid.rst +++ b/docs/api/v1/project/projectsprojectid.rst @@ -28,6 +28,12 @@ Output temporary ✔ boolean If project is a temporary project +Sample session +*************** + + +.. literalinclude:: ../../examples/get_projectsprojectid.txt + PUT /v1/projects/**{project_id}** ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -65,6 +71,12 @@ Output temporary ✔ boolean If project is a temporary project +Sample session +*************** + + +.. literalinclude:: ../../examples/put_projectsprojectid.txt + DELETE /v1/projects/**{project_id}** ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -79,3 +91,9 @@ Response status codes - **404**: The project doesn't exist - **204**: Changes have been written on disk +Sample session +*************** + + +.. literalinclude:: ../../examples/delete_projectsprojectid.txt + diff --git a/docs/api/api.project/v1projectsprojectidclose.rst b/docs/api/v1/project/projectsprojectidclose.rst similarity index 84% rename from docs/api/api.project/v1projectsprojectidclose.rst rename to docs/api/v1/project/projectsprojectidclose.rst index c0623c9b..5f9b867f 100644 --- a/docs/api/api.project/v1projectsprojectidclose.rst +++ b/docs/api/v1/project/projectsprojectidclose.rst @@ -16,3 +16,9 @@ Response status codes - **404**: The project doesn't exist - **204**: The project has been closed +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidclose.txt + diff --git a/docs/api/api.project/v1projectsprojectidcommit.rst b/docs/api/v1/project/projectsprojectidcommit.rst similarity index 84% rename from docs/api/api.project/v1projectsprojectidcommit.rst rename to docs/api/v1/project/projectsprojectidcommit.rst index 49c6fb8a..f08f2a33 100644 --- a/docs/api/api.project/v1projectsprojectidcommit.rst +++ b/docs/api/v1/project/projectsprojectidcommit.rst @@ -16,3 +16,9 @@ Response status codes - **404**: The project doesn't exist - **204**: Changes have been written on disk +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidcommit.txt + diff --git a/docs/api/api.qemu.rst b/docs/api/v1/qemu.rst similarity index 82% rename from docs/api/api.qemu.rst rename to docs/api/v1/qemu.rst index 553c3953..70fd8fc2 100644 --- a/docs/api/api.qemu.rst +++ b/docs/api/v1/qemu.rst @@ -5,4 +5,4 @@ Qemu :glob: :maxdepth: 2 - api.qemu/* + qemu/* diff --git a/docs/api/api.qemu/v1projectsprojectidqemuvms.rst b/docs/api/v1/qemu/projectsprojectidqemuvms.rst similarity index 98% rename from docs/api/api.qemu/v1projectsprojectidqemuvms.rst rename to docs/api/v1/qemu/projectsprojectidqemuvms.rst index 4a88a237..155fa581 100644 --- a/docs/api/api.qemu/v1projectsprojectidqemuvms.rst +++ b/docs/api/v1/qemu/projectsprojectidqemuvms.rst @@ -68,3 +68,9 @@ Output vm_id ✔ string QEMU VM uuid +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidqemuvms.txt + diff --git a/docs/api/api.qemu/v1projectsprojectidqemuvmsvmid.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmid.rst similarity index 97% rename from docs/api/api.qemu/v1projectsprojectidqemuvmsvmid.rst rename to docs/api/v1/qemu/projectsprojectidqemuvmsvmid.rst index e90b3cfd..0270bbd9 100644 --- a/docs/api/api.qemu/v1projectsprojectidqemuvmsvmid.rst +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmid.rst @@ -44,6 +44,12 @@ Output vm_id ✔ string QEMU VM uuid +Sample session +*************** + + +.. literalinclude:: ../../examples/get_projectsprojectidqemuvmsvmid.txt + PUT /v1/projects/**{project_id}**/qemu/vms/**{vm_id}** ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -111,6 +117,12 @@ Output vm_id ✔ string QEMU VM uuid +Sample session +*************** + + +.. literalinclude:: ../../examples/put_projectsprojectidqemuvmsvmid.txt + DELETE /v1/projects/**{project_id}**/qemu/vms/**{vm_id}** ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -127,3 +139,9 @@ Response status codes - **404**: Instance doesn't exist - **204**: Instance deleted +Sample session +*************** + + +.. literalinclude:: ../../examples/delete_projectsprojectidqemuvmsvmid.txt + diff --git a/docs/api/api.qemu/v1projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst similarity index 83% rename from docs/api/api.qemu/v1projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst rename to docs/api/v1/qemu/projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst index a5515da9..985f53e0 100644 --- a/docs/api/api.qemu/v1projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -11,8 +11,8 @@ Parameters ********** - **project_id**: UUID for the project - **adapter_number**: Network adapter where the nio is located -- **port_number**: Port where the nio should be added - **vm_id**: UUID for the instance +- **port_number**: Port where the nio should be added Response status codes ********************** @@ -20,6 +20,12 @@ Response status codes - **201**: NIO created - **404**: Instance doesn't exist +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.txt + DELETE /v1/projects/**{project_id}**/qemu/vms/**{vm_id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/nio ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -29,8 +35,8 @@ Parameters ********** - **project_id**: UUID for the project - **adapter_number**: Network adapter where the nio is located -- **port_number**: Port from where the nio should be removed - **vm_id**: UUID for the instance +- **port_number**: Port from where the nio should be removed Response status codes ********************** @@ -38,3 +44,9 @@ Response status codes - **404**: Instance doesn't exist - **204**: NIO deleted +Sample session +*************** + + +.. literalinclude:: ../../examples/delete_projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.txt + diff --git a/docs/api/api.qemu/v1projectsprojectidqemuvmsvmidreload.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidreload.rst similarity index 85% rename from docs/api/api.qemu/v1projectsprojectidqemuvmsvmidreload.rst rename to docs/api/v1/qemu/projectsprojectidqemuvmsvmidreload.rst index 04e239f6..5b29cec3 100644 --- a/docs/api/api.qemu/v1projectsprojectidqemuvmsvmidreload.rst +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidreload.rst @@ -18,3 +18,9 @@ Response status codes - **404**: Instance doesn't exist - **204**: Instance reloaded +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidqemuvmsvmidreload.txt + diff --git a/docs/api/api.qemu/v1projectsprojectidqemuvmsvmidresume.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidresume.rst similarity index 85% rename from docs/api/api.qemu/v1projectsprojectidqemuvmsvmidresume.rst rename to docs/api/v1/qemu/projectsprojectidqemuvmsvmidresume.rst index a06a45c2..59b5c1f7 100644 --- a/docs/api/api.qemu/v1projectsprojectidqemuvmsvmidresume.rst +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidresume.rst @@ -18,3 +18,9 @@ Response status codes - **404**: Instance doesn't exist - **204**: Instance resumed +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidqemuvmsvmidresume.txt + diff --git a/docs/api/api.qemu/v1projectsprojectidqemuvmsvmidstart.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstart.rst similarity index 85% rename from docs/api/api.qemu/v1projectsprojectidqemuvmsvmidstart.rst rename to docs/api/v1/qemu/projectsprojectidqemuvmsvmidstart.rst index d2649825..f5306892 100644 --- a/docs/api/api.qemu/v1projectsprojectidqemuvmsvmidstart.rst +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstart.rst @@ -18,3 +18,9 @@ Response status codes - **404**: Instance doesn't exist - **204**: Instance started +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidqemuvmsvmidstart.txt + diff --git a/docs/api/api.qemu/v1projectsprojectidqemuvmsvmidstop.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstop.rst similarity index 85% rename from docs/api/api.qemu/v1projectsprojectidqemuvmsvmidstop.rst rename to docs/api/v1/qemu/projectsprojectidqemuvmsvmidstop.rst index be132747..d5c41c96 100644 --- a/docs/api/api.qemu/v1projectsprojectidqemuvmsvmidstop.rst +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstop.rst @@ -18,3 +18,9 @@ Response status codes - **404**: Instance doesn't exist - **204**: Instance stopped +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidqemuvmsvmidstop.txt + diff --git a/docs/api/api.qemu/v1projectsprojectidqemuvmsvmidsuspend.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidsuspend.rst similarity index 85% rename from docs/api/api.qemu/v1projectsprojectidqemuvmsvmidsuspend.rst rename to docs/api/v1/qemu/projectsprojectidqemuvmsvmidsuspend.rst index 26f4216b..63e8c94a 100644 --- a/docs/api/api.qemu/v1projectsprojectidqemuvmsvmidsuspend.rst +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidsuspend.rst @@ -18,3 +18,9 @@ Response status codes - **404**: Instance doesn't exist - **204**: Instance suspended +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidqemuvmsvmidsuspend.txt + diff --git a/docs/api/api.qemu/v1qemubinaries.rst b/docs/api/v1/qemu/qemubinaries.rst similarity index 84% rename from docs/api/api.qemu/v1qemubinaries.rst rename to docs/api/v1/qemu/qemubinaries.rst index 4d9494bb..6f851195 100644 --- a/docs/api/api.qemu/v1qemubinaries.rst +++ b/docs/api/v1/qemu/qemubinaries.rst @@ -13,3 +13,9 @@ Response status codes - **400**: Invalid request - **404**: Instance doesn't exist +Sample session +*************** + + +.. literalinclude:: ../../examples/get_qemubinaries.txt + diff --git a/docs/api/api.version.rst b/docs/api/v1/version.rst similarity index 80% rename from docs/api/api.version.rst rename to docs/api/v1/version.rst index 62427503..adc4c1f0 100644 --- a/docs/api/api.version.rst +++ b/docs/api/v1/version.rst @@ -5,4 +5,4 @@ Version :glob: :maxdepth: 2 - api.version/* + version/* diff --git a/docs/api/api.version/v1version.rst b/docs/api/v1/version/version.rst similarity index 91% rename from docs/api/api.version/v1version.rst rename to docs/api/v1/version/version.rst index 2fdf1edb..8733379d 100644 --- a/docs/api/api.version/v1version.rst +++ b/docs/api/v1/version/version.rst @@ -20,6 +20,12 @@ Output version ✔ string Version number human readable +Sample session +*************** + + +.. literalinclude:: ../../examples/get_version.txt + POST /v1/version ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -48,3 +54,9 @@ Output version ✔ string Version number human readable +Sample session +*************** + + +.. literalinclude:: ../../examples/post_version.txt + diff --git a/docs/api/api.virtualbox.rst b/docs/api/v1/virtualbox.rst similarity index 78% rename from docs/api/api.virtualbox.rst rename to docs/api/v1/virtualbox.rst index e1700535..517624b2 100644 --- a/docs/api/api.virtualbox.rst +++ b/docs/api/v1/virtualbox.rst @@ -5,4 +5,4 @@ Virtualbox :glob: :maxdepth: 2 - api.virtualbox/* + virtualbox/* diff --git a/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvms.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvms.rst similarity index 97% rename from docs/api/api.virtualbox/v1projectsprojectidvirtualboxvms.rst rename to docs/api/v1/virtualbox/projectsprojectidvirtualboxvms.rst index 8f85e7a5..03d96831 100644 --- a/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvms.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvms.rst @@ -53,3 +53,9 @@ Output vmname string VirtualBox VM name (in VirtualBox itself) +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidvirtualboxvms.txt + diff --git a/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmid.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmid.rst similarity index 96% rename from docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmid.rst rename to docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmid.rst index b1cf371d..8ec38157 100644 --- a/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmid.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmid.rst @@ -36,6 +36,12 @@ Output vmname string VirtualBox VM name (in VirtualBox itself) +Sample session +*************** + + +.. literalinclude:: ../../examples/get_projectsprojectidvirtualboxvmsvmid.txt + PUT /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}** ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -87,6 +93,12 @@ Output vmname string VirtualBox VM name (in VirtualBox itself) +Sample session +*************** + + +.. literalinclude:: ../../examples/put_projectsprojectidvirtualboxvmsvmid.txt + DELETE /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}** ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst similarity index 83% rename from docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst rename to docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst index e670257d..8e8016f4 100644 --- a/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -11,8 +11,8 @@ Parameters ********** - **project_id**: UUID for the project - **adapter_number**: Adapter where the nio should be added -- **port_number**: Port on the adapter (always 0) - **vm_id**: UUID for the instance +- **port_number**: Port on the adapter (always 0) Response status codes ********************** @@ -20,6 +20,12 @@ Response status codes - **201**: NIO created - **404**: Instance doesn't exist +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.txt + DELETE /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/nio ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -29,8 +35,8 @@ Parameters ********** - **project_id**: UUID for the project - **adapter_number**: Adapter from where the nio should be removed -- **port_number**: Port on the adapter (always) - **vm_id**: UUID for the instance +- **port_number**: Port on the adapter (always) Response status codes ********************** @@ -38,3 +44,9 @@ Response status codes - **404**: Instance doesn't exist - **204**: NIO deleted +Sample session +*************** + + +.. literalinclude:: ../../examples/delete_projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.txt + diff --git a/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst similarity index 100% rename from docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst rename to docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst index 47b4fdca..31103c86 100644 --- a/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst @@ -11,8 +11,8 @@ Parameters ********** - **project_id**: UUID for the project - **adapter_number**: Adapter to start a packet capture -- **port_number**: Port on the adapter (always 0) - **vm_id**: UUID for the instance +- **port_number**: Port on the adapter (always 0) Response status codes ********************** diff --git a/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst similarity index 100% rename from docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst rename to docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst index 057170b0..0571712e 100644 --- a/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst @@ -11,8 +11,8 @@ Parameters ********** - **project_id**: UUID for the project - **adapter_number**: Adapter to stop a packet capture -- **port_number**: Port on the adapter (always 0) - **vm_id**: UUID for the instance +- **port_number**: Port on the adapter (always 0) Response status codes ********************** diff --git a/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidreload.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidreload.rst similarity index 84% rename from docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidreload.rst rename to docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidreload.rst index 66771462..9ae84c29 100644 --- a/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidreload.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidreload.rst @@ -18,3 +18,9 @@ Response status codes - **404**: Instance doesn't exist - **204**: Instance reloaded +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidvirtualboxvmsvmidreload.txt + diff --git a/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidresume.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidresume.rst similarity index 85% rename from docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidresume.rst rename to docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidresume.rst index 58e70528..0fb9d427 100644 --- a/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidresume.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidresume.rst @@ -18,3 +18,9 @@ Response status codes - **404**: Instance doesn't exist - **204**: Instance resumed +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidvirtualboxvmsvmidresume.txt + diff --git a/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidstart.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidstart.rst similarity index 84% rename from docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidstart.rst rename to docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidstart.rst index bd324d65..5e6a6c42 100644 --- a/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidstart.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidstart.rst @@ -18,3 +18,9 @@ Response status codes - **404**: Instance doesn't exist - **204**: Instance started +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidvirtualboxvmsvmidstart.txt + diff --git a/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidstop.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidstop.rst similarity index 84% rename from docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidstop.rst rename to docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidstop.rst index 7d4ef0ee..1eaac889 100644 --- a/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidstop.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidstop.rst @@ -18,3 +18,9 @@ Response status codes - **404**: Instance doesn't exist - **204**: Instance stopped +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidvirtualboxvmsvmidstop.txt + diff --git a/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidsuspend.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidsuspend.rst similarity index 84% rename from docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidsuspend.rst rename to docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidsuspend.rst index 6f84582e..ad7f469b 100644 --- a/docs/api/api.virtualbox/v1projectsprojectidvirtualboxvmsvmidsuspend.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidsuspend.rst @@ -18,3 +18,9 @@ Response status codes - **404**: Instance doesn't exist - **204**: Instance suspended +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidvirtualboxvmsvmidsuspend.txt + diff --git a/docs/api/api.virtualbox/v1virtualboxvms.rst b/docs/api/v1/virtualbox/virtualboxvms.rst similarity index 100% rename from docs/api/api.virtualbox/v1virtualboxvms.rst rename to docs/api/v1/virtualbox/virtualboxvms.rst diff --git a/docs/api/api.vpcs.rst b/docs/api/v1/vpcs.rst similarity index 82% rename from docs/api/api.vpcs.rst rename to docs/api/v1/vpcs.rst index e4b06735..ab00c921 100644 --- a/docs/api/api.vpcs.rst +++ b/docs/api/v1/vpcs.rst @@ -5,4 +5,4 @@ Vpcs :glob: :maxdepth: 2 - api.vpcs/* + vpcs/* diff --git a/docs/api/api.vpcs/v1projectsprojectidvpcsvms.rst b/docs/api/v1/vpcs/projectsprojectidvpcsvms.rst similarity index 96% rename from docs/api/api.vpcs/v1projectsprojectidvpcsvms.rst rename to docs/api/v1/vpcs/projectsprojectidvpcsvms.rst index c59992e6..8b66e040 100644 --- a/docs/api/api.vpcs/v1projectsprojectidvpcsvms.rst +++ b/docs/api/v1/vpcs/projectsprojectidvpcsvms.rst @@ -42,3 +42,9 @@ Output vm_id ✔ string VPCS VM UUID +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidvpcsvms.txt + diff --git a/docs/api/api.vpcs/v1projectsprojectidvpcsvmsvmid.rst b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmid.rst similarity index 93% rename from docs/api/api.vpcs/v1projectsprojectidvpcsvmsvmid.rst rename to docs/api/v1/vpcs/projectsprojectidvpcsvmsvmid.rst index a7867e67..dcfee3cf 100644 --- a/docs/api/api.vpcs/v1projectsprojectidvpcsvmsvmid.rst +++ b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmid.rst @@ -31,6 +31,12 @@ Output vm_id ✔ string VPCS VM UUID +Sample session +*************** + + +.. literalinclude:: ../../examples/get_projectsprojectidvpcsvmsvmid.txt + PUT /v1/projects/**{project_id}**/vpcs/vms/**{vm_id}** ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -72,6 +78,12 @@ Output vm_id ✔ string VPCS VM UUID +Sample session +*************** + + +.. literalinclude:: ../../examples/put_projectsprojectidvpcsvmsvmid.txt + DELETE /v1/projects/**{project_id}**/vpcs/vms/**{vm_id}** ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -88,3 +100,9 @@ Response status codes - **404**: Instance doesn't exist - **204**: Instance deleted +Sample session +*************** + + +.. literalinclude:: ../../examples/delete_projectsprojectidvpcsvmsvmid.txt + diff --git a/docs/api/api.vpcs/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst similarity index 83% rename from docs/api/api.vpcs/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst rename to docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst index ce72f5d7..fcfff01d 100644 --- a/docs/api/api.vpcs/v1projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -11,8 +11,8 @@ Parameters ********** - **project_id**: UUID for the project - **adapter_number**: Network adapter where the nio is located -- **port_number**: Port where the nio should be added - **vm_id**: UUID for the instance +- **port_number**: Port where the nio should be added Response status codes ********************** @@ -20,6 +20,12 @@ Response status codes - **201**: NIO created - **404**: Instance doesn't exist +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt + DELETE /v1/projects/**{project_id}**/vpcs/vms/**{vm_id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/nio ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -29,8 +35,8 @@ Parameters ********** - **project_id**: UUID for the project - **adapter_number**: Network adapter where the nio is located -- **port_number**: Port from where the nio should be removed - **vm_id**: UUID for the instance +- **port_number**: Port from where the nio should be removed Response status codes ********************** @@ -38,3 +44,9 @@ Response status codes - **404**: Instance doesn't exist - **204**: NIO deleted +Sample session +*************** + + +.. literalinclude:: ../../examples/delete_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt + diff --git a/docs/api/api.vpcs/v1projectsprojectidvpcsvmsvmidreload.rst b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidreload.rst similarity index 85% rename from docs/api/api.vpcs/v1projectsprojectidvpcsvmsvmidreload.rst rename to docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidreload.rst index 4736a952..224798fd 100644 --- a/docs/api/api.vpcs/v1projectsprojectidvpcsvmsvmidreload.rst +++ b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidreload.rst @@ -18,3 +18,9 @@ Response status codes - **404**: Instance doesn't exist - **204**: Instance reloaded +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidvpcsvmsvmidreload.txt + diff --git a/docs/api/api.vpcs/v1projectsprojectidvpcsvmsvmidstart.rst b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidstart.rst similarity index 85% rename from docs/api/api.vpcs/v1projectsprojectidvpcsvmsvmidstart.rst rename to docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidstart.rst index 285b711b..b87f43a6 100644 --- a/docs/api/api.vpcs/v1projectsprojectidvpcsvmsvmidstart.rst +++ b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidstart.rst @@ -18,3 +18,9 @@ Response status codes - **404**: Instance doesn't exist - **204**: Instance started +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidvpcsvmsvmidstart.txt + diff --git a/docs/api/api.vpcs/v1projectsprojectidvpcsvmsvmidstop.rst b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidstop.rst similarity index 85% rename from docs/api/api.vpcs/v1projectsprojectidvpcsvmsvmidstop.rst rename to docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidstop.rst index 9f65fe19..269c8953 100644 --- a/docs/api/api.vpcs/v1projectsprojectidvpcsvmsvmidstop.rst +++ b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidstop.rst @@ -18,3 +18,9 @@ Response status codes - **404**: Instance doesn't exist - **204**: Instance stopped +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidvpcsvmsvmidstop.txt + diff --git a/docs/index.rst b/docs/index.rst index 35f652b5..4f12a125 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -18,5 +18,5 @@ API Endpoints :glob: :maxdepth: 2 - api/* + api/v1/* diff --git a/gns3server/web/documentation.py b/gns3server/web/documentation.py index fd972529..05ae9af3 100644 --- a/gns3server/web/documentation.py +++ b/gns3server/web/documentation.py @@ -27,18 +27,28 @@ class Documentation(object): """Extract API documentation as Sphinx compatible files""" - def __init__(self, route): + def __init__(self, route, directory): + """ + :param route: Route instance + :param directory: Output directory + """ self._documentation = route.get_documentation() + self._directory = directory def write(self): for handler_name in sorted(self._documentation): - self._create_handler_directory(handler_name) - for path in sorted(self._documentation[handler_name]): + + api_version = self._documentation[handler_name][path]["api_version"] + if api_version is None: + continue + + self._create_handler_directory(handler_name, api_version) + filename = self._file_path(path) handler_doc = self._documentation[handler_name][path] - with open("docs/api/{}/{}.rst".format(handler_name, filename), 'w+') as f: + with open("{}/api/v{}/{}/{}.rst".format(self._directory, api_version, handler_name, filename), 'w+') as f: f.write('{}\n----------------------------------------------------------------------------------------------------------------------\n\n'.format(path)) f.write('.. contents::\n') for method in handler_doc["methods"]: @@ -68,29 +78,29 @@ class Documentation(object): f.write("Output\n*******\n") self._write_json_schema(f, method["output_schema"]) - self._include_query_example(f, method, path) + self._include_query_example(f, method, path, api_version) - def _create_handler_directory(self, handler_name): + def _create_handler_directory(self, handler_name, api_version): """Create a directory for the handler and add an index inside""" - directory = "docs/api/{}".format(handler_name) + directory = "{}/api/v{}/{}".format(self._directory, api_version, handler_name) os.makedirs(directory, exist_ok=True) - with open("docs/api/{}.rst".format(handler_name), "w+") as f: + with open("{}/api/v{}/{}.rst".format(self._directory, api_version, handler_name), "w+") as f: f.write(handler_name.replace("api.", "").replace("_", " ", ).capitalize()) f.write("\n---------------------\n\n") f.write(".. toctree::\n :glob:\n :maxdepth: 2\n\n {}/*\n".format(handler_name)) - def _include_query_example(self, f, method, path): + def _include_query_example(self, f, method, path, api_version): """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)): + query_path = "{}_{}.txt".format(m, self._file_path(path)) + if os.path.isfile(os.path.join(self._directory, "api", "examples", query_path)): f.write("Sample session\n***************\n") - f.write("\n\n.. literalinclude:: {}\n\n".format(query_path)) + f.write("\n\n.. literalinclude:: ../../examples/{}\n\n".format(query_path)) def _file_path(self, path): - return re.sub('[^a-z0-9]', '', path) + return re.sub("^v1", "", re.sub("[^a-z0-9]", "", path)) def _write_definitions(self, f, schema): if "definitions" in schema: @@ -148,4 +158,4 @@ class Documentation(object): if __name__ == '__main__': print("Generate API documentation") - Documentation(Route).write() + Documentation(Route, "docs").write() diff --git a/gns3server/web/route.py b/gns3server/web/route.py index 93b10eed..a26a8f8b 100644 --- a/gns3server/web/route.py +++ b/gns3server/web/route.py @@ -92,9 +92,10 @@ class Route(object): def register(func): route = cls._path - handler = func.__module__.replace("_handler", "").replace("gns3server.handlers.", "") + handler = func.__module__.replace("_handler", "").replace("gns3server.handlers.api.", "") cls._documentation.setdefault(handler, {}) - cls._documentation[handler].setdefault(route, {"methods": []}) + cls._documentation[handler].setdefault(route, {"api_version": api_version, + "methods": []}) cls._documentation[handler][route]["methods"].append({ "method": method, diff --git a/scripts/documentation.sh b/scripts/documentation.sh index fff4ed24..41111993 100755 --- a/scripts/documentation.sh +++ b/scripts/documentation.sh @@ -28,7 +28,7 @@ export PYTEST_BUILD_DOCUMENTATION=1 rm -Rf docs/api/ mkdir -p docs/api/examples -#py.test -v +py.test -v python3 gns3server/web/documentation.py cd docs make html diff --git a/tests/handlers/api/test_iou.py b/tests/handlers/api/test_iou.py index 5e1b29dc..5221f0eb 100644 --- a/tests/handlers/api/test_iou.py +++ b/tests/handlers/api/test_iou.py @@ -104,28 +104,28 @@ def test_iou_get(server, project, vm): def test_iou_start(server, vm): with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM.start", return_value=True) as mock: - response = server.post("/projects/{project_id}/iou/vms/{vm_id}/start".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + response = server.post("/projects/{project_id}/iou/vms/{vm_id}/start".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert mock.called assert response.status == 204 def test_iou_stop(server, vm): with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM.stop", return_value=True) as mock: - response = server.post("/projects/{project_id}/iou/vms/{vm_id}/stop".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + response = server.post("/projects/{project_id}/iou/vms/{vm_id}/stop".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert mock.called assert response.status == 204 def test_iou_reload(server, vm): with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM.reload", return_value=True) as mock: - response = server.post("/projects/{project_id}/iou/vms/{vm_id}/reload".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + response = server.post("/projects/{project_id}/iou/vms/{vm_id}/reload".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert mock.called assert response.status == 204 def test_iou_delete(server, vm): with asyncio_patch("gns3server.modules.iou.IOU.delete_vm", return_value=True) as mock: - response = server.delete("/projects/{project_id}/iou/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + response = server.delete("/projects/{project_id}/iou/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert mock.called assert response.status == 204 @@ -141,7 +141,7 @@ def test_iou_update(server, vm, tmpdir, free_console_port, project): "l1_keepalives": True, "initial_config_content": "hostname test" } - response = server.put("/projects/{project_id}/iou/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), params) + response = server.put("/projects/{project_id}/iou/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), params, example=True) assert response.status == 200 assert response.json["name"] == "test" assert response.json["console"] == free_console_port diff --git a/tests/handlers/api/test_project.py b/tests/handlers/api/test_project.py index b633a57c..a82834a0 100644 --- a/tests/handlers/api/test_project.py +++ b/tests/handlers/api/test_project.py @@ -33,7 +33,7 @@ def test_create_project_with_path(server, tmpdir): def test_create_project_without_dir(server): query = {} - response = server.post("/projects", query) + response = server.post("/projects", query, example=True) assert response.status == 200 assert response.json["project_id"] is not None assert response.json["temporary"] is False diff --git a/tests/handlers/api/test_qemu.py b/tests/handlers/api/test_qemu.py index f0fe64d0..b32cd945 100644 --- a/tests/handlers/api/test_qemu.py +++ b/tests/handlers/api/test_qemu.py @@ -77,42 +77,42 @@ def test_qemu_get(server, project, vm): def test_qemu_start(server, vm): with asyncio_patch("gns3server.modules.qemu.qemu_vm.QemuVM.start", return_value=True) as mock: - response = server.post("/projects/{project_id}/qemu/vms/{vm_id}/start".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + response = server.post("/projects/{project_id}/qemu/vms/{vm_id}/start".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert mock.called assert response.status == 204 def test_qemu_stop(server, vm): with asyncio_patch("gns3server.modules.qemu.qemu_vm.QemuVM.stop", return_value=True) as mock: - response = server.post("/projects/{project_id}/qemu/vms/{vm_id}/stop".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + response = server.post("/projects/{project_id}/qemu/vms/{vm_id}/stop".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert mock.called assert response.status == 204 def test_qemu_reload(server, vm): with asyncio_patch("gns3server.modules.qemu.qemu_vm.QemuVM.reload", return_value=True) as mock: - response = server.post("/projects/{project_id}/qemu/vms/{vm_id}/reload".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + response = server.post("/projects/{project_id}/qemu/vms/{vm_id}/reload".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert mock.called assert response.status == 204 def test_qemu_suspend(server, vm): with asyncio_patch("gns3server.modules.qemu.qemu_vm.QemuVM.suspend", return_value=True) as mock: - response = server.post("/projects/{project_id}/qemu/vms/{vm_id}/suspend".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + response = server.post("/projects/{project_id}/qemu/vms/{vm_id}/suspend".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert mock.called assert response.status == 204 def test_qemu_resume(server, vm): with asyncio_patch("gns3server.modules.qemu.qemu_vm.QemuVM.resume", return_value=True) as mock: - response = server.post("/projects/{project_id}/qemu/vms/{vm_id}/resume".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + response = server.post("/projects/{project_id}/qemu/vms/{vm_id}/resume".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert mock.called assert response.status == 204 def test_qemu_delete(server, vm): with asyncio_patch("gns3server.modules.qemu.Qemu.delete_vm", return_value=True) as mock: - response = server.delete("/projects/{project_id}/qemu/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + response = server.delete("/projects/{project_id}/qemu/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert mock.called assert response.status == 204 @@ -124,7 +124,7 @@ def test_qemu_update(server, vm, tmpdir, free_console_port, project): "ram": 1024, "hdb_disk_image": "hdb" } - response = server.put("/projects/{project_id}/qemu/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), params) + response = server.put("/projects/{project_id}/qemu/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), params, example=True) assert response.status == 200 assert response.json["name"] == "test" assert response.json["console"] == free_console_port @@ -168,7 +168,7 @@ def test_qemu_list_binaries(server, vm): ret = [{"path": "/tmp/1", "version": "2.2.0"}, {"path": "/tmp/2", "version": "2.1.0"}] with asyncio_patch("gns3server.modules.qemu.Qemu.binary_list", return_value=ret) as mock: - response = server.get("/qemu/binaries".format(project_id=vm["project_id"])) + response = server.get("/qemu/binaries".format(project_id=vm["project_id"]), example=True) assert mock.called assert response.status == 200 assert response.json == ret diff --git a/tests/handlers/api/test_virtualbox.py b/tests/handlers/api/test_virtualbox.py index c195b459..16825d1b 100644 --- a/tests/handlers/api/test_virtualbox.py +++ b/tests/handlers/api/test_virtualbox.py @@ -57,35 +57,35 @@ def test_vbox_get(server, project, vm): def test_vbox_start(server, vm): with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.start", return_value=True) as mock: - response = server.post("/projects/{project_id}/virtualbox/vms/{vm_id}/start".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + response = server.post("/projects/{project_id}/virtualbox/vms/{vm_id}/start".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert mock.called assert response.status == 204 def test_vbox_stop(server, vm): with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.stop", return_value=True) as mock: - response = server.post("/projects/{project_id}/virtualbox/vms/{vm_id}/stop".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + response = server.post("/projects/{project_id}/virtualbox/vms/{vm_id}/stop".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert mock.called assert response.status == 204 def test_vbox_suspend(server, vm): with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.suspend", return_value=True) as mock: - response = server.post("/projects/{project_id}/virtualbox/vms/{vm_id}/suspend".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + response = server.post("/projects/{project_id}/virtualbox/vms/{vm_id}/suspend".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert mock.called assert response.status == 204 def test_vbox_resume(server, vm): with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.resume", return_value=True) as mock: - response = server.post("/projects/{project_id}/virtualbox/vms/{vm_id}/resume".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + response = server.post("/projects/{project_id}/virtualbox/vms/{vm_id}/resume".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert mock.called assert response.status == 204 def test_vbox_reload(server, vm): with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.reload", return_value=True) as mock: - response = server.post("/projects/{project_id}/virtualbox/vms/{vm_id}/reload".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + response = server.post("/projects/{project_id}/virtualbox/vms/{vm_id}/reload".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert mock.called assert response.status == 204 @@ -124,7 +124,8 @@ def test_vbox_delete_nio(server, vm): def test_vbox_update(server, vm, free_console_port): response = server.put("/projects/{project_id}/virtualbox/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"name": "test", - "console": free_console_port}) + "console": free_console_port}, + example=True) assert response.status == 200 assert response.json["name"] == "test" assert response.json["console"] == free_console_port diff --git a/tests/handlers/api/test_vpcs.py b/tests/handlers/api/test_vpcs.py index 009da325..e0a4acca 100644 --- a/tests/handlers/api/test_vpcs.py +++ b/tests/handlers/api/test_vpcs.py @@ -94,28 +94,28 @@ def test_vpcs_delete_nio(server, vm): def test_vpcs_start(server, vm): with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM.start", return_value=True) as mock: - response = server.post("/projects/{project_id}/vpcs/vms/{vm_id}/start".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + response = server.post("/projects/{project_id}/vpcs/vms/{vm_id}/start".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert mock.called assert response.status == 204 def test_vpcs_stop(server, vm): with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM.stop", return_value=True) as mock: - response = server.post("/projects/{project_id}/vpcs/vms/{vm_id}/stop".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + response = server.post("/projects/{project_id}/vpcs/vms/{vm_id}/stop".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert mock.called assert response.status == 204 def test_vpcs_reload(server, vm): with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM.reload", return_value=True) as mock: - response = server.post("/projects/{project_id}/vpcs/vms/{vm_id}/reload".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + response = server.post("/projects/{project_id}/vpcs/vms/{vm_id}/reload".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert mock.called assert response.status == 204 def test_vpcs_delete(server, vm): with asyncio_patch("gns3server.modules.vpcs.VPCS.delete_vm", return_value=True) as mock: - response = server.delete("/projects/{project_id}/vpcs/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + response = server.delete("/projects/{project_id}/vpcs/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert mock.called assert response.status == 204 @@ -123,7 +123,8 @@ def test_vpcs_delete(server, vm): def test_vpcs_update(server, vm, tmpdir, free_console_port): response = server.put("/projects/{project_id}/vpcs/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"name": "test", "console": free_console_port, - "startup_script": "ip 192.168.1.1"}) + "startup_script": "ip 192.168.1.1"}, + example=True) assert response.status == 200 assert response.json["name"] == "test" assert response.json["console"] == free_console_port diff --git a/tests/web/test_documentation.py b/tests/web/test_documentation.py new file mode 100644 index 00000000..111d24aa --- /dev/null +++ b/tests/web/test_documentation.py @@ -0,0 +1,40 @@ +# -*- 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 os + +from gns3server.web.documentation import Documentation +from gns3server.handlers import * +from gns3server.web.route import Route + + +def test_documentation_write(tmpdir): + os.makedirs(str(tmpdir / "api/examples")) + with open(str(tmpdir / "api/examples/post_projectsprojectidvirtualboxvms.txt"), "w+") as f: + f.write("curl test") + + Documentation(Route, str(tmpdir)).write() + + assert os.path.exists(str(tmpdir / "api")) + assert os.path.exists(str(tmpdir / "api" / "v1")) + assert os.path.exists(str(tmpdir / "api" / "v1" / "virtualbox.rst")) + assert os.path.exists(str(tmpdir / "api" / "v1" / "virtualbox")) + assert os.path.exists(str(tmpdir / "api" / "v1" / "virtualbox" / "virtualboxvms.rst")) + with open(str(tmpdir / "api" / "v1" / "virtualbox" / "projectsprojectidvirtualboxvms.rst")) as f: + content = f.read() + assert "Sample session" in content + assert "literalinclude:: ../../examples/post_projectsprojectidvirtualboxvms.txt" in content From 250bb38d7cca06d40c8af7b74494705f5b23b7c0 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 24 Feb 2015 17:40:01 +0100 Subject: [PATCH 310/485] Crash report with Sentry --- gns3server/crash_report.py | 53 ++++++++++++++++++++++ gns3server/handlers/api/version_handler.py | 3 +- gns3server/main.py | 3 +- gns3server/web/route.py | 4 ++ requirements.txt | 1 + setup.py | 3 +- 6 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 gns3server/crash_report.py diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py new file mode 100644 index 00000000..c8afca1b --- /dev/null +++ b/gns3server/crash_report.py @@ -0,0 +1,53 @@ +# -*- 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 . + +import raven +import json + +from .version import __version__ +from .config import Config + +import logging +log = logging.getLogger(__name__) + + +class CrashReport: + + """ + Report crash to a third party service + """ + + DSN = "aiohttp+https://50af75d8641d4ea7a4ea6b38c7df6cf9:41d54936f8f14e558066262e2ec8bbeb@app.getsentry.com/38482" + _instance = None + + def capture_exception(self, request): + server_config = Config.instance().get_section_config("Server") + if server_config.getboolean("report_errors"): + if self._client is None: + self._client = raven.Client(CrashReport.DSN, release=__version__) + self._client.http_context({ + "method": request.method, + "url": request.path, + "data": request.json, + }) + self._client.captureException() + + @classmethod + def instance(cls): + if cls._instance is None: + cls._instance = CrashReport() + return cls._instance diff --git a/gns3server/handlers/api/version_handler.py b/gns3server/handlers/api/version_handler.py index a935e3ca..000e932a 100644 --- a/gns3server/handlers/api/version_handler.py +++ b/gns3server/handlers/api/version_handler.py @@ -43,6 +43,5 @@ class VersionHandler: }) def check_version(request, response): if request.json["version"] != __version__: - raise HTTPConflict(text="Client version {} differs with server version {}".format(request.json["version"], - __version__)) + raise HTTPConflict(text="Client version {} differs with server version {}".format(request.json["version"], __version__)) response.json({"version": __version__}) diff --git a/gns3server/main.py b/gns3server/main.py index 889078d2..056d2b62 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -28,7 +28,7 @@ from gns3server.web.logger import init_logger from gns3server.version import __version__ from gns3server.config import Config from gns3server.modules.project import Project - +from gns3server.crash_report import CrashReport import logging log = logging.getLogger(__name__) @@ -168,6 +168,7 @@ def main(): Project.clean_project_directory() + CrashReport.instance() host = server_config["host"] port = int(server_config["port"]) server = Server(host, port) diff --git a/gns3server/web/route.py b/gns3server/web/route.py index a26a8f8b..13344bf9 100644 --- a/gns3server/web/route.py +++ b/gns3server/web/route.py @@ -27,6 +27,7 @@ log = logging.getLogger(__name__) from ..modules.vm_error import VMError from .response import Response +from ..crash_report import CrashReport @asyncio.coroutine @@ -39,6 +40,8 @@ def parse_request(request, input_schema): request.json = json.loads(body.decode('utf-8')) except ValueError as e: raise aiohttp.web.HTTPBadRequest(text="Invalid JSON {}".format(e)) + else: + request.json = {} try: jsonschema.validate(request.json, input_schema) except jsonschema.ValidationError as e: @@ -135,6 +138,7 @@ class Route(object): log.error("Uncaught exception detected: {type}".format(type=type(e)), exc_info=1) response = Response(route=route) response.set_status(500) + CrashReport.instance().capture_exception(request) exc_type, exc_value, exc_tb = sys.exc_info() lines = traceback.format_exception(exc_type, exc_value, exc_tb) if api_version is not None: diff --git a/requirements.txt b/requirements.txt index cd8c3f8d..db4f67fd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ apache-libcloud==0.16.0 requests==2.5.0 aiohttp==0.14.4 Jinja2==2.7.3 +raven==5.2.0 diff --git a/setup.py b/setup.py index 238efc3d..4b716076 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,8 @@ dependencies = ["aiohttp==0.14.4", "jsonschema==2.4.0", "apache-libcloud==0.16.0", "requests==2.5.0", - "Jinja2==2.7.3"] + "Jinja2==2.7.3", + "raven==5.2.0"] if sys.version_info == (3, 3): dependencies.append("asyncio==3.4.2") From 9153b42b9d58b037af3f9e065045eba0f705e336 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 24 Feb 2015 20:22:10 +0100 Subject: [PATCH 311/485] Fix crash in crash report --- gns3server/crash_report.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py index c8afca1b..06d11803 100644 --- a/gns3server/crash_report.py +++ b/gns3server/crash_report.py @@ -34,6 +34,9 @@ class CrashReport: DSN = "aiohttp+https://50af75d8641d4ea7a4ea6b38c7df6cf9:41d54936f8f14e558066262e2ec8bbeb@app.getsentry.com/38482" _instance = None + def __init__(self): + self._client = None + def capture_exception(self, request): server_config = Config.instance().get_section_config("Server") if server_config.getboolean("report_errors"): From 46b348e46a0a00740be745a1ed9d6289163a3035 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 24 Feb 2015 21:53:38 +0100 Subject: [PATCH 312/485] VM concurrency --- gns3server/handlers/api/version_handler.py | 11 +++++++++++ gns3server/web/route.py | 19 +++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/gns3server/handlers/api/version_handler.py b/gns3server/handlers/api/version_handler.py index 000e932a..af01fe14 100644 --- a/gns3server/handlers/api/version_handler.py +++ b/gns3server/handlers/api/version_handler.py @@ -20,6 +20,8 @@ from ...schemas.version import VERSION_SCHEMA from ...version import __version__ from aiohttp.web import HTTPConflict +import asyncio + class VersionHandler: @@ -45,3 +47,12 @@ class VersionHandler: if request.json["version"] != __version__: raise HTTPConflict(text="Client version {} differs with server version {}".format(request.json["version"], __version__)) response.json({"version": __version__}) + + @staticmethod + @Route.get( + r"/sleep/{vm_id}", + description="Retrieve the server version number", + output=VERSION_SCHEMA) + def sleep(request, response): + yield from asyncio.sleep(1) + response.json({"version": __version__}) diff --git a/gns3server/web/route.py b/gns3server/web/route.py index 13344bf9..4e411b5a 100644 --- a/gns3server/web/route.py +++ b/gns3server/web/route.py @@ -63,6 +63,8 @@ class Route(object): _routes = [] _documentation = {} + _vms_lock = {} + @classmethod def get(cls, path, *args, **kw): return cls._route('GET', path, *args, **kw) @@ -150,9 +152,22 @@ class Route(object): return response - cls._routes.append((method, cls._path, control_schema)) + @asyncio.coroutine + def vm_concurrency(request): + """ + To avoid strange effect we prevent concurrency + between the same instance of the vm + """ + + if "vm_id" in request.match_info: + cls._vms_lock.setdefault(request.match_info["vm_id"], asyncio.Lock()) + with (yield from cls._vms_lock[request.match_info["vm_id"]]): + response = yield from control_schema(request) + return response + + cls._routes.append((method, cls._path, vm_concurrency)) - return control_schema + return vm_concurrency return register @classmethod From 6bb7ab20b3c8b51cfa8653281b30dfc07ae7481d Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 24 Feb 2015 15:26:03 -0700 Subject: [PATCH 313/485] Fixes vm concurrency and support for devices. --- gns3server/web/route.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/gns3server/web/route.py b/gns3server/web/route.py index 4e411b5a..bdc4c62e 100644 --- a/gns3server/web/route.py +++ b/gns3server/web/route.py @@ -159,10 +159,15 @@ class Route(object): between the same instance of the vm """ - if "vm_id" in request.match_info: - cls._vms_lock.setdefault(request.match_info["vm_id"], asyncio.Lock()) - with (yield from cls._vms_lock[request.match_info["vm_id"]]): + if "vm_id" in request.match_info or "device_id" in request.match_info: + vm_id = request.match_info.get("vm_id") + if vm_id is None: + vm_id = request.match_info["device_id"] + cls._vms_lock.setdefault(vm_id, asyncio.Lock()) + with (yield from cls._vms_lock[vm_id]): response = yield from control_schema(request) + else: + response = yield from control_schema(request) return response cls._routes.append((method, cls._path, vm_concurrency)) From 550cc7f5080bf09c5a7617504d617e6ed8601db8 Mon Sep 17 00:00:00 2001 From: grossmj Date: Tue, 24 Feb 2015 21:02:37 -0700 Subject: [PATCH 314/485] Rename vms_lock to vm_locks. --- gns3server/web/route.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gns3server/web/route.py b/gns3server/web/route.py index bdc4c62e..8afaae20 100644 --- a/gns3server/web/route.py +++ b/gns3server/web/route.py @@ -63,7 +63,7 @@ class Route(object): _routes = [] _documentation = {} - _vms_lock = {} + _vm_locks = {} @classmethod def get(cls, path, *args, **kw): @@ -163,8 +163,8 @@ class Route(object): vm_id = request.match_info.get("vm_id") if vm_id is None: vm_id = request.match_info["device_id"] - cls._vms_lock.setdefault(vm_id, asyncio.Lock()) - with (yield from cls._vms_lock[vm_id]): + cls._vm_locks.setdefault(vm_id, asyncio.Lock()) + with (yield from cls._vm_locks[vm_id]): response = yield from control_schema(request) else: response = yield from control_schema(request) From 3528efb1e02925862a3ca3ee114c7a3785063fbe Mon Sep 17 00:00:00 2001 From: grossmj Date: Tue, 24 Feb 2015 23:12:09 -0700 Subject: [PATCH 315/485] Fixes packet capture for devices when spaces are present in the output file. --- gns3server/modules/dynamips/nodes/atm_switch.py | 2 +- gns3server/modules/dynamips/nodes/ethernet_hub.py | 2 +- gns3server/modules/dynamips/nodes/ethernet_switch.py | 2 +- gns3server/modules/dynamips/nodes/frame_relay_switch.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gns3server/modules/dynamips/nodes/atm_switch.py b/gns3server/modules/dynamips/nodes/atm_switch.py index 36b8f343..bf56945b 100644 --- a/gns3server/modules/dynamips/nodes/atm_switch.py +++ b/gns3server/modules/dynamips/nodes/atm_switch.py @@ -374,7 +374,7 @@ class ATMSwitch(Device): raise DynamipsError("Port {} has already a filter applied".format(port_number)) yield from nio.bind_filter("both", "capture") - yield from nio.setup_filter("both", "{} {}".format(data_link_type, output_file)) + yield from nio.setup_filter("both", '{} "{}"'.format(data_link_type, output_file)) log.info('ATM switch "{name}" [{id}]: starting packet capture on port {port}'.format(name=self._name, id=self._id, diff --git a/gns3server/modules/dynamips/nodes/ethernet_hub.py b/gns3server/modules/dynamips/nodes/ethernet_hub.py index 33807c97..23fb2129 100644 --- a/gns3server/modules/dynamips/nodes/ethernet_hub.py +++ b/gns3server/modules/dynamips/nodes/ethernet_hub.py @@ -155,7 +155,7 @@ class EthernetHub(Bridge): raise DynamipsError("Port {} has already a filter applied".format(port_number)) yield from nio.bind_filter("both", "capture") - yield from nio.setup_filter("both", "{} {}".format(data_link_type, output_file)) + yield from nio.setup_filter("both", '{} "{}"'.format(data_link_type, output_file)) log.info('Ethernet hub "{name}" [{id}]: starting packet capture on port {port}'.format(name=self._name, id=self._id, diff --git a/gns3server/modules/dynamips/nodes/ethernet_switch.py b/gns3server/modules/dynamips/nodes/ethernet_switch.py index 1f3abdbe..3814e720 100644 --- a/gns3server/modules/dynamips/nodes/ethernet_switch.py +++ b/gns3server/modules/dynamips/nodes/ethernet_switch.py @@ -305,7 +305,7 @@ class EthernetSwitch(Device): raise DynamipsError("Port {} has already a filter applied".format(port_number)) yield from nio.bind_filter("both", "capture") - yield from nio.setup_filter("both", "{} {}".format(data_link_type, output_file)) + yield from nio.setup_filter("both", '{} "{}"'.format(data_link_type, output_file)) log.info('Ethernet switch "{name}" [{id}]: starting packet capture on port {port}'.format(name=self._name, id=self._id, diff --git a/gns3server/modules/dynamips/nodes/frame_relay_switch.py b/gns3server/modules/dynamips/nodes/frame_relay_switch.py index d30578be..1a21dd56 100644 --- a/gns3server/modules/dynamips/nodes/frame_relay_switch.py +++ b/gns3server/modules/dynamips/nodes/frame_relay_switch.py @@ -280,7 +280,7 @@ class FrameRelaySwitch(Device): raise DynamipsError("Port {} has already a filter applied".format(port_number)) yield from nio.bind_filter("both", "capture") - yield from nio.setup_filter("both", "{} {}".format(data_link_type, output_file)) + yield from nio.setup_filter("both", '{} "{}"'.format(data_link_type, output_file)) log.info('Frame relay switch "{name}" [{id}]: starting packet capture on port {port}'.format(name=self._name, id=self._id, From 36bb510ac1ea2924bc604b4893f5a13d29959112 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 25 Feb 2015 09:47:55 +0100 Subject: [PATCH 316/485] Add api limitations in the documentation --- docs/general.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/general.rst b/docs/general.rst index 9c0855f7..6d08118f 100644 --- a/docs/general.rst +++ b/docs/general.rst @@ -15,3 +15,21 @@ JSON like that "status": 409, "message": "Conflict" } + +Limitations +============ + +Concurrency +------------ + +A VM can't process multiple request in the same time. But you can make +multiple request on multiple VM. It's transparent for the client +when the first request on a VM start a lock is acquire for this VM id +and released for the next request at the end. You can safely send all +the requests in the same time and let the server manage an efficent concurrency. + +We think it can be a little slower for some operations, but it's remove a big +complexity for the client due to the fact only some command on some VM can be +concurrent. + + From 545acd1f06e173942fe39fd17515e2853304ee51 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 25 Feb 2015 10:29:20 +0100 Subject: [PATCH 317/485] Limitation documentation --- docs/general.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/general.rst b/docs/general.rst index 6d08118f..69afacfc 100644 --- a/docs/general.rst +++ b/docs/general.rst @@ -33,3 +33,12 @@ complexity for the client due to the fact only some command on some VM can be concurrent. +Authentification +----------------- + +In this version of the API you have no authentification system. If you +listen on your network interface instead of localhost be carefull. Due +to the nature of the multiple supported VM it's easy for an user to +upload and run code on your machine. + + From 7c2329d8709c268872c970766a3238d60874ce4c Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 25 Feb 2015 11:19:16 +0100 Subject: [PATCH 318/485] Garbage collect the lock --- gns3server/handlers/api/version_handler.py | 9 --------- gns3server/web/route.py | 11 +++++++++-- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/gns3server/handlers/api/version_handler.py b/gns3server/handlers/api/version_handler.py index af01fe14..930c733e 100644 --- a/gns3server/handlers/api/version_handler.py +++ b/gns3server/handlers/api/version_handler.py @@ -47,12 +47,3 @@ class VersionHandler: if request.json["version"] != __version__: raise HTTPConflict(text="Client version {} differs with server version {}".format(request.json["version"], __version__)) response.json({"version": __version__}) - - @staticmethod - @Route.get( - r"/sleep/{vm_id}", - description="Retrieve the server version number", - output=VERSION_SCHEMA) - def sleep(request, response): - yield from asyncio.sleep(1) - response.json({"version": __version__}) diff --git a/gns3server/web/route.py b/gns3server/web/route.py index 8afaae20..bf30e35f 100644 --- a/gns3server/web/route.py +++ b/gns3server/web/route.py @@ -163,9 +163,16 @@ class Route(object): vm_id = request.match_info.get("vm_id") if vm_id is None: vm_id = request.match_info["device_id"] - cls._vm_locks.setdefault(vm_id, asyncio.Lock()) - with (yield from cls._vm_locks[vm_id]): + cls._vm_locks.setdefault(vm_id, {"lock": asyncio.Lock(), "concurrency": 0}) + cls._vm_locks[vm_id]["concurrency"] += 1 + + with (yield from cls._vm_locks[vm_id]["lock"]): response = yield from control_schema(request) + cls._vm_locks[vm_id]["concurrency"] -= 1 + + # No more waiting requests, garbage collect the lock + if cls._vm_locks[vm_id]["concurrency"] <= 0: + del cls._vm_locks[vm_id] else: response = yield from control_schema(request) return response From 0713724a97ffd0648fd5235095f78f7df9f3f87f Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 25 Feb 2015 11:42:02 +0100 Subject: [PATCH 319/485] Properly handle when client cancel's query --- gns3server/crash_report.py | 6 +++++- gns3server/web/route.py | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py index 06d11803..44f96588 100644 --- a/gns3server/crash_report.py +++ b/gns3server/crash_report.py @@ -17,6 +17,7 @@ import raven import json +import asyncio.futures from .version import __version__ from .config import Config @@ -47,7 +48,10 @@ class CrashReport: "url": request.path, "data": request.json, }) - self._client.captureException() + try: + self._client.captureException() + except asyncio.futures.TimeoutError: + pass # We don't care if we can send the bug report @classmethod def instance(cls): diff --git a/gns3server/web/route.py b/gns3server/web/route.py index bf30e35f..f6f9407f 100644 --- a/gns3server/web/route.py +++ b/gns3server/web/route.py @@ -136,6 +136,11 @@ class Route(object): response = Response(route=route) response.set_status(409) response.json({"message": str(e), "status": 409}) + except asyncio.futures.CancelledError as e: + log.error("Request canceled") + response = Response(route=route) + response.set_status(408) + response.json({"message": "Request canceled", "status": 408}) except Exception as e: log.error("Uncaught exception detected: {type}".format(type=type(e)), exc_info=1) response = Response(route=route) From 4ea25739e5539b15c1f87b1819ceda42f7b93349 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 25 Feb 2015 15:42:01 +0100 Subject: [PATCH 320/485] Correctly check if qemu is running Fixes #71 Related to #70 --- gns3server/modules/qemu/qemu_vm.py | 6 +++++- tests/modules/qemu/test_qemu_vm.py | 29 +++++++++++++++++++++++------ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index 54ae3e4c..6210fe7d 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -805,7 +805,11 @@ class QemuVM(BaseVM): """ if self._process: - return True + print(self._process.returncode) + if self._process.returncode is None: + return True + else: + self._process = None return False def command(self): diff --git a/tests/modules/qemu/test_qemu_vm.py b/tests/modules/qemu/test_qemu_vm.py index 1cc9596e..76b4a97c 100644 --- a/tests/modules/qemu/test_qemu_vm.py +++ b/tests/modules/qemu/test_qemu_vm.py @@ -62,20 +62,37 @@ def vm(project, manager, fake_qemu_binary, fake_qemu_img_binary): return QemuVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager, qemu_path=fake_qemu_binary) +@pytest.fixture(scope="function") +def running_subprocess_mock(): + mm = MagicMock() + mm.returncode = None + return mm + + def test_vm(project, manager, fake_qemu_binary): vm = QemuVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager, qemu_path=fake_qemu_binary) assert vm.name == "test" assert vm.id == "00010203-0405-0607-0809-0a0b0c0d0e0f" -def test_start(loop, vm): - with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): +def test_is_running(vm, running_subprocess_mock): + + vm._process = None + assert vm.is_running() is False + vm._process = running_subprocess_mock + assert vm.is_running() + vm._process.returncode = -1 + assert vm.is_running() is False + + +def test_start(loop, vm, running_subprocess_mock): + with asyncio_patch("asyncio.create_subprocess_exec", return_value=running_subprocess_mock): loop.run_until_complete(asyncio.async(vm.start())) assert vm.is_running() -def test_stop(loop, vm): - process = MagicMock() +def test_stop(loop, vm, running_subprocess_mock): + process = running_subprocess_mock # Wait process kill success future = asyncio.Future() @@ -217,9 +234,9 @@ def test_control_vm(vm, loop): assert res is None -def test_control_vm_expect_text(vm, loop): +def test_control_vm_expect_text(vm, loop, running_subprocess_mock): - vm._process = MagicMock() + vm._process = running_subprocess_mock vm._monitor = 4242 reader = MagicMock() writer = MagicMock() From 47f8ac00c03fdecafbc831c68a565d36c3361121 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 25 Feb 2015 16:04:18 +0100 Subject: [PATCH 321/485] Remove debug --- gns3server/modules/qemu/qemu_vm.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index 6210fe7d..4a07d587 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -805,7 +805,6 @@ class QemuVM(BaseVM): """ if self._process: - print(self._process.returncode) if self._process.returncode is None: return True else: From 818676ce5ed8348925c1bf57323a0806b625ab80 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 25 Feb 2015 16:26:17 +0100 Subject: [PATCH 322/485] Support relative path in iou --- gns3server/modules/iou/iou_vm.py | 5 +++++ tests/modules/iou/test_iou_vm.py | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 79682479..e2f3ea80 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -39,6 +39,7 @@ from ..nios.nio_tap import NIOTAP from ..nios.nio_generic_ethernet import NIOGenericEthernet from ..base_vm import BaseVM from .ioucon import start_ioucon +from ...config import Config import gns3server.utils.asyncio @@ -131,6 +132,10 @@ class IOUVM(BaseVM): :params path: Path to the binary """ + if path[0] != "/": + server_config = Config.instance().get_section_config("Server") + path = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), path) + self._path = path if not os.path.isfile(self._path) or not os.path.exists(self._path): if os.path.islink(self._path): diff --git a/tests/modules/iou/test_iou_vm.py b/tests/modules/iou/test_iou_vm.py index 4ee16327..90d59759 100644 --- a/tests/modules/iou/test_iou_vm.py +++ b/tests/modules/iou/test_iou_vm.py @@ -175,6 +175,14 @@ def test_path(vm, fake_iou_bin): assert vm.path == fake_iou_bin + +def test_path_relative(vm, fake_iou_bin, tmpdir): + + with patch("gns3server.config.Config.get_section_config", return_value={"images_path": str(tmpdir)}): + vm.path = "iou.bin" + assert vm.path == fake_iou_bin + + def test_path_invalid_bin(vm, tmpdir): path = str(tmpdir / "test.bin") From 8434a286b6d4e7436340aed088fa01444dd4f423 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 25 Feb 2015 16:35:13 +0100 Subject: [PATCH 323/485] Fix IOU old project import Fixes #69 --- gns3server/modules/iou/__init__.py | 10 ++++++++++ tests/modules/iou/test_iou_vm.py | 6 +++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/gns3server/modules/iou/__init__.py b/gns3server/modules/iou/__init__.py index c3ba15b4..30d35915 100644 --- a/gns3server/modules/iou/__init__.py +++ b/gns3server/modules/iou/__init__.py @@ -62,3 +62,13 @@ class IOU(BaseManager): """ return self._used_application_ids.get(vm_id, 1) + + def get_legacy_vm_workdir_name(legacy_vm_id): + """ + Returns the name of the legacy working directory (pre 1.3) name for a VM. + + :param legacy_vm_id: legacy VM identifier (integer) + :returns: working directory name + """ + + return "device-{}".format(legacy_vm_id) diff --git a/tests/modules/iou/test_iou_vm.py b/tests/modules/iou/test_iou_vm.py index 90d59759..cb5507cc 100644 --- a/tests/modules/iou/test_iou_vm.py +++ b/tests/modules/iou/test_iou_vm.py @@ -175,7 +175,6 @@ def test_path(vm, fake_iou_bin): assert vm.path == fake_iou_bin - def test_path_relative(vm, fake_iou_bin, tmpdir): with patch("gns3server.config.Config.get_section_config", return_value={"images_path": str(tmpdir)}): @@ -302,3 +301,8 @@ def test_stop_capture(vm, tmpdir, manager, free_console_port, loop): assert vm._adapters[0].get_nio(0).capturing loop.run_until_complete(asyncio.async(vm.stop_capture(0, 0))) assert vm._adapters[0].get_nio(0).capturing is False + + +def test_get_legacy_vm_workdir_name(): + + assert IOU.get_legacy_vm_workdir_name(42) == "device-42" From f12d3f07f7844f7d94b014365bd8ba2128d44466 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 25 Feb 2015 18:23:41 +0100 Subject: [PATCH 324/485] Drop the old -files in the project --- gns3server/modules/base_manager.py | 16 ++++++++++++++-- tests/modules/test_manager.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index acf9c1ab..4bafe78c 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -158,7 +158,7 @@ class BaseManager: project = ProjectManager.instance().get_project(project_id) - # If it's not an UUID + # If it's not an UUID, old topology if vm_id and (isinstance(vm_id, int) or len(vm_id) != 36): legacy_id = int(vm_id) vm_id = str(uuid4()) @@ -173,7 +173,19 @@ class BaseManager: try: yield from wait_run_in_executor(shutil.move, vm_working_dir, new_vm_working_dir) except OSError as e: - raise aiohttp.web.HTTPInternalServerError(text="Could not move VM working directory: {}".format(e)) + raise aiohttp.web.HTTPInternalServerError(text="Could not move VM working directory: {} to {} {}".format(vm_working_dir, new_vm_working_dir, e)) + + if os.listdir(module_path) == []: + try: + os.rmdir(module_path) + except OSError as e: + raise aiohttp.web.HTTPInternalServerError(text="Could not delete {}: {}".format(module_path, e)) + + if os.listdir(project_files_dir) == []: + try: + os.rmdir(project_files_dir) + except OSError as e: + raise aiohttp.web.HTTPInternalServerError(text="Could not delete {}: {}".format(project_files_dir, e)) if not vm_id: vm_id = str(uuid4()) diff --git a/tests/modules/test_manager.py b/tests/modules/test_manager.py index b38eb804..54996169 100644 --- a/tests/modules/test_manager.py +++ b/tests/modules/test_manager.py @@ -62,6 +62,35 @@ def test_create_vm_old_topology(loop, project, tmpdir, port_manager): vm = loop.run_until_complete(vpcs.create_vm("PC 1", project.id, vm_id)) assert len(vm.id) == 36 + assert os.path.exists(os.path.join(project_dir, "testold-files")) is False + + vm_dir = os.path.join(project_dir, "project-files", "vpcs", vm.id) + with open(os.path.join(vm_dir, "startup.vpc")) as f: + assert f.read() == "1" + + +def test_create_vm_old_topology_with_garbage_in_project_dir(loop, project, tmpdir, port_manager): + + with patch("gns3server.config.Config.get_section_config", return_value={"local": True}): + # Create an old topology directory + project_dir = str(tmpdir / "testold") + vm_dir = os.path.join(project_dir, "testold-files", "vpcs", "pc-1") + project.path = project_dir + os.makedirs(vm_dir, exist_ok=True) + with open(os.path.join(vm_dir, "startup.vpc"), "w+") as f: + f.write("1") + with open(os.path.join(os.path.join(project_dir, "testold-files"), "crash.log"), "w+") as f: + f.write("1") + + VPCS._instance = None + vpcs = VPCS.instance() + vpcs.port_manager = port_manager + vm_id = 1 + vm = loop.run_until_complete(vpcs.create_vm("PC 1", project.id, vm_id)) + assert len(vm.id) == 36 + + assert os.path.exists(os.path.join(project_dir, "testold-files")) is True + vm_dir = os.path.join(project_dir, "project-files", "vpcs", vm.id) with open(os.path.join(vm_dir, "startup.vpc")) as f: assert f.read() == "1" From 349d9d45403068e6ae8213aeb5d1168c378555d0 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 25 Feb 2015 11:52:52 -0700 Subject: [PATCH 325/485] Fixes small issues when deleting Dynamips devices. --- gns3server/modules/dynamips/nodes/atm_switch.py | 3 ++- gns3server/modules/dynamips/nodes/bridge.py | 6 ++++-- gns3server/modules/dynamips/nodes/ethernet_hub.py | 2 +- gns3server/modules/dynamips/nodes/ethernet_switch.py | 3 ++- gns3server/modules/dynamips/nodes/frame_relay_switch.py | 3 ++- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/gns3server/modules/dynamips/nodes/atm_switch.py b/gns3server/modules/dynamips/nodes/atm_switch.py index bf56945b..0fb9e1e1 100644 --- a/gns3server/modules/dynamips/nodes/atm_switch.py +++ b/gns3server/modules/dynamips/nodes/atm_switch.py @@ -116,7 +116,8 @@ class ATMSwitch(Device): log.info('ATM switch "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id)) except DynamipsError: log.debug("Could not properly delete ATM switch {}".format(self._name)) - self._hypervisor.devices.remove(self) + if self._hypervisor and self in self._hypervisor.devices: + self._hypervisor.devices.remove(self) if self._hypervisor and not self._hypervisor.devices: yield from self.hypervisor.stop() diff --git a/gns3server/modules/dynamips/nodes/bridge.py b/gns3server/modules/dynamips/nodes/bridge.py index 174fbb86..7dc4e31b 100644 --- a/gns3server/modules/dynamips/nodes/bridge.py +++ b/gns3server/modules/dynamips/nodes/bridge.py @@ -80,8 +80,10 @@ class Bridge(Device): Deletes this bridge. """ - self._hypervisor.devices.remove(self) - yield from self._hypervisor.send('nio_bridge delete "{}"'.format(self._name)) + if self._hypervisor and self in self._hypervisor.devices: + self._hypervisor.devices.remove(self) + if self._hypervisor and not self._hypervisor.devices: + yield from self._hypervisor.send('nio_bridge delete "{}"'.format(self._name)) @asyncio.coroutine def add_nio(self, nio): diff --git a/gns3server/modules/dynamips/nodes/ethernet_hub.py b/gns3server/modules/dynamips/nodes/ethernet_hub.py index 23fb2129..d41e5117 100644 --- a/gns3server/modules/dynamips/nodes/ethernet_hub.py +++ b/gns3server/modules/dynamips/nodes/ethernet_hub.py @@ -74,7 +74,7 @@ class EthernetHub(Bridge): Deletes this hub. """ - for nio in self._nios.values(): + for nio in self._nios: if nio and isinstance(nio, NIOUDP): self.manager.port_manager.release_udp_port(nio.lport) diff --git a/gns3server/modules/dynamips/nodes/ethernet_switch.py b/gns3server/modules/dynamips/nodes/ethernet_switch.py index 3814e720..a748a9b3 100644 --- a/gns3server/modules/dynamips/nodes/ethernet_switch.py +++ b/gns3server/modules/dynamips/nodes/ethernet_switch.py @@ -124,7 +124,8 @@ class EthernetSwitch(Device): log.info('Ethernet switch "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id)) except DynamipsError: log.debug("Could not properly delete Ethernet switch {}".format(self._name)) - self._hypervisor.devices.remove(self) + if self._hypervisor and self in self._hypervisor.devices: + self._hypervisor.devices.remove(self) if self._hypervisor and not self._hypervisor.devices: yield from self.hypervisor.stop() diff --git a/gns3server/modules/dynamips/nodes/frame_relay_switch.py b/gns3server/modules/dynamips/nodes/frame_relay_switch.py index 1a21dd56..b46055b4 100644 --- a/gns3server/modules/dynamips/nodes/frame_relay_switch.py +++ b/gns3server/modules/dynamips/nodes/frame_relay_switch.py @@ -115,7 +115,8 @@ class FrameRelaySwitch(Device): log.info('Frame Relay switch "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id)) except DynamipsError: log.debug("Could not properly delete Frame relay switch {}".format(self._name)) - self._hypervisor.devices.remove(self) + if self._hypervisor and self in self._hypervisor.devices: + self._hypervisor.devices.remove(self) if self._hypervisor and not self._hypervisor.devices: yield from self.hypervisor.stop() From 54fc873be58d9e279254f1068004b291a7b9ae6a Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 25 Feb 2015 16:05:57 -0700 Subject: [PATCH 326/485] Prevent multiple projects with the same ID to be created. --- gns3server/handlers/api/project_handler.py | 7 ++++++ gns3server/modules/base_manager.py | 21 +++++++++------- gns3server/modules/dynamips/nodes/router.py | 1 + gns3server/modules/iou/iou_vm.py | 2 ++ gns3server/modules/project.py | 5 +++- gns3server/modules/project_manager.py | 17 +++++++++++-- gns3server/modules/qemu/qemu_vm.py | 1 + .../modules/virtualbox/virtualbox_vm.py | 1 + gns3server/modules/vpcs/vpcs_vm.py | 9 +++---- tests/handlers/api/test_project.py | 24 +++++++++---------- 10 files changed, 61 insertions(+), 27 deletions(-) diff --git a/gns3server/handlers/api/project_handler.py b/gns3server/handlers/api/project_handler.py index 00559dca..f9bd0e72 100644 --- a/gns3server/handlers/api/project_handler.py +++ b/gns3server/handlers/api/project_handler.py @@ -27,6 +27,10 @@ class ProjectHandler: @Route.post( r"/projects", description="Create a new project on the server", + status_codes={ + 201: "Project created", + 409: "Project already created" + }, output=PROJECT_OBJECT_SCHEMA, input=PROJECT_CREATE_SCHEMA) def create_project(request, response): @@ -37,6 +41,7 @@ class ProjectHandler: project_id=request.json.get("project_id"), temporary=request.json.get("temporary", False) ) + response.set_status(201) response.json(p) @classmethod @@ -115,6 +120,7 @@ class ProjectHandler: yield from project.close() for module in MODULES: yield from module.instance().project_closed(project.path) + pm.remove_project(project.id) response.set_status(204) @classmethod @@ -133,4 +139,5 @@ class ProjectHandler: pm = ProjectManager.instance() project = pm.get_project(request.match_info["project_id"]) yield from project.delete() + pm.remove_project(project.id) response.set_status(204) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 4bafe78c..41e63ced 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -165,6 +165,7 @@ class BaseManager: if hasattr(self, "get_legacy_vm_workdir_name"): # move old project VM files to a new location + log.info("Converting old project...") project_name = os.path.basename(project.path) project_files_dir = os.path.join(project.path, "{}-files".format(project_name)) module_path = os.path.join(project_files_dir, self.module_name.lower()) @@ -175,17 +176,21 @@ class BaseManager: except OSError as e: raise aiohttp.web.HTTPInternalServerError(text="Could not move VM working directory: {} to {} {}".format(vm_working_dir, new_vm_working_dir, e)) - if os.listdir(module_path) == []: - try: + try: + if not os.listdir(module_path): os.rmdir(module_path) - except OSError as e: - raise aiohttp.web.HTTPInternalServerError(text="Could not delete {}: {}".format(module_path, e)) + except OSError as e: + raise aiohttp.web.HTTPInternalServerError(text="Could not delete {}: {}".format(module_path, e)) + except FileNotFoundError as e: + log.warning(e) - if os.listdir(project_files_dir) == []: - try: + try: + if not os.listdir(project_files_dir): os.rmdir(project_files_dir) - except OSError as e: - raise aiohttp.web.HTTPInternalServerError(text="Could not delete {}: {}".format(project_files_dir, e)) + except OSError as e: + raise aiohttp.web.HTTPInternalServerError(text="Could not delete {}: {}".format(project_files_dir, e)) + except FileNotFoundError as e: + log.warning(e) if not vm_id: vm_id = str(uuid4()) diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index 7b456fed..3d53b16b 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -310,6 +310,7 @@ class Router(BaseVM): # router is already closed return + log.debug('Router "{name}" [{id}] is closing'.format(name=self._name, id=self._id)) if self._dynamips_id in self._dynamips_ids[self._project.id]: self._dynamips_ids[self._project.id].remove(self._dynamips_id) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index e2f3ea80..fa6d4377 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -105,6 +105,8 @@ class IOUVM(BaseVM): @asyncio.coroutine def close(self): + log.debug('IOU "{name}" [{id}] is closing'.format(name=self._name, id=self._id)) + if self._console: self._manager.port_manager.release_tcp_port(self._console) self._console = None diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index 52aea2db..e2044ec1 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -70,7 +70,7 @@ class Project: raise aiohttp.web.HTTPInternalServerError(text="Could not create project directory: {}".format(e)) self.path = path - log.debug("Create project {id} in directory {path}".format(path=self._path, id=self._id)) + log.info("Project {id} with path '{path}' created".format(path=self._path, id=self._id)) def __json__(self): @@ -278,8 +278,11 @@ class Project: if cleanup and os.path.exists(self.path): try: yield from wait_run_in_executor(shutil.rmtree, self.path) + log.info("Project {id} with path '{path}' deleted".format(path=self._path, id=self._id)) except OSError as e: raise aiohttp.web.HTTPInternalServerError(text="Could not delete the project directory: {}".format(e)) + else: + log.info("Project {id} with path '{path}' closed".format(path=self._path, id=self._id)) port_manager = PortManager.instance() if port_manager.tcp_ports: diff --git a/gns3server/modules/project_manager.py b/gns3server/modules/project_manager.py index 87ed9288..82737b2c 100644 --- a/gns3server/modules/project_manager.py +++ b/gns3server/modules/project_manager.py @@ -60,13 +60,26 @@ class ProjectManager: raise aiohttp.web.HTTPNotFound(text="Project ID {} doesn't exist".format(project_id)) return self._projects[project_id] - def create_project(self, **kwargs): + def create_project(self, project_id=None, path=None, temporary=False): """ Create a project and keep a references to it in project manager. See documentation of Project for arguments """ - project = Project(**kwargs) + if project_id is not None and project_id in self._projects: + raise aiohttp.web.HTTPConflict(text="Project ID {} is already in use on this server".format(project_id)) + project = Project(project_id=project_id, path=path, temporary=temporary) self._projects[project.id] = project return project + + def remove_project(self, project_id): + """ + Removes a Project instance from the list of projects in use. + + :param project_id: Project identifier + """ + + if project_id not in self._projects: + raise aiohttp.web.HTTPNotFound(text="Project ID {} doesn't exist".format(project_id)) + del self._projects[project_id] diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index 4a07d587..5321aac6 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -647,6 +647,7 @@ class QemuVM(BaseVM): @asyncio.coroutine def close(self): + log.debug('QEMU VM "{name}" [{id}] is closing'.format(name=self._name, id=self._id)) yield from self.stop() if self._console: self._manager.port_manager.release_tcp_port(self._console) diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index a7728f94..a7b61fe6 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -297,6 +297,7 @@ class VirtualBoxVM(BaseVM): # VM is already closed return + log.debug("VirtualBox VM '{name}' [{id}] is closing".format(name=self.name, id=self.id)) if self._console: self._manager.port_manager.release_tcp_port(self._console) self._console = None diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index 6549ebbc..e72d1dde 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -72,6 +72,7 @@ class VPCSVM(BaseVM): @asyncio.coroutine def close(self): + log.debug("VPCS {name} [{id}] is closing".format(name=self._name, id=self._id)) if self._console: self._manager.port_manager.release_tcp_port(self._console) self._console = None @@ -297,10 +298,10 @@ class VPCSVM(BaseVM): port_number=port_number)) self._ethernet_adapter.add_nio(port_number, nio) - log.info("VPCS {name} {id}]: {nio} added to port {port_number}".format(name=self._name, - id=self.id, - nio=nio, - port_number=port_number)) + log.info("VPCS {name} [{id}]: {nio} added to port {port_number}".format(name=self._name, + id=self.id, + nio=nio, + port_number=port_number)) return nio def port_remove_nio_binding(self, port_number): diff --git a/tests/handlers/api/test_project.py b/tests/handlers/api/test_project.py index a82834a0..659c78a8 100644 --- a/tests/handlers/api/test_project.py +++ b/tests/handlers/api/test_project.py @@ -27,14 +27,14 @@ from tests.utils import asyncio_patch def test_create_project_with_path(server, tmpdir): with patch("gns3server.config.Config.get_section_config", return_value={"local": True}): response = server.post("/projects", {"path": str(tmpdir)}) - assert response.status == 200 + assert response.status == 201 assert response.json["path"] == str(tmpdir) def test_create_project_without_dir(server): query = {} response = server.post("/projects", query, example=True) - assert response.status == 200 + assert response.status == 201 assert response.json["project_id"] is not None assert response.json["temporary"] is False @@ -42,7 +42,7 @@ def test_create_project_without_dir(server): def test_create_temporary_project(server): query = {"temporary": True} response = server.post("/projects", query) - assert response.status == 200 + assert response.status == 201 assert response.json["project_id"] is not None assert response.json["temporary"] is True @@ -50,18 +50,18 @@ def test_create_temporary_project(server): def test_create_project_with_uuid(server): query = {"project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f"} response = server.post("/projects", query) - assert response.status == 200 + assert response.status == 201 assert response.json["project_id"] == "00010203-0405-0607-0809-0a0b0c0d0e0f" def test_show_project(server): - query = {"project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f", "temporary": False} + query = {"project_id": "00010203-0405-0607-0809-0a0b0c0d0e02", "temporary": False} response = server.post("/projects", query) - assert response.status == 200 - response = server.get("/projects/00010203-0405-0607-0809-0a0b0c0d0e0f", example=True) + assert response.status == 201 + response = server.get("/projects/00010203-0405-0607-0809-0a0b0c0d0e02", example=True) assert len(response.json.keys()) == 4 assert len(response.json["location"]) > 0 - assert response.json["project_id"] == "00010203-0405-0607-0809-0a0b0c0d0e0f" + assert response.json["project_id"] == "00010203-0405-0607-0809-0a0b0c0d0e02" assert response.json["temporary"] is False @@ -73,7 +73,7 @@ def test_show_project_invalid_uuid(server): def test_update_temporary_project(server): query = {"temporary": True} response = server.post("/projects", query) - assert response.status == 200 + assert response.status == 201 query = {"temporary": False} response = server.put("/projects/{project_id}".format(project_id=response.json["project_id"]), query, example=True) assert response.status == 200 @@ -84,7 +84,7 @@ def test_update_path_project(server, tmpdir): with patch("gns3server.config.Config.get_section_config", return_value={"local": True}): response = server.post("/projects", {}) - assert response.status == 200 + assert response.status == 201 query = {"path": str(tmpdir)} response = server.put("/projects/{project_id}".format(project_id=response.json["project_id"]), query, example=True) assert response.status == 200 @@ -95,7 +95,7 @@ def test_update_path_project_non_local(server, tmpdir): with patch("gns3server.config.Config.get_section_config", return_value={"local": False}): response = server.post("/projects", {}) - assert response.status == 200 + assert response.status == 201 query = {"path": str(tmpdir)} response = server.put("/projects/{project_id}".format(project_id=response.json["project_id"]), query, example=True) assert response.status == 403 @@ -132,6 +132,6 @@ def test_close_project(server, project): assert mock.called -def test_close_project_invalid_uuid(server, project): +def test_close_project_invalid_uuid(server): response = server.post("/projects/{project_id}/close".format(project_id=uuid.uuid4())) assert response.status == 404 From de1be0961fbf9485f858bda116092619b081debb Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 25 Feb 2015 17:19:12 -0700 Subject: [PATCH 327/485] Do not return an error when creating the same project multiple times (for now). --- gns3server/modules/project_manager.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gns3server/modules/project_manager.py b/gns3server/modules/project_manager.py index 82737b2c..fa34217d 100644 --- a/gns3server/modules/project_manager.py +++ b/gns3server/modules/project_manager.py @@ -68,7 +68,9 @@ class ProjectManager: """ if project_id is not None and project_id in self._projects: - raise aiohttp.web.HTTPConflict(text="Project ID {} is already in use on this server".format(project_id)) + return self._projects[project_id] + # FIXME: should we have an error? + #raise aiohttp.web.HTTPConflict(text="Project ID {} is already in use on this server".format(project_id)) project = Project(project_id=project_id, path=path, temporary=temporary) self._projects[project.id] = project return project From 473eb0280e801da268b17b00ead461fee9db2a27 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 25 Feb 2015 17:19:37 -0700 Subject: [PATCH 328/485] Support for relative path in Dynamips. --- gns3server/modules/dynamips/nodes/router.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index 3d53b16b..e8c6dfef 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -33,6 +33,8 @@ log = logging.getLogger(__name__) from ...base_vm import BaseVM from ..dynamips_error import DynamipsError from ..nios.nio_udp import NIOUDP + +from gns3server.config import Config from gns3server.utils.asyncio import wait_run_in_executor @@ -413,6 +415,14 @@ class Router(BaseVM): :param image: path to IOS image file """ + if not os.path.dirname(image): + # this must be a relative path + server_config = Config.instance().get_section_config("Server") + image = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), image) + + if not os.path.isfile(image): + raise DynamipsError("IOS image '{}' is not accessible".format(image)) + yield from self._hypervisor.send('vm set_ios "{name}" "{image}"'.format(name=self._name, image=image)) log.info('Router "{name}" [{id}]: has a new IOS image set: "{image}"'.format(name=self._name, From 9dc713f31db226496c78640060d64a0c90480c7b Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 25 Feb 2015 17:38:36 -0700 Subject: [PATCH 329/485] Fixes race condition when deleting empty legacy project files dir. --- gns3server/modules/base_manager.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 41e63ced..8845d4cb 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -177,20 +177,14 @@ class BaseManager: raise aiohttp.web.HTTPInternalServerError(text="Could not move VM working directory: {} to {} {}".format(vm_working_dir, new_vm_working_dir, e)) try: - if not os.listdir(module_path): - os.rmdir(module_path) - except OSError as e: - raise aiohttp.web.HTTPInternalServerError(text="Could not delete {}: {}".format(module_path, e)) - except FileNotFoundError as e: - log.warning(e) + os.rmdir(module_path) + except OSError: + pass try: - if not os.listdir(project_files_dir): - os.rmdir(project_files_dir) - except OSError as e: - raise aiohttp.web.HTTPInternalServerError(text="Could not delete {}: {}".format(project_files_dir, e)) - except FileNotFoundError as e: - log.warning(e) + os.rmdir(project_files_dir) + except OSError: + pass if not vm_id: vm_id = str(uuid4()) From 85518a3cd6e91ea06db5f2c7c7e1878304b56108 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 25 Feb 2015 17:38:55 -0700 Subject: [PATCH 330/485] Fixes race condition when generating an ghost IOS file. --- gns3server/modules/dynamips/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index e01fc388..55376ed2 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -105,6 +105,7 @@ class Dynamips(BaseManager): _VM_CLASS = DynamipsVM _DEVICE_CLASS = DynamipsDevice + ghost_ios_lock = asyncio.Lock() def __init__(self): @@ -330,7 +331,8 @@ class Dynamips(BaseManager): ghost_ios_support = self.config.get_section_config("Dynamips").get("ghost_ios_support", True) if ghost_ios_support: - yield from self._set_ghost_ios(vm) + with (yield from Dynamips.ghost_ios_lock): + yield from self._set_ghost_ios(vm) @asyncio.coroutine def create_nio(self, node, nio_settings): From 0eaad579c21a79c5235dc8d516b7adfd4ddeade2 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 25 Feb 2015 18:55:35 -0700 Subject: [PATCH 331/485] IOU + VirtualBox conversion of old projects. --- gns3server/modules/base_manager.py | 71 ++++++++++++++--------- gns3server/modules/dynamips/__init__.py | 4 +- gns3server/modules/iou/__init__.py | 8 ++- gns3server/modules/qemu/__init__.py | 24 ++++---- gns3server/modules/virtualbox/__init__.py | 8 ++- gns3server/modules/vpcs/__init__.py | 7 ++- tests/modules/iou/test_iou_vm.py | 4 +- tests/modules/qemu/test_qemu_manager.py | 4 +- 8 files changed, 79 insertions(+), 51 deletions(-) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 8845d4cb..82d3104d 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -44,6 +44,8 @@ class BaseManager: Responsible of management of a VM pool """ + _convert_lock = asyncio.Lock() + def __init__(self): self._vms = {} @@ -146,6 +148,45 @@ class BaseManager: return vm + @asyncio.coroutine + def _convert_old_project(self, project, legacy_id, name): + """ + Convert project made before version 1.3 + + :param project: Project instance + :param legacy_id: old identifier + :param name: VM name + + :returns: new VM identifier + """ + + vm_id = str(uuid4()) + if hasattr(self, "get_legacy_vm_workdir"): + # move old project VM files to a new location + log.info("Converting old project...") + project_name = os.path.basename(project.path) + project_files_dir = os.path.join(project.path, "{}-files".format(project_name)) + legacy_vm_dir = self.get_legacy_vm_workdir(legacy_id, name) + vm_working_dir = os.path.join(project_files_dir, legacy_vm_dir) + new_vm_working_dir = os.path.join(project.path, "project-files", self.module_name.lower(), vm_id) + try: + log.info('Moving "{}" to "{}"'.format(vm_working_dir, new_vm_working_dir)) + yield from wait_run_in_executor(shutil.move, vm_working_dir, new_vm_working_dir) + except OSError as e: + raise aiohttp.web.HTTPInternalServerError(text="Could not move VM working directory: {} to {} {}".format(vm_working_dir, new_vm_working_dir, e)) + + try: + os.rmdir(os.path.dirname(vm_working_dir)) + except OSError: + pass + + try: + os.rmdir(project_files_dir) + except OSError: + pass + + return vm_id + @asyncio.coroutine def create_vm(self, name, project_id, vm_id, *args, **kwargs): """ @@ -157,34 +198,10 @@ class BaseManager: """ project = ProjectManager.instance().get_project(project_id) - # If it's not an UUID, old topology - if vm_id and (isinstance(vm_id, int) or len(vm_id) != 36): - legacy_id = int(vm_id) - vm_id = str(uuid4()) - if hasattr(self, "get_legacy_vm_workdir_name"): - # move old project VM files to a new location - - log.info("Converting old project...") - project_name = os.path.basename(project.path) - project_files_dir = os.path.join(project.path, "{}-files".format(project_name)) - module_path = os.path.join(project_files_dir, self.module_name.lower()) - vm_working_dir = os.path.join(module_path, self.get_legacy_vm_workdir_name(legacy_id)) - new_vm_working_dir = os.path.join(project.path, "project-files", self.module_name.lower(), vm_id) - try: - yield from wait_run_in_executor(shutil.move, vm_working_dir, new_vm_working_dir) - except OSError as e: - raise aiohttp.web.HTTPInternalServerError(text="Could not move VM working directory: {} to {} {}".format(vm_working_dir, new_vm_working_dir, e)) - - try: - os.rmdir(module_path) - except OSError: - pass - - try: - os.rmdir(project_files_dir) - except OSError: - pass + if vm_id and isinstance(vm_id, int): + with (yield from BaseManager._convert_lock): + vm_id = yield from self._convert_old_project(project, vm_id, name) if not vm_id: vm_id = str(uuid4()) diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index 55376ed2..004dc14a 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -105,7 +105,7 @@ class Dynamips(BaseManager): _VM_CLASS = DynamipsVM _DEVICE_CLASS = DynamipsDevice - ghost_ios_lock = asyncio.Lock() + _ghost_ios_lock = asyncio.Lock() def __init__(self): @@ -331,7 +331,7 @@ class Dynamips(BaseManager): ghost_ios_support = self.config.get_section_config("Dynamips").get("ghost_ios_support", True) if ghost_ios_support: - with (yield from Dynamips.ghost_ios_lock): + with (yield from Dynamips._ghost_ios_lock): yield from self._set_ghost_ios(vm) @asyncio.coroutine diff --git a/gns3server/modules/iou/__init__.py b/gns3server/modules/iou/__init__.py index 30d35915..1a7211e5 100644 --- a/gns3server/modules/iou/__init__.py +++ b/gns3server/modules/iou/__init__.py @@ -19,6 +19,7 @@ IOU server module. """ +import os import asyncio from ..base_manager import BaseManager @@ -63,12 +64,15 @@ class IOU(BaseManager): return self._used_application_ids.get(vm_id, 1) - def get_legacy_vm_workdir_name(legacy_vm_id): + @staticmethod + def get_legacy_vm_workdir(legacy_vm_id, name): """ Returns the name of the legacy working directory (pre 1.3) name for a VM. :param legacy_vm_id: legacy VM identifier (integer) + :param name: VM name (not used) + :returns: working directory name """ - return "device-{}".format(legacy_vm_id) + return os.path.join("iou", "device-{}".format(legacy_vm_id)) diff --git a/gns3server/modules/qemu/__init__.py b/gns3server/modules/qemu/__init__.py index 2d643204..aa9e2f90 100644 --- a/gns3server/modules/qemu/__init__.py +++ b/gns3server/modules/qemu/__init__.py @@ -34,17 +34,6 @@ from .qemu_vm import QemuVM class Qemu(BaseManager): _VM_CLASS = QemuVM - @staticmethod - def get_legacy_vm_workdir_name(legacy_vm_id): - """ - Returns the name of the legacy working directory name for a VM. - - :param legacy_vm_id: legacy VM identifier (integer) - :returns: working directory name - """ - - return "vm-{}".format(legacy_vm_id) - @staticmethod def binary_list(): """ @@ -108,3 +97,16 @@ class Qemu(BaseManager): raise QemuError("Could not determine the Qemu version for {}".format(qemu_path)) except subprocess.SubprocessError as e: raise QemuError("Error while looking for the Qemu version: {}".format(e)) + + @staticmethod + def get_legacy_vm_workdir(legacy_vm_id, name): + """ + Returns the name of the legacy working directory name for a VM. + + :param legacy_vm_id: legacy VM identifier (integer) + :param: VM name (not used) + + :returns: working directory name + """ + + return os.path.join("qemu", "vm-{}".format(legacy_vm_id)) diff --git a/gns3server/modules/virtualbox/__init__.py b/gns3server/modules/virtualbox/__init__.py index 346b237c..e0394373 100644 --- a/gns3server/modules/virtualbox/__init__.py +++ b/gns3server/modules/virtualbox/__init__.py @@ -129,12 +129,14 @@ class VirtualBox(BaseManager): return vms @staticmethod - def get_legacy_vm_workdir_name(legacy_vm_id): + def get_legacy_vm_workdir(legacy_vm_id, name): """ Returns the name of the legacy working directory name for a VM. - :param legacy_vm_id: legacy VM identifier (integer) + :param legacy_vm_id: legacy VM identifier (not used) + :param name: VM name + :returns: working directory name """ - return "vm-{}".format(legacy_vm_id) + return os.path.join("vbox", "{}".format(name)) diff --git a/gns3server/modules/vpcs/__init__.py b/gns3server/modules/vpcs/__init__.py index 826e6fd8..03250c55 100644 --- a/gns3server/modules/vpcs/__init__.py +++ b/gns3server/modules/vpcs/__init__.py @@ -19,6 +19,7 @@ VPCS server module. """ +import os import asyncio from ..base_manager import BaseManager @@ -65,12 +66,14 @@ class VPCS(BaseManager): return self._used_mac_ids.get(vm_id, 1) @staticmethod - def get_legacy_vm_workdir_name(legacy_vm_id): + def get_legacy_vm_workdir(legacy_vm_id, name): """ Returns the name of the legacy working directory name for a VM. :param legacy_vm_id: legacy VM identifier (integer) + :param name: VM name (not used) + :returns: working directory name """ - return "pc-{}".format(legacy_vm_id) + return os.path.join("vpcs", "pc-{}".format(legacy_vm_id)) diff --git a/tests/modules/iou/test_iou_vm.py b/tests/modules/iou/test_iou_vm.py index cb5507cc..2333c217 100644 --- a/tests/modules/iou/test_iou_vm.py +++ b/tests/modules/iou/test_iou_vm.py @@ -303,6 +303,6 @@ def test_stop_capture(vm, tmpdir, manager, free_console_port, loop): assert vm._adapters[0].get_nio(0).capturing is False -def test_get_legacy_vm_workdir_name(): +def test_get_legacy_vm_workdir(): - assert IOU.get_legacy_vm_workdir_name(42) == "device-42" + assert IOU.get_legacy_vm_workdir(42) == "iou/device-42" diff --git a/tests/modules/qemu/test_qemu_manager.py b/tests/modules/qemu/test_qemu_manager.py index 65870191..83092dbe 100644 --- a/tests/modules/qemu/test_qemu_manager.py +++ b/tests/modules/qemu/test_qemu_manager.py @@ -48,6 +48,6 @@ def test_binary_list(loop): assert {"path": os.path.join(os.environ["PATH"], "hello"), "version": "2.2.0"} not in qemus -def test_get_legacy_vm_workdir_name(): +def test_get_legacy_vm_workdir(): - assert Qemu.get_legacy_vm_workdir_name(42) == "vm-42" + assert Qemu.get_legacy_vm_workdir(42) == "qemu/vm-42" From c07b8c746eaa0a58007567869e43ca255ed0203c Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 26 Feb 2015 10:18:01 +0100 Subject: [PATCH 332/485] Drop poll from Qemu --- gns3server/modules/qemu/qemu_vm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index 5321aac6..d42e23e1 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -522,7 +522,7 @@ class QemuVM(BaseVM): Stops the cpulimit process. """ - if self._cpulimit_process and self._cpulimit_process.poll() is None: + if self._cpulimit_process and self._cpulimit_process.returncode is None: self._cpulimit_process.kill() try: self._process.wait(3) @@ -597,7 +597,7 @@ class QemuVM(BaseVM): self._process.wait() except subprocess.TimeoutExpired: self._process.kill() - if self._process.poll() is None: + if self._process.returncode is None: log.warn("QEMU VM instance {} PID={} is still running".format(self._id, self._process.pid)) self._process = None From aa40e6097e51d246637975533dd2b227f44e89a1 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 26 Feb 2015 10:45:37 +0100 Subject: [PATCH 333/485] Fix tests --- tests/conftest.py | 2 +- tests/handlers/api/test_virtualbox.py | 2 +- tests/handlers/api/test_vpcs.py | 2 +- tests/modules/iou/test_iou_vm.py | 2 +- tests/modules/qemu/test_qemu_manager.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index a64d9535..e9448aaf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -97,7 +97,7 @@ def server(request, loop, port_manager, monkeypatch): return Query(loop, host=host, port=port) -@pytest.fixture(scope="module") +@pytest.fixture(scope="function") def project(): """A GNS3 lab""" diff --git a/tests/handlers/api/test_virtualbox.py b/tests/handlers/api/test_virtualbox.py index 16825d1b..ff69aa4f 100644 --- a/tests/handlers/api/test_virtualbox.py +++ b/tests/handlers/api/test_virtualbox.py @@ -19,7 +19,7 @@ import pytest from tests.utils import asyncio_patch -@pytest.yield_fixture(scope="module") +@pytest.yield_fixture(scope="function") def vm(server, project, monkeypatch): vboxmanage_path = "/fake/VboxManage" diff --git a/tests/handlers/api/test_vpcs.py b/tests/handlers/api/test_vpcs.py index e0a4acca..190ab8d0 100644 --- a/tests/handlers/api/test_vpcs.py +++ b/tests/handlers/api/test_vpcs.py @@ -21,7 +21,7 @@ from tests.utils import asyncio_patch from unittest.mock import patch -@pytest.fixture(scope="module") +@pytest.fixture(scope="function") def vm(server, project): response = server.post("/projects/{project_id}/vpcs/vms".format(project_id=project.id), {"name": "PC TEST 1"}) assert response.status == 201 diff --git a/tests/modules/iou/test_iou_vm.py b/tests/modules/iou/test_iou_vm.py index 2333c217..79b2d5c5 100644 --- a/tests/modules/iou/test_iou_vm.py +++ b/tests/modules/iou/test_iou_vm.py @@ -305,4 +305,4 @@ def test_stop_capture(vm, tmpdir, manager, free_console_port, loop): def test_get_legacy_vm_workdir(): - assert IOU.get_legacy_vm_workdir(42) == "iou/device-42" + assert IOU.get_legacy_vm_workdir(42, "bla") == "iou/device-42" diff --git a/tests/modules/qemu/test_qemu_manager.py b/tests/modules/qemu/test_qemu_manager.py index 83092dbe..ee732d11 100644 --- a/tests/modules/qemu/test_qemu_manager.py +++ b/tests/modules/qemu/test_qemu_manager.py @@ -50,4 +50,4 @@ def test_binary_list(loop): def test_get_legacy_vm_workdir(): - assert Qemu.get_legacy_vm_workdir(42) == "qemu/vm-42" + assert Qemu.get_legacy_vm_workdir(42, "bla") == "qemu/vm-42" From 5a58f6efc82ca15346ce0efd3cfc6150bfa0013c Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 26 Feb 2015 11:29:57 +0100 Subject: [PATCH 334/485] Fix a crash with Python 3.4 when you stop IOU http://bugs.python.org/issue23140 --- gns3server/modules/iou/iou_vm.py | 15 ++++++++------- gns3server/utils/asyncio.py | 24 ++++++++++++++++++++++++ tests/utils/test_asyncio.py | 17 ++++++++++++++++- 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index fa6d4377..ade3aea4 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -483,18 +483,19 @@ class IOUVM(BaseVM): self._ioucon_thread = None self._terminate_process_iou() - try: - yield from asyncio.wait_for(self._iou_process.wait(), timeout=3) - except asyncio.TimeoutError: - self._iou_process.kill() - if self._iou_process.returncode is None: - log.warn("IOU process {} is still running".format(self._iou_process.pid)) + if self._iou_process.returncode is None: + try: + yield from gns3server.utils.asyncio.wait_for_process_termination(self._iou_process, timeout=3) + except asyncio.TimeoutError: + self._iou_process.kill() + if self._iou_process.returncode is None: + log.warn("IOU process {} is still running".format(self._iou_process.pid)) self._iou_process = None if self._iouyap_process is not None: self._terminate_process_iouyap() try: - yield from asyncio.wait_for(self._iouyap_process.wait(), timeout=3) + yield from gns3server.utils.asyncio.wait_for_process_termination(self._iouyap_process, timeout=3) except asyncio.TimeoutError: self._iouyap_process.kill() if self._iouyap_process.returncode is None: diff --git a/gns3server/utils/asyncio.py b/gns3server/utils/asyncio.py index 16dc8cd2..a977b717 100644 --- a/gns3server/utils/asyncio.py +++ b/gns3server/utils/asyncio.py @@ -53,3 +53,27 @@ def subprocess_check_output(*args, cwd=None, env=None): if output is None: return "" return output.decode("utf-8") + + +@asyncio.coroutine +def wait_for_process_termination(process, timeout=10): + """ + Wait for a process terminate, and raise asyncio.TimeoutError in case of + timeout. + + In theory this can be implemented by just: + yield from asyncio.wait_for(self._iou_process.wait(), timeout=100) + + But it's broken before Python 3.4: + http://bugs.python.org/issue23140 + + :param process: An asyncio subprocess + :param timeout: Timeout in seconds + """ + + while timeout > 0: + if process.returncode is not None: + return + yield from asyncio.sleep(0.1) + timeout -= 0.1 + raise asyncio.TimeoutError() diff --git a/tests/utils/test_asyncio.py b/tests/utils/test_asyncio.py index 38a1795f..96fbde7b 100644 --- a/tests/utils/test_asyncio.py +++ b/tests/utils/test_asyncio.py @@ -18,8 +18,9 @@ import asyncio import pytest +from unittest.mock import MagicMock -from gns3server.utils.asyncio import wait_run_in_executor, subprocess_check_output +from gns3server.utils.asyncio import wait_run_in_executor, subprocess_check_output, wait_for_process_termination def test_wait_run_in_executor(loop): @@ -50,3 +51,17 @@ def test_subprocess_check_output(loop, tmpdir, restore_original_path): exec = subprocess_check_output("cat", path) result = loop.run_until_complete(asyncio.async(exec)) assert result == "TEST" + + +def test_wait_for_process_termination(loop): + + process = MagicMock() + process.returncode = 0 + exec = wait_for_process_termination(process) + loop.run_until_complete(asyncio.async(exec)) + + process = MagicMock() + process.returncode = None + exec = wait_for_process_termination(process, timeout=0.5) + with pytest.raises(asyncio.TimeoutError): + loop.run_until_complete(asyncio.async(exec)) From 29e8d917458fe12b78a721c759dcde76131664f6 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 26 Feb 2015 13:06:10 +0100 Subject: [PATCH 335/485] Do not output debug for ioucon standard telnet commands --- gns3server/modules/iou/ioucon.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/gns3server/modules/iou/ioucon.py b/gns3server/modules/iou/ioucon.py index d4889433..f5496da0 100644 --- a/gns3server/modules/iou/ioucon.py +++ b/gns3server/modules/iou/ioucon.py @@ -302,9 +302,12 @@ class TelnetServer(Console): buf.extend(self._read_block(1)) iac_cmd.append(buf[iac_loc + 2]) # We do ECHO, SGA, and BINARY. Period. - if iac_cmd[1] == DO and iac_cmd[2] not in [ECHO, SGA, BINARY]: - self._write_cur(bytes([IAC, WONT, iac_cmd[2]])) - log.debug("Telnet WON'T {:#x}".format(iac_cmd[2])) + if iac_cmd[1] == DO: + if iac_cmd[2] not in [ECHO, SGA, BINARY]: + self._write_cur(bytes([IAC, WONT, iac_cmd[2]])) + log.debug("Telnet WON'T {:#x}".format(iac_cmd[2])) + elif iac_cmd[1] == WILL and iac_cmd[2] == BINARY: + pass # It's standard negociation we can ignore it else: log.debug("Unhandled telnet command: " "{0:#x} {1:#x} {2:#x}".format(*iac_cmd)) @@ -357,7 +360,6 @@ class TelnetServer(Console): sock_fd.listen(socket.SOMAXCONN) self.sock_fd = sock_fd log.info("Telnet server ready for connections on {}:{}".format(self.addr, self.port)) - log.info(self.stop_event.is_set()) return self From 5e591459481d1c0f3a42ab9f5902289f3bc220c5 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 26 Feb 2015 15:09:15 +0100 Subject: [PATCH 336/485] If you type reload inside iou you are no longer disconnected --- gns3server/modules/iou/ioucon.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/gns3server/modules/iou/ioucon.py b/gns3server/modules/iou/ioucon.py index f5496da0..2b5556ab 100644 --- a/gns3server/modules/iou/ioucon.py +++ b/gns3server/modules/iou/ioucon.py @@ -159,10 +159,6 @@ class Console: raise NotImplementedError("Only routers have fileno()") -class Router: - pass - - class TTY(Console): def read(self, fileno, bufsize): @@ -370,7 +366,7 @@ class TelnetServer(Console): return False -class IOU(Router): +class IOU: def __init__(self, ttyC, ttyS, stop_event): self.ttyC = ttyC @@ -616,8 +612,13 @@ def start_ioucon(cmdline_args, stop_event): try: if args.telnet_server: with TelnetServer(addr, nport, stop_event) as console: - with IOU(ttyC, ttyS, stop_event) as router: - send_recv_loop(console, router, b'', stop_event) + # We loop inside the Telnet server otherwise the client is disconnected when user use the reload command inside a terminal + while not stop_event.is_set(): + try: + with IOU(ttyC, ttyS, stop_event) as router: + send_recv_loop(console, router, b'', stop_event) + except ConnectionRefusedError: + pass else: with IOU(ttyC, ttyS, stop_event) as router, TTY() as console: send_recv_loop(console, router, esc_char, stop_event) From 62afef06af914fe0c0584a0cc5ae8c9633549ea0 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 26 Feb 2015 15:47:47 +0100 Subject: [PATCH 337/485] After an iou reload you can write on the console --- gns3server/modules/iou/ioucon.py | 133 +++++++++++++++++-------------- 1 file changed, 73 insertions(+), 60 deletions(-) diff --git a/gns3server/modules/iou/ioucon.py b/gns3server/modules/iou/ioucon.py index 2b5556ab..764e81e8 100644 --- a/gns3server/modules/iou/ioucon.py +++ b/gns3server/modules/iou/ioucon.py @@ -171,6 +171,9 @@ class TTY(Console): self.epoll = epoll epoll.register(self.fd, select.EPOLLIN | select.EPOLLET) + def unregister(self, epoll): + epoll.unregister(self.fd) + def __enter__(self): try: self.fd = open('/dev/tty', 'r+b', buffering=0) @@ -240,6 +243,9 @@ class TelnetServer(Console): self.epoll = epoll epoll.register(self.sock_fd, select.EPOLLIN) + def unregister(self, epoll): + epoll.unregister(self.sock_fd) + def _read_block(self, bufsize): buf = self._read_cur(bufsize, socket.MSG_WAITALL) # If we don't get everything we were looking for then the @@ -420,6 +426,9 @@ class IOU: self.epoll = epoll epoll.register(self.fd, select.EPOLLIN | select.EPOLLET) + def unregister(self, epoll): + epoll.unregister(self.fd) + def fileno(self): return self.fd.fileno() @@ -468,70 +477,73 @@ def mkdir_netio(netio_dir): raise NetioError("Couldn't create directory {}: {}".format(netio_dir, e)) -def send_recv_loop(console, router, esc_char, stop_event): - - epoll = select.epoll() +def send_recv_loop(epoll, console, router, esc_char, stop_event): router.register(epoll) console.register(epoll) - router_fileno = router.fileno() - esc_quit = bytes(ESC_QUIT.upper(), 'ascii') - esc_state = False - - while not stop_event.is_set(): - event_list = epoll.poll(timeout=POLL_TIMEOUT) - - # When/if the poll times out we send an empty datagram. If IOU - # has gone away then this will toss a ConnectionRefusedError - # exception. - if not event_list: - router.write(b'') - continue - - for fileno, event in event_list: - buf = bytearray() - - # IOU --> tty(s) - if fileno == router_fileno: - while not stop_event.is_set(): - data = router.read(BUFFER_SIZE) - if not data: - break - buf.extend(data) - console.write(buf) - - # tty --> IOU - else: - while not stop_event.is_set(): - data = console.read(fileno, BUFFER_SIZE) - if not data: - break - buf.extend(data) - - # If we just received the escape character then - # enter the escape state. - # - # If we are in the escape state then check for a - # quit command. Or if it's the escape character then - # send the escape character. Else, send the escape - # character we ate earlier and whatever character we - # just got. Exit escape state. - # - # If we're not in the escape state and this isn't an - # escape character then just send it to IOU. - if esc_state: - if buf.upper() == esc_quit: - sys.exit(EXIT_SUCCESS) + try: + router_fileno = router.fileno() + esc_quit = bytes(ESC_QUIT.upper(), 'ascii') + esc_state = False + + while not stop_event.is_set(): + event_list = epoll.poll(timeout=POLL_TIMEOUT) + + # When/if the poll times out we send an empty datagram. If IOU + # has gone away then this will toss a ConnectionRefusedError + # exception. + if not event_list: + router.write(b'') + continue + + for fileno, event in event_list: + buf = bytearray() + + # IOU --> tty(s) + if fileno == router_fileno: + while not stop_event.is_set(): + data = router.read(BUFFER_SIZE) + if not data: + break + buf.extend(data) + console.write(buf) + + # tty --> IOU + else: + while not stop_event.is_set(): + data = console.read(fileno, BUFFER_SIZE) + if not data: + break + buf.extend(data) + + # If we just received the escape character then + # enter the escape state. + # + # If we are in the escape state then check for a + # quit command. Or if it's the escape character then + # send the escape character. Else, send the escape + # character we ate earlier and whatever character we + # just got. Exit escape state. + # + # If we're not in the escape state and this isn't an + # escape character then just send it to IOU. + if esc_state: + if buf.upper() == esc_quit: + sys.exit(EXIT_SUCCESS) + elif buf == esc_char: + router.write(esc_char) + else: + router.write(esc_char) + router.write(buf) + esc_state = False elif buf == esc_char: - router.write(esc_char) + esc_state = True else: - router.write(esc_char) router.write(buf) - esc_state = False - elif buf == esc_char: - esc_state = True - else: - router.write(buf) + finally: + log.debug("Finally") + router.unregister(epoll) + console.unregister(epoll) def get_args(): @@ -609,6 +621,7 @@ def start_ioucon(cmdline_args, stop_event): 'ADDR:PORT (like 127.0.0.1:20000)') while not stop_event.is_set(): + epoll = select.epoll() try: if args.telnet_server: with TelnetServer(addr, nport, stop_event) as console: @@ -616,12 +629,12 @@ def start_ioucon(cmdline_args, stop_event): while not stop_event.is_set(): try: with IOU(ttyC, ttyS, stop_event) as router: - send_recv_loop(console, router, b'', stop_event) + send_recv_loop(epoll, console, router, b'', stop_event) except ConnectionRefusedError: pass else: with IOU(ttyC, ttyS, stop_event) as router, TTY() as console: - send_recv_loop(console, router, esc_char, stop_event) + send_recv_loop(epoll, console, router, esc_char, stop_event) except ConnectionRefusedError: pass except KeyboardInterrupt: From 58d92f1584c224746ad245d0d60528612f92f6f3 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 26 Feb 2015 16:15:44 -0700 Subject: [PATCH 338/485] Fixes Dynamips console/aux ports restoration when loading a project. --- gns3server/handlers/api/dynamips_vm_handler.py | 4 +++- gns3server/modules/dynamips/__init__.py | 2 +- gns3server/modules/dynamips/nodes/c1700.py | 6 ++++-- gns3server/modules/dynamips/nodes/c2600.py | 6 ++++-- gns3server/modules/dynamips/nodes/c2691.py | 6 ++++-- gns3server/modules/dynamips/nodes/c3600.py | 6 ++++-- gns3server/modules/dynamips/nodes/c3725.py | 6 ++++-- gns3server/modules/dynamips/nodes/c3745.py | 6 ++++-- gns3server/modules/dynamips/nodes/c7200.py | 6 ++++-- gns3server/modules/dynamips/nodes/router.py | 12 ++++++++---- 10 files changed, 40 insertions(+), 20 deletions(-) diff --git a/gns3server/handlers/api/dynamips_vm_handler.py b/gns3server/handlers/api/dynamips_vm_handler.py index c17ceb71..dd5d5720 100644 --- a/gns3server/handlers/api/dynamips_vm_handler.py +++ b/gns3server/handlers/api/dynamips_vm_handler.py @@ -58,7 +58,9 @@ class DynamipsVMHandler: request.match_info["project_id"], request.json.get("vm_id"), request.json.get("dynamips_id"), - request.json.pop("platform")) + request.json.pop("platform"), + console=request.json.get("console"), + aux=request.json.get("aux")) yield from dynamips_manager.update_vm_settings(vm, request.json) yield from dynamips_manager.ghost_ios_support(vm) diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index 004dc14a..0e8b648a 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -314,7 +314,7 @@ class Dynamips(BaseManager): hypervisor = Hypervisor(self._dynamips_path, working_dir, "127.0.0.1", port) - log.info("Ceating new hypervisor {}:{} with working directory {}".format(hypervisor.host, hypervisor.port, working_dir)) + log.info("Creating new hypervisor {}:{} with working directory {}".format(hypervisor.host, hypervisor.port, working_dir)) yield from hypervisor.start() yield from self._wait_for_hypervisor("127.0.0.1", port) diff --git a/gns3server/modules/dynamips/nodes/c1700.py b/gns3server/modules/dynamips/nodes/c1700.py index 707c2baf..1f7db1a0 100644 --- a/gns3server/modules/dynamips/nodes/c1700.py +++ b/gns3server/modules/dynamips/nodes/c1700.py @@ -39,13 +39,15 @@ class C1700(Router): :param project: Project instance :param manager: Parent VM Manager :param dynamips_id: ID to use with Dynamips + :param console: console port + :param aux: auxiliary console port :param chassis: chassis for this router: 1720, 1721, 1750, 1751 or 1760 (default = 1720). 1710 is not supported. """ - def __init__(self, name, vm_id, project, manager, dynamips_id, chassis="1720"): - Router.__init__(self, name, vm_id, project, manager, dynamips_id, platform="c1700") + def __init__(self, name, vm_id, project, manager, dynamips_id, console=None, aux=None, chassis="1720"): + Router.__init__(self, name, vm_id, project, manager, dynamips_id, console, aux, platform="c1700") # Set default values for this platform (must be the same as Dynamips) self._ram = 64 diff --git a/gns3server/modules/dynamips/nodes/c2600.py b/gns3server/modules/dynamips/nodes/c2600.py index 9a63c487..ce5721d1 100644 --- a/gns3server/modules/dynamips/nodes/c2600.py +++ b/gns3server/modules/dynamips/nodes/c2600.py @@ -41,6 +41,8 @@ class C2600(Router): :param project: Project instance :param manager: Parent VM Manager :param dynamips_id: ID to use with Dynamips + :param console: console port + :param aux: auxiliary console port :param chassis: chassis for this router: 2610, 2611, 2620, 2621, 2610XM, 2611XM 2620XM, 2621XM, 2650XM or 2651XM (default = 2610). @@ -59,8 +61,8 @@ class C2600(Router): "2650XM": C2600_MB_1FE, "2651XM": C2600_MB_2FE} - def __init__(self, name, vm_id, project, manager, dynamips_id, chassis="2610"): - Router.__init__(self, name, vm_id, project, manager, dynamips_id, platform="c2600") + def __init__(self, name, vm_id, project, manager, dynamips_id, console=None, aux=None, chassis="2610"): + Router.__init__(self, name, vm_id, project, manager, dynamips_id, console, aux, platform="c2600") # Set default values for this platform (must be the same as Dynamips) self._ram = 64 diff --git a/gns3server/modules/dynamips/nodes/c2691.py b/gns3server/modules/dynamips/nodes/c2691.py index 3b5b7332..d161c90e 100644 --- a/gns3server/modules/dynamips/nodes/c2691.py +++ b/gns3server/modules/dynamips/nodes/c2691.py @@ -38,10 +38,12 @@ class C2691(Router): :param project: Project instance :param manager: Parent VM Manager :param dynamips_id: ID to use with Dynamips + :param console: console port + :param aux: auxiliary console port """ - def __init__(self, name, vm_id, project, manager, dynamips_id): - Router.__init__(self, name, vm_id, project, manager, dynamips_id, platform="c2691") + def __init__(self, name, vm_id, project, manager, dynamips_id, console=None, aux=None): + Router.__init__(self, name, vm_id, project, manager, dynamips_id, console, aux, platform="c2691") # Set default values for this platform (must be the same as Dynamips) self._ram = 128 diff --git a/gns3server/modules/dynamips/nodes/c3600.py b/gns3server/modules/dynamips/nodes/c3600.py index aa0d2249..9eff5964 100644 --- a/gns3server/modules/dynamips/nodes/c3600.py +++ b/gns3server/modules/dynamips/nodes/c3600.py @@ -38,12 +38,14 @@ class C3600(Router): :param project: Project instance :param manager: Parent VM Manager :param dynamips_id: ID to use with Dynamips + :param console: console port + :param aux: auxiliary console port :param chassis: chassis for this router: 3620, 3640 or 3660 (default = 3640). """ - def __init__(self, name, vm_id, project, manager, dynamips_id, chassis="3640"): - Router.__init__(self, name, vm_id, project, manager, dynamips_id, platform="c3600") + def __init__(self, name, vm_id, project, manager, dynamips_id, console=None, aux=None, chassis="3640"): + Router.__init__(self, name, vm_id, project, manager, dynamips_id, console, aux, platform="c3600") # Set default values for this platform (must be the same as Dynamips) self._ram = 128 diff --git a/gns3server/modules/dynamips/nodes/c3725.py b/gns3server/modules/dynamips/nodes/c3725.py index 6a1481a1..41c3b19c 100644 --- a/gns3server/modules/dynamips/nodes/c3725.py +++ b/gns3server/modules/dynamips/nodes/c3725.py @@ -38,10 +38,12 @@ class C3725(Router): :param project: Project instance :param manager: Parent VM Manager :param dynamips_id: ID to use with Dynamips + :param console: console port + :param aux: auxiliary console port """ - def __init__(self, name, vm_id, project, manager, dynamips_id): - Router.__init__(self, name, vm_id, project, manager, dynamips_id, platform="c3725") + def __init__(self, name, vm_id, project, manager, dynamips_id, console=None, aux=None): + Router.__init__(self, name, vm_id, project, manager, dynamips_id, console, aux, platform="c3725") # Set default values for this platform (must be the same as Dynamips) self._ram = 128 diff --git a/gns3server/modules/dynamips/nodes/c3745.py b/gns3server/modules/dynamips/nodes/c3745.py index e9bd84e1..62a4f267 100644 --- a/gns3server/modules/dynamips/nodes/c3745.py +++ b/gns3server/modules/dynamips/nodes/c3745.py @@ -38,10 +38,12 @@ class C3745(Router): :param project: Project instance :param manager: Parent VM Manager :param dynamips_id: ID to use with Dynamips + :param console: console port + :param aux: auxiliary console port """ - def __init__(self, name, vm_id, project, manager, dynamips_id): - Router.__init__(self, name, vm_id, project, manager, dynamips_id, platform="c3745") + def __init__(self, name, vm_id, project, manager, dynamips_id, console=None, aux=None): + Router.__init__(self, name, vm_id, project, manager, dynamips_id, console, aux, platform="c3745") # Set default values for this platform (must be the same as Dynamips) self._ram = 128 diff --git a/gns3server/modules/dynamips/nodes/c7200.py b/gns3server/modules/dynamips/nodes/c7200.py index 218d35ab..784bb241 100644 --- a/gns3server/modules/dynamips/nodes/c7200.py +++ b/gns3server/modules/dynamips/nodes/c7200.py @@ -41,11 +41,13 @@ class C7200(Router): :param project: Project instance :param manager: Parent VM Manager :param dynamips_id: ID to use with Dynamips + :param console: console port + :param aux: auxiliary console port :param npe: Default NPE """ - def __init__(self, name, vm_id, project, manager, dynamips_id, npe="npe-400"): - Router.__init__(self, name, vm_id, project, manager, dynamips_id, platform="c7200") + def __init__(self, name, vm_id, project, manager, dynamips_id, console=None, aux=None, npe="npe-400"): + Router.__init__(self, name, vm_id, project, manager, dynamips_id, console, aux, platform="c7200") # Set default values for this platform (must be the same as Dynamips) self._ram = 256 diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index e8c6dfef..16045537 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -48,6 +48,8 @@ class Router(BaseVM): :param project: Project instance :param manager: Parent VM Manager :param dynamips_id: ID to use with Dynamips + :param console: console port + :param aux: auxiliary console port :param platform: Platform of this router """ @@ -57,9 +59,9 @@ class Router(BaseVM): 2: "running", 3: "suspended"} - def __init__(self, name, vm_id, project, manager, dynamips_id=None, platform="c7200", hypervisor=None, ghost_flag=False): + def __init__(self, name, vm_id, project, manager, dynamips_id=None, console=None, aux=None, platform="c7200", hypervisor=None, ghost_flag=False): - super().__init__(name, vm_id, project, manager) + super().__init__(name, vm_id, project, manager, console=console) self._hypervisor = hypervisor self._dynamips_id = dynamips_id @@ -86,7 +88,7 @@ class Router(BaseVM): self._disk0 = 0 # Megabytes self._disk1 = 0 # Megabytes self._confreg = "0x2102" - self._aux = None + self._aux = aux self._mac_addr = "" self._system_id = "FTX0945W0MY" # processor board ID in IOS self._slots = [] @@ -200,7 +202,9 @@ class Router(BaseVM): id=self._id)) yield from self._hypervisor.send('vm set_con_tcp_port "{name}" {console}'.format(name=self._name, console=self._console)) - yield from self._hypervisor.send('vm set_aux_tcp_port "{name}" {aux}'.format(name=self._name, aux=self._aux)) + + if self._aux is not None: + yield from self._hypervisor.send('vm set_aux_tcp_port "{name}" {aux}'.format(name=self._name, aux=self._aux)) # get the default base MAC address mac_addr = yield from self._hypervisor.send('{platform} get_mac_addr "{name}"'.format(platform=self._platform, From 985c23a40eb56a324add1356f9697724b1f2237c Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 26 Feb 2015 19:31:18 -0700 Subject: [PATCH 339/485] Explicitly import handlers so freezing application can find and include the right modules. Do not import IOU on Windows to avoid importing unknown modules like fcntl on that platform. --- gns3server/handlers/__init__.py | 13 ++++++++++++- gns3server/handlers/api/__init__.py | 9 --------- gns3server/modules/__init__.py | 9 +++++++-- gns3server/server.py | 4 ++-- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/gns3server/handlers/__init__.py b/gns3server/handlers/__init__.py index 8d2587c9..c2247f30 100644 --- a/gns3server/handlers/__init__.py +++ b/gns3server/handlers/__init__.py @@ -14,5 +14,16 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from gns3server.handlers.api import * +import sys +from gns3server.handlers.api.version_handler import VersionHandler +from gns3server.handlers.api.network_handler import NetworkHandler +from gns3server.handlers.api.project_handler import ProjectHandler +from gns3server.handlers.api.dynamips_device_handler import DynamipsDeviceHandler +from gns3server.handlers.api.dynamips_vm_handler import DynamipsVMHandler +from gns3server.handlers.api.qemu_handler import QEMUHandler +from gns3server.handlers.api.virtualbox_handler import VirtualBoxHandler +from gns3server.handlers.api.vpcs_handler import VPCSHandler from gns3server.handlers.upload_handler import UploadHandler + +if sys.platform.startswith("linux"): + from gns3server.handlers.api.iou_handler import IOUHandler diff --git a/gns3server/handlers/api/__init__.py b/gns3server/handlers/api/__init__.py index 5d558245..e69de29b 100644 --- a/gns3server/handlers/api/__init__.py +++ b/gns3server/handlers/api/__init__.py @@ -1,9 +0,0 @@ -__all__ = ["version_handler", - "network_handler", - "vpcs_handler", - "project_handler", - "virtualbox_handler", - "dynamips_vm_handler", - "dynamips_device_handler", - "iou_handler", - "qemu_handler"] diff --git a/gns3server/modules/__init__.py b/gns3server/modules/__init__.py index 0f55e396..6b679996 100644 --- a/gns3server/modules/__init__.py +++ b/gns3server/modules/__init__.py @@ -15,10 +15,15 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import sys from .vpcs import VPCS from .virtualbox import VirtualBox from .dynamips import Dynamips -from .iou import IOU from .qemu import Qemu -MODULES = [VPCS, VirtualBox, Dynamips, IOU, Qemu] +MODULES = [VPCS, VirtualBox, Dynamips, Qemu] + +if sys.platform.startswith("linux"): + # IOU runs only on Linux + from .iou import IOU + MODULES.append(IOU) diff --git a/gns3server/server.py b/gns3server/server.py index 13b76f3d..406c0b3f 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -34,8 +34,8 @@ from .config import Config from .modules import MODULES from .modules.port_manager import PortManager -# TODO: get rid of * have something generic to automatically import handlers so the routes can be found -from gns3server.handlers import * +# do not delete this import +import gns3server.handlers import logging log = logging.getLogger(__name__) From ebb865d9730cb9447c5795f7f248ecf4c3df8636 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 27 Feb 2015 13:36:11 +0100 Subject: [PATCH 340/485] Export vpcs config path --- gns3server/modules/vpcs/vpcs_vm.py | 17 ++++++++++++++++- gns3server/schemas/vpcs.py | 6 +++++- tests/handlers/api/test_vpcs.py | 2 ++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index e72d1dde..56c89bd1 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -106,7 +106,22 @@ class VPCSVM(BaseVM): "vm_id": self.id, "console": self._console, "project_id": self.project.id, - "startup_script": self.startup_script} + "startup_script": self.startup_script, + "startup_script_path": self.relative_startup_script} + + @property + def relative_startup_script(self): + """ + Returns the startup config file relative to the project directory. + + :returns: path to config file. None if the file doesn't exist + """ + + path = os.path.join(self.working_dir, 'startup.vpc') + if os.path.exists(path): + return 'startup.vpc' + else: + return None @property def vpcs_path(self): diff --git a/gns3server/schemas/vpcs.py b/gns3server/schemas/vpcs.py index 8ba53064..14cad8be 100644 --- a/gns3server/schemas/vpcs.py +++ b/gns3server/schemas/vpcs.py @@ -165,7 +165,11 @@ VPCS_OBJECT_SCHEMA = { "description": "Content of the VPCS startup script", "type": ["string", "null"] }, + "startup_script_path": { + "description": "Path of the VPCS startup script relative to project directory", + "type": ["string", "null"] + }, }, "additionalProperties": False, - "required": ["name", "vm_id", "console", "project_id"] + "required": ["name", "vm_id", "console", "project_id", "startup_script_path"] } diff --git a/tests/handlers/api/test_vpcs.py b/tests/handlers/api/test_vpcs.py index 190ab8d0..473a533e 100644 --- a/tests/handlers/api/test_vpcs.py +++ b/tests/handlers/api/test_vpcs.py @@ -42,6 +42,7 @@ def test_vpcs_get(server, project, vm): assert response.route == "/projects/{project_id}/vpcs/vms/{vm_id}" assert response.json["name"] == "PC TEST 1" assert response.json["project_id"] == project.id + assert response.json["startup_script_path"] == None def test_vpcs_create_startup_script(server, project): @@ -51,6 +52,7 @@ def test_vpcs_create_startup_script(server, project): assert response.json["name"] == "PC TEST 1" assert response.json["project_id"] == project.id assert response.json["startup_script"] == "ip 192.168.1.2\necho TEST" + assert response.json["startup_script_path"] == "startup.vpc" def test_vpcs_create_port(server, project, free_console_port): From f6448bb05d3f69190c200addebb4902e1b4c102d Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 27 Feb 2015 15:27:13 +0100 Subject: [PATCH 341/485] Turn off collored log output on windows --- gns3server/web/logger.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/gns3server/web/logger.py b/gns3server/web/logger.py index f2a51484..6c9c1d2b 100644 --- a/gns3server/web/logger.py +++ b/gns3server/web/logger.py @@ -79,9 +79,12 @@ class ColouredStreamHandler(logging.StreamHandler): def init_logger(level, quiet=False): - - stream_handler = ColouredStreamHandler(sys.stdout) - stream_handler.formatter = ColouredFormatter("{asctime} {levelname} {filename}:{lineno}#RESET# {message}", "%Y-%m-%d %H:%M:%S", "{") + if sys.platform.startswith("win"): + stream_handler = logging.StreamHandler(sys.stdout) + stream_handler.formatter = ColouredFormatter("{asctime} {levelname} {filename}:{lineno} {message}", "%Y-%m-%d %H:%M:%S", "{") + else: + stream_handler = ColouredStreamHandler(sys.stdout) + stream_handler.formatter = ColouredFormatter("{asctime} {levelname} {filename}:{lineno}#RESET# {message}", "%Y-%m-%d %H:%M:%S", "{") if quiet: stream_handler.addFilter(logging.Filter(name="user_facing")) logging.getLogger('user_facing').propagate = False From 38326f7d729de7ef05a3cba93e579a69a48626a9 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 27 Feb 2015 16:13:30 +0100 Subject: [PATCH 342/485] Add changelog file --- CHANGELOG | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 CHANGELOG diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 00000000..df657600 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,49 @@ +# Change Log + +## Unreleased + +* HTTP Rest API instead of WebSocket +* API documentation +* Create a dedicated configuration file for the server: server.conf +* Temporary projects are real project +* Use UUID instead of id + +## 1.2.3 2015/01/17 +* Fixed broken -netdev + legacy virtio in Qemu support. +* Ping and traceroute added to the IOU VM. + +## 1.2.2 2015/01/16 + +### Small improvements / new features + +* Auxiliary console support for IOS routers. +* Suspend / resume support for Qemu. +* Dynamically configure network connections of running Qemu VMs (only with recent Qemu versions). +* VPCS multi-host support (useful for old .net labs). +* Possibility to run VirtualBox as another user (Linux/OSX only). +* Support for IOURC file on the server side. +* Bumped the maximum network adapters to 32 for Qemu (depending on Qemu version you cannot go above 8 or even 28, Qemu will just not start). +* Added snapshot named 'reset' to linked cloned VirtualBox VMs. +* More network interface options to the Qemu VM configuration interface as well as descriptions for all NICs. +* More checks on minimum RAM for IOS routers and updates default values to match the latest IOS image requirements. +* Fixed bug when importing Host node with UDP NIOs. + +## 1.2.1 2014/12/04 +* Early support for IOSv and IOSv-L2 (with Qemu for now, which is slow on Windows/Mac OS X). +* Support for CPU throttling and process priority for Qemu. +* Fixed C7200 IO cards insert/remove issues and makes C7200-IO-FE the default. +* Updated the IOU VM with iouyap version 0.95 (packet capture). + + +## 1.2 2014/11/20 +* New VirtualBox support +* New Telnet server for VirtualBox. +* Add detection of qemu and qemu.exe binaries. +* New host node (cloud with all available Ethernet & TAP interfaces added). +* Option to allow console connections to any local IP address when using the local server. +* VirtualBox linked clones support (experimental, still some problems with temporary projects). + + +## 1.1 2014/10/23 +* Serial console for local VirtualBox. + From ae7bf828cda5bf88a684554d4af5e218c27be149 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 27 Feb 2015 18:30:22 +0100 Subject: [PATCH 343/485] Fix tests on MacOS --- gns3server/handlers/__init__.py | 5 ++++- gns3server/modules/__init__.py | 2 +- tests/conftest.py | 10 ++++++---- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/gns3server/handlers/__init__.py b/gns3server/handlers/__init__.py index c2247f30..464fa4a8 100644 --- a/gns3server/handlers/__init__.py +++ b/gns3server/handlers/__init__.py @@ -15,6 +15,8 @@ # along with this program. If not, see . import sys +import os + from gns3server.handlers.api.version_handler import VersionHandler from gns3server.handlers.api.network_handler import NetworkHandler from gns3server.handlers.api.project_handler import ProjectHandler @@ -25,5 +27,6 @@ from gns3server.handlers.api.virtualbox_handler import VirtualBoxHandler from gns3server.handlers.api.vpcs_handler import VPCSHandler from gns3server.handlers.upload_handler import UploadHandler -if sys.platform.startswith("linux"): +print(os.environ) +if sys.platform.startswith("linux") or hasattr(sys, "_called_from_test"): from gns3server.handlers.api.iou_handler import IOUHandler diff --git a/gns3server/modules/__init__.py b/gns3server/modules/__init__.py index 6b679996..4dce8ccc 100644 --- a/gns3server/modules/__init__.py +++ b/gns3server/modules/__init__.py @@ -23,7 +23,7 @@ from .qemu import Qemu MODULES = [VPCS, VirtualBox, Dynamips, Qemu] -if sys.platform.startswith("linux"): +if sys.platform.startswith("linux") or hasattr(sys, "_called_from_test"): # IOU runs only on Linux from .iou import IOU MODULES.append(IOU) diff --git a/tests/conftest.py b/tests/conftest.py index e9448aaf..b4599ce2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,14 +15,20 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . + import pytest import socket import asyncio import tempfile import shutil import os +import sys from aiohttp import web +sys._called_from_test = True +# Prevent execution of external binaries +os.environ["PATH"] = tempfile.mkdtemp() + from gns3server.config import Config from gns3server.web.route import Route # TODO: get rid of * @@ -33,10 +39,6 @@ from gns3server.modules.project_manager import ProjectManager from tests.handlers.api.base import Query -# Prevent execution of external binaries -os.environ["PATH"] = tempfile.mkdtemp() - - @pytest.yield_fixture def restore_original_path(): """ From ebd72d1149c39d0e8f2f5a05d42fbcf2922f02db Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 27 Feb 2015 18:39:20 +0100 Subject: [PATCH 344/485] Add a curl sample sessions --- docs/api/examples/get_projectsprojectid.txt | 6 +- .../get_projectsprojectidiouvmsvmid.txt | 4 +- .../get_projectsprojectidqemuvmsvmid.txt | 4 +- ...get_projectsprojectidvirtualboxvmsvmid.txt | 2 +- .../get_projectsprojectidvpcsvmsvmid.txt | 5 +- docs/api/examples/post_projects.txt | 8 +- .../examples/post_projectsprojectidiouvms.txt | 8 +- ...ternumberdportsportnumberdstartcapture.txt | 2 +- .../post_projectsprojectidqemuvms.txt | 8 +- .../post_projectsprojectidvirtualboxvms.txt | 2 +- .../post_projectsprojectidvpcsvms.txt | 5 +- docs/api/examples/put_projectsprojectid.txt | 4 +- .../put_projectsprojectidiouvmsvmid.txt | 4 +- .../put_projectsprojectidqemuvmsvmid.txt | 4 +- ...put_projectsprojectidvirtualboxvmsvmid.txt | 2 +- .../put_projectsprojectidvpcsvmsvmid.txt | 5 +- .../projectsprojectiddynamipsvmsvmid.rst | 6 +- ...ptersadapternumberdportsportnumberdnio.rst | 8 +- ...ternumberdportsportnumberdstartcapture.rst | 4 +- ...pternumberdportsportnumberdstopcapture.rst | 4 +- ...projectsprojectiddynamipsvmsvmidreload.rst | 2 +- ...projectsprojectiddynamipsvmsvmidresume.rst | 2 +- .../projectsprojectiddynamipsvmsvmidstart.rst | 2 +- .../projectsprojectiddynamipsvmsvmidstop.rst | 2 +- ...rojectsprojectiddynamipsvmsvmidsuspend.rst | 2 +- docs/api/v1/iou.rst | 8 - docs/api/v1/iou/projectsprojectidiouvms.rst | 62 ------- .../v1/iou/projectsprojectidiouvmsvmid.rst | 126 -------------- ...ptersadapternumberdportsportnumberdnio.rst | 52 ------ ...ternumberdportsportnumberdstartcapture.rst | 38 ----- ...pternumberdportsportnumberdstopcapture.rst | 28 ---- ...ojectsprojectidiouvmsvmidinitialconfig.rst | 30 ---- .../iou/projectsprojectidiouvmsvmidreload.rst | 26 --- .../iou/projectsprojectidiouvmsvmidstart.rst | 26 --- .../iou/projectsprojectidiouvmsvmidstop.rst | 26 --- docs/api/v1/project/projects.rst | 3 +- .../v1/qemu/projectsprojectidqemuvmsvmid.rst | 6 +- ...ptersadapternumberdportsportnumberdnio.rst | 8 +- .../projectsprojectidqemuvmsvmidreload.rst | 2 +- .../projectsprojectidqemuvmsvmidresume.rst | 2 +- .../projectsprojectidqemuvmsvmidstart.rst | 2 +- .../qemu/projectsprojectidqemuvmsvmidstop.rst | 2 +- .../projectsprojectidqemuvmsvmidsuspend.rst | 2 +- .../projectsprojectidvirtualboxvmsvmid.rst | 6 +- ...ptersadapternumberdportsportnumberdnio.rst | 8 +- ...ternumberdportsportnumberdstartcapture.rst | 4 +- ...pternumberdportsportnumberdstopcapture.rst | 4 +- ...ojectsprojectidvirtualboxvmsvmidreload.rst | 2 +- ...ojectsprojectidvirtualboxvmsvmidresume.rst | 2 +- ...rojectsprojectidvirtualboxvmsvmidstart.rst | 2 +- ...projectsprojectidvirtualboxvmsvmidstop.rst | 2 +- ...jectsprojectidvirtualboxvmsvmidsuspend.rst | 2 +- docs/api/v1/vpcs/projectsprojectidvpcsvms.rst | 1 + .../v1/vpcs/projectsprojectidvpcsvmsvmid.rst | 8 +- ...ptersadapternumberdportsportnumberdnio.rst | 8 +- .../projectsprojectidvpcsvmsvmidreload.rst | 2 +- .../projectsprojectidvpcsvmsvmidstart.rst | 2 +- .../vpcs/projectsprojectidvpcsvmsvmidstop.rst | 2 +- docs/general.rst | 156 ++++++++++++++++++ 59 files changed, 253 insertions(+), 512 deletions(-) delete mode 100644 docs/api/v1/iou.rst delete mode 100644 docs/api/v1/iou/projectsprojectidiouvms.rst delete mode 100644 docs/api/v1/iou/projectsprojectidiouvmsvmid.rst delete mode 100644 docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst delete mode 100644 docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst delete mode 100644 docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst delete mode 100644 docs/api/v1/iou/projectsprojectidiouvmsvmidinitialconfig.rst delete mode 100644 docs/api/v1/iou/projectsprojectidiouvmsvmidreload.rst delete mode 100644 docs/api/v1/iou/projectsprojectidiouvmsvmidstart.rst delete mode 100644 docs/api/v1/iou/projectsprojectidiouvmsvmidstop.rst diff --git a/docs/api/examples/get_projectsprojectid.txt b/docs/api/examples/get_projectsprojectid.txt index 559a0388..c1daf6eb 100644 --- a/docs/api/examples/get_projectsprojectid.txt +++ b/docs/api/examples/get_projectsprojectid.txt @@ -13,8 +13,8 @@ SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /v1/projects/{project_id} { - "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpjlh4s0j0", - "path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpjlh4s0j0/00010203-0405-0607-0809-0a0b0c0d0e0f", - "project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f", + "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpqekqsxgq", + "path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpqekqsxgq/00010203-0405-0607-0809-0a0b0c0d0e02", + "project_id": "00010203-0405-0607-0809-0a0b0c0d0e02", "temporary": false } diff --git a/docs/api/examples/get_projectsprojectidiouvmsvmid.txt b/docs/api/examples/get_projectsprojectidiouvmsvmid.txt index 84850ddb..5fd3cfcb 100644 --- a/docs/api/examples/get_projectsprojectidiouvmsvmid.txt +++ b/docs/api/examples/get_projectsprojectidiouvmsvmid.txt @@ -19,9 +19,9 @@ X-ROUTE: /v1/projects/{project_id}/iou/vms/{vm_id} "l1_keepalives": false, "name": "PC TEST 1", "nvram": 128, - "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3621/test_iou_get0/iou.bin", + "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3782/test_iou_get0/iou.bin", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "ram": 256, "serial_adapters": 2, - "vm_id": "f75ff9e7-e658-45f7-9021-1651cfed1194" + "vm_id": "b9022212-4b9e-48fd-b910-85f0d896f951" } diff --git a/docs/api/examples/get_projectsprojectidqemuvmsvmid.txt b/docs/api/examples/get_projectsprojectidqemuvmsvmid.txt index 47159a3c..274275c6 100644 --- a/docs/api/examples/get_projectsprojectidqemuvmsvmid.txt +++ b/docs/api/examples/get_projectsprojectidqemuvmsvmid.txt @@ -28,7 +28,7 @@ X-ROUTE: /v1/projects/{project_id}/qemu/vms/{vm_id} "options": "", "process_priority": "low", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpu7smjb0q/qemu_x42", + "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmprhap9vc_/qemu_x42", "ram": 256, - "vm_id": "b41caecc-86fc-4986-a0b2-36892ac8baba" + "vm_id": "5eee27c5-c590-4ddf-aa1f-783e15a3c41a" } diff --git a/docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt b/docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt index 9ddcdd09..6bb301b8 100644 --- a/docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt +++ b/docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt @@ -21,6 +21,6 @@ X-ROUTE: /v1/projects/{project_id}/virtualbox/vms/{vm_id} "name": "VMTEST", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "use_any_adapter": false, - "vm_id": "bb20d4fa-f233-400d-af07-2fbdcb337022", + "vm_id": "4d4c9e18-0bad-4345-9338-17b98f2f6680", "vmname": "VMTEST" } diff --git a/docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt b/docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt index 56cdbf0d..f69edb84 100644 --- a/docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt +++ b/docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt @@ -6,7 +6,7 @@ GET /projects/{project_id}/vpcs/vms/{vm_id} HTTP/1.1 HTTP/1.1 200 CONNECTION: keep-alive -CONTENT-LENGTH: 187 +CONTENT-LENGTH: 220 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT SERVER: Python/3.4 GNS3/1.3.dev1 @@ -17,5 +17,6 @@ X-ROUTE: /v1/projects/{project_id}/vpcs/vms/{vm_id} "name": "PC TEST 1", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "startup_script": null, - "vm_id": "1b0843ea-4fcf-4d2a-94e8-bc3b7a92be88" + "startup_script_path": null, + "vm_id": "ca159ab0-42d5-4c9a-bfb0-c2290ac90556" } diff --git a/docs/api/examples/post_projects.txt b/docs/api/examples/post_projects.txt index 610adf2d..cc85ac3b 100644 --- a/docs/api/examples/post_projects.txt +++ b/docs/api/examples/post_projects.txt @@ -4,7 +4,7 @@ POST /projects HTTP/1.1 {} -HTTP/1.1 200 +HTTP/1.1 201 CONNECTION: keep-alive CONTENT-LENGTH: 277 CONTENT-TYPE: application/json @@ -13,8 +13,8 @@ SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /v1/projects { - "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpanmwxfqf", - "path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpanmwxfqf/f0f4987c-b1d3-432f-a354-1179d1c727f9", - "project_id": "f0f4987c-b1d3-432f-a354-1179d1c727f9", + "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpgrmlhkz5", + "path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpgrmlhkz5/8a563c49-902c-4ee8-8a3c-d188e5832741", + "project_id": "8a563c49-902c-4ee8-8a3c-d188e5832741", "temporary": false } diff --git a/docs/api/examples/post_projectsprojectidiouvms.txt b/docs/api/examples/post_projectsprojectidiouvms.txt index 65f5b1ce..fda39294 100644 --- a/docs/api/examples/post_projectsprojectidiouvms.txt +++ b/docs/api/examples/post_projectsprojectidiouvms.txt @@ -1,4 +1,4 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/iou/vms' -d '{"ethernet_adapters": 0, "initial_config_content": "hostname test", "l1_keepalives": true, "name": "PC TEST 1", "nvram": 512, "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3621/test_iou_create_with_params0/iou.bin", "ram": 1024, "serial_adapters": 4}' +curl -i -X POST 'http://localhost:8000/projects/{project_id}/iou/vms' -d '{"ethernet_adapters": 0, "initial_config_content": "hostname test", "l1_keepalives": true, "name": "PC TEST 1", "nvram": 512, "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3782/test_iou_create_with_params0/iou.bin", "ram": 1024, "serial_adapters": 4}' POST /projects/{project_id}/iou/vms HTTP/1.1 { @@ -7,7 +7,7 @@ POST /projects/{project_id}/iou/vms HTTP/1.1 "l1_keepalives": true, "name": "PC TEST 1", "nvram": 512, - "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3621/test_iou_create_with_params0/iou.bin", + "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3782/test_iou_create_with_params0/iou.bin", "ram": 1024, "serial_adapters": 4 } @@ -28,9 +28,9 @@ X-ROUTE: /v1/projects/{project_id}/iou/vms "l1_keepalives": true, "name": "PC TEST 1", "nvram": 512, - "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3621/test_iou_create_with_params0/iou.bin", + "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3782/test_iou_create_with_params0/iou.bin", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "ram": 1024, "serial_adapters": 4, - "vm_id": "20e66cd4-52ef-4ad2-a44e-16bce06fd6f2" + "vm_id": "d6832edf-97ad-45a2-b588-c64d5c3f5dec" } diff --git a/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.txt b/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.txt index a43ed225..10093b80 100644 --- a/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.txt +++ b/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.txt @@ -16,5 +16,5 @@ SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /v1/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/start_capture { - "pcap_file_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpd25gn8du/a1e920ca-338a-4e9f-b363-aa607b09dd80/project-files/captures/test.pcap" + "pcap_file_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpina5gsqc/a1e920ca-338a-4e9f-b363-aa607b09dd80/project-files/captures/test.pcap" } diff --git a/docs/api/examples/post_projectsprojectidqemuvms.txt b/docs/api/examples/post_projectsprojectidqemuvms.txt index c301f2da..85123e88 100644 --- a/docs/api/examples/post_projectsprojectidqemuvms.txt +++ b/docs/api/examples/post_projectsprojectidqemuvms.txt @@ -1,10 +1,10 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/qemu/vms' -d '{"hda_disk_image": "hda", "name": "PC TEST 1", "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpu7smjb0q/qemu_x42", "ram": 1024}' +curl -i -X POST 'http://localhost:8000/projects/{project_id}/qemu/vms' -d '{"hda_disk_image": "hda", "name": "PC TEST 1", "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmprhap9vc_/qemu_x42", "ram": 1024}' POST /projects/{project_id}/qemu/vms HTTP/1.1 { "hda_disk_image": "hda", "name": "PC TEST 1", - "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpu7smjb0q/qemu_x42", + "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmprhap9vc_/qemu_x42", "ram": 1024 } @@ -33,7 +33,7 @@ X-ROUTE: /v1/projects/{project_id}/qemu/vms "options": "", "process_priority": "low", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpu7smjb0q/qemu_x42", + "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmprhap9vc_/qemu_x42", "ram": 1024, - "vm_id": "3176897a-996a-4020-86b8-3cd7a0031cbc" + "vm_id": "4c3596aa-e44c-4129-94e4-45dd93ef7e6b" } diff --git a/docs/api/examples/post_projectsprojectidvirtualboxvms.txt b/docs/api/examples/post_projectsprojectidvirtualboxvms.txt index 7f6b4a53..0090a3d7 100644 --- a/docs/api/examples/post_projectsprojectidvirtualboxvms.txt +++ b/docs/api/examples/post_projectsprojectidvirtualboxvms.txt @@ -25,6 +25,6 @@ X-ROUTE: /v1/projects/{project_id}/virtualbox/vms "name": "VM1", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "use_any_adapter": false, - "vm_id": "8f384969-8478-4e8a-a6cd-c91376ccc89b", + "vm_id": "d162a4e3-f28b-48db-a9f4-1c87db4ed0d2", "vmname": "VM1" } diff --git a/docs/api/examples/post_projectsprojectidvpcsvms.txt b/docs/api/examples/post_projectsprojectidvpcsvms.txt index 8f7c689f..6598d953 100644 --- a/docs/api/examples/post_projectsprojectidvpcsvms.txt +++ b/docs/api/examples/post_projectsprojectidvpcsvms.txt @@ -8,7 +8,7 @@ POST /projects/{project_id}/vpcs/vms HTTP/1.1 HTTP/1.1 201 CONNECTION: keep-alive -CONTENT-LENGTH: 187 +CONTENT-LENGTH: 220 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT SERVER: Python/3.4 GNS3/1.3.dev1 @@ -19,5 +19,6 @@ X-ROUTE: /v1/projects/{project_id}/vpcs/vms "name": "PC TEST 1", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "startup_script": null, - "vm_id": "90e4cc09-4013-4e94-97ce-3ca48676de22" + "startup_script_path": null, + "vm_id": "4bec08c2-b54b-4040-90a3-2add2fd82f32" } diff --git a/docs/api/examples/put_projectsprojectid.txt b/docs/api/examples/put_projectsprojectid.txt index 26774c07..8f5ee665 100644 --- a/docs/api/examples/put_projectsprojectid.txt +++ b/docs/api/examples/put_projectsprojectid.txt @@ -1,8 +1,8 @@ -curl -i -X PUT 'http://localhost:8000/projects/{project_id}' -d '{"path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3621/test_update_path_project_non_l0"}' +curl -i -X PUT 'http://localhost:8000/projects/{project_id}' -d '{"path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3782/test_update_path_project_non_l0"}' PUT /projects/{project_id} HTTP/1.1 { - "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3621/test_update_path_project_non_l0" + "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3782/test_update_path_project_non_l0" } diff --git a/docs/api/examples/put_projectsprojectidiouvmsvmid.txt b/docs/api/examples/put_projectsprojectidiouvmsvmid.txt index 7c4c1058..b37461ab 100644 --- a/docs/api/examples/put_projectsprojectidiouvmsvmid.txt +++ b/docs/api/examples/put_projectsprojectidiouvmsvmid.txt @@ -28,9 +28,9 @@ X-ROUTE: /v1/projects/{project_id}/iou/vms/{vm_id} "l1_keepalives": true, "name": "test", "nvram": 2048, - "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3621/test_iou_update0/iou.bin", + "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3782/test_iou_update0/iou.bin", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "ram": 512, "serial_adapters": 0, - "vm_id": "e867af73-aaf1-4770-a935-78a357ea5db3" + "vm_id": "69ad7d57-d1ec-447e-b8da-7b2f888a85e5" } diff --git a/docs/api/examples/put_projectsprojectidqemuvmsvmid.txt b/docs/api/examples/put_projectsprojectidqemuvmsvmid.txt index b52c7a2d..a6ed21bf 100644 --- a/docs/api/examples/put_projectsprojectidqemuvmsvmid.txt +++ b/docs/api/examples/put_projectsprojectidqemuvmsvmid.txt @@ -33,7 +33,7 @@ X-ROUTE: /v1/projects/{project_id}/qemu/vms/{vm_id} "options": "", "process_priority": "low", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpu7smjb0q/qemu_x42", + "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmprhap9vc_/qemu_x42", "ram": 1024, - "vm_id": "8a0d735b-d485-4a2e-bae1-be54f426fbeb" + "vm_id": "385c7895-f085-4682-b31a-a6a63930c5c4" } diff --git a/docs/api/examples/put_projectsprojectidvirtualboxvmsvmid.txt b/docs/api/examples/put_projectsprojectidvirtualboxvmsvmid.txt index 8f37d59d..e4f99347 100644 --- a/docs/api/examples/put_projectsprojectidvirtualboxvmsvmid.txt +++ b/docs/api/examples/put_projectsprojectidvirtualboxvmsvmid.txt @@ -24,6 +24,6 @@ X-ROUTE: /v1/projects/{project_id}/virtualbox/vms/{vm_id} "name": "test", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "use_any_adapter": false, - "vm_id": "dcf2c555-703d-47cb-9dd2-8ee6c11b9f6c", + "vm_id": "15c34b6c-63bb-48a6-b4b0-e772fdb74925", "vmname": "VMTEST" } diff --git a/docs/api/examples/put_projectsprojectidvpcsvmsvmid.txt b/docs/api/examples/put_projectsprojectidvpcsvmsvmid.txt index 532b274b..23adddbe 100644 --- a/docs/api/examples/put_projectsprojectidvpcsvmsvmid.txt +++ b/docs/api/examples/put_projectsprojectidvpcsvmsvmid.txt @@ -10,7 +10,7 @@ PUT /projects/{project_id}/vpcs/vms/{vm_id} HTTP/1.1 HTTP/1.1 200 CONNECTION: keep-alive -CONTENT-LENGTH: 194 +CONTENT-LENGTH: 236 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT SERVER: Python/3.4 GNS3/1.3.dev1 @@ -21,5 +21,6 @@ X-ROUTE: /v1/projects/{project_id}/vpcs/vms/{vm_id} "name": "test", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "startup_script": "ip 192.168.1.1", - "vm_id": "f27aa7da-e267-483a-adc6-2587e1377e8c" + "startup_script_path": "startup.vpc", + "vm_id": "a239c325-d437-4209-a583-9eee28905802" } diff --git a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmid.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmid.rst index 11b3c065..c8e677e4 100644 --- a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmid.rst +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmid.rst @@ -9,8 +9,8 @@ Get a Dynamips VM instance Parameters ********** -- **project_id**: UUID for the project - **vm_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** @@ -75,8 +75,8 @@ Update a Dynamips VM instance Parameters ********** -- **project_id**: UUID for the project - **vm_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** @@ -191,8 +191,8 @@ Delete a Dynamips VM instance Parameters ********** -- **project_id**: UUID for the project - **vm_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** diff --git a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst index 13c726c7..84b37b86 100644 --- a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -9,10 +9,10 @@ Add a NIO to a Dynamips VM instance Parameters ********** -- **project_id**: UUID for the project -- **adapter_number**: Adapter where the nio should be added - **vm_id**: UUID for the instance +- **project_id**: UUID for the project - **port_number**: Port on the adapter +- **adapter_number**: Adapter where the nio should be added Response status codes ********************** @@ -27,10 +27,10 @@ Remove a NIO from a Dynamips VM instance Parameters ********** -- **project_id**: UUID for the project -- **adapter_number**: Adapter from where the nio should be removed - **vm_id**: UUID for the instance +- **project_id**: UUID for the project - **port_number**: Port on the adapter +- **adapter_number**: Adapter from where the nio should be removed Response status codes ********************** diff --git a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst index 16b5b121..27ac8daf 100644 --- a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst @@ -9,10 +9,10 @@ Start a packet capture on a Dynamips VM instance Parameters ********** -- **project_id**: UUID for the project -- **adapter_number**: Adapter to start a packet capture - **vm_id**: UUID for the instance +- **project_id**: UUID for the project - **port_number**: Port on the adapter +- **adapter_number**: Adapter to start a packet capture Response status codes ********************** diff --git a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst index 5522c169..08c3cf32 100644 --- a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst @@ -9,10 +9,10 @@ Stop a packet capture on a Dynamips VM instance Parameters ********** -- **project_id**: UUID for the project -- **adapter_number**: Adapter to stop a packet capture - **vm_id**: UUID for the instance +- **project_id**: UUID for the project - **port_number**: Port on the adapter (always 0) +- **adapter_number**: Adapter to stop a packet capture Response status codes ********************** diff --git a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidreload.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidreload.rst index 23bb67f3..d7d7df55 100644 --- a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidreload.rst +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidreload.rst @@ -9,8 +9,8 @@ Reload a Dynamips VM instance Parameters ********** -- **project_id**: UUID for the project - **vm_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** diff --git a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidresume.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidresume.rst index 73ce9d01..fcd48ab5 100644 --- a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidresume.rst +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidresume.rst @@ -9,8 +9,8 @@ Resume a suspended Dynamips VM instance Parameters ********** -- **project_id**: UUID for the project - **vm_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** diff --git a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidstart.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidstart.rst index 24c3d2af..2dbd25b8 100644 --- a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidstart.rst +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidstart.rst @@ -9,8 +9,8 @@ Start a Dynamips VM instance Parameters ********** -- **project_id**: UUID for the project - **vm_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** diff --git a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidstop.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidstop.rst index fca471b6..ff62c2c9 100644 --- a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidstop.rst +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidstop.rst @@ -9,8 +9,8 @@ Stop a Dynamips VM instance Parameters ********** -- **project_id**: UUID for the project - **vm_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** diff --git a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidsuspend.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidsuspend.rst index 54394e01..b6fb8a13 100644 --- a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidsuspend.rst +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidsuspend.rst @@ -9,8 +9,8 @@ Suspend a Dynamips VM instance Parameters ********** -- **project_id**: UUID for the project - **vm_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** diff --git a/docs/api/v1/iou.rst b/docs/api/v1/iou.rst deleted file mode 100644 index c2188031..00000000 --- a/docs/api/v1/iou.rst +++ /dev/null @@ -1,8 +0,0 @@ -Iou ---------------------- - -.. toctree:: - :glob: - :maxdepth: 2 - - iou/* diff --git a/docs/api/v1/iou/projectsprojectidiouvms.rst b/docs/api/v1/iou/projectsprojectidiouvms.rst deleted file mode 100644 index 5944bf4d..00000000 --- a/docs/api/v1/iou/projectsprojectidiouvms.rst +++ /dev/null @@ -1,62 +0,0 @@ -/v1/projects/{project_id}/iou/vms ----------------------------------------------------------------------------------------------------------------------- - -.. contents:: - -POST /v1/projects/**{project_id}**/iou/vms -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Create a new IOU instance - -Parameters -********** -- **project_id**: UUID for the project - -Response status codes -********************** -- **400**: Invalid request -- **201**: Instance created -- **409**: Conflict - -Input -******* -.. raw:: html - - - - - - - - - - - - - -
Name Mandatory Type Description
console ['integer', 'null'] console TCP port
ethernet_adapters integer How many ethernet adapters are connected to the IOU
initial_config_content ['string', 'null'] Initial configuration of the IOU
l1_keepalives ['boolean', 'null'] Always up ethernet interface
name string IOU VM name
nvram ['integer', 'null'] Allocated NVRAM KB
path string Path of iou binary
ram ['integer', 'null'] Allocated RAM MB
serial_adapters integer How many serial adapters are connected to the IOU
vm_id IOU VM identifier
- -Output -******* -.. raw:: html - - - - - - - - - - - - - - -
Name Mandatory Type Description
console integer console TCP port
ethernet_adapters integer How many ethernet adapters are connected to the IOU
initial_config ['string', 'null'] Path of the initial config content relative to project directory
l1_keepalives boolean Always up ethernet interface
name string IOU VM name
nvram integer Allocated NVRAM KB
path string Path of iou binary
project_id string Project UUID
ram integer Allocated RAM MB
serial_adapters integer How many serial adapters are connected to the IOU
vm_id string IOU VM UUID
- -Sample session -*************** - - -.. literalinclude:: ../../examples/post_projectsprojectidiouvms.txt - diff --git a/docs/api/v1/iou/projectsprojectidiouvmsvmid.rst b/docs/api/v1/iou/projectsprojectidiouvmsvmid.rst deleted file mode 100644 index b4bb82f6..00000000 --- a/docs/api/v1/iou/projectsprojectidiouvmsvmid.rst +++ /dev/null @@ -1,126 +0,0 @@ -/v1/projects/{project_id}/iou/vms/{vm_id} ----------------------------------------------------------------------------------------------------------------------- - -.. contents:: - -GET /v1/projects/**{project_id}**/iou/vms/**{vm_id}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Get a IOU instance - -Parameters -********** -- **project_id**: UUID for the project -- **vm_id**: UUID for the instance - -Response status codes -********************** -- **200**: Success -- **400**: Invalid request -- **404**: Instance doesn't exist - -Output -******* -.. raw:: html - - - - - - - - - - - - - - -
Name Mandatory Type Description
console integer console TCP port
ethernet_adapters integer How many ethernet adapters are connected to the IOU
initial_config ['string', 'null'] Path of the initial config content relative to project directory
l1_keepalives boolean Always up ethernet interface
name string IOU VM name
nvram integer Allocated NVRAM KB
path string Path of iou binary
project_id string Project UUID
ram integer Allocated RAM MB
serial_adapters integer How many serial adapters are connected to the IOU
vm_id string IOU VM UUID
- -Sample session -*************** - - -.. literalinclude:: ../../examples/get_projectsprojectidiouvmsvmid.txt - - -PUT /v1/projects/**{project_id}**/iou/vms/**{vm_id}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Update a IOU instance - -Parameters -********** -- **project_id**: UUID for the project -- **vm_id**: UUID for the instance - -Response status codes -********************** -- **200**: Instance updated -- **400**: Invalid request -- **404**: Instance doesn't exist -- **409**: Conflict - -Input -******* -.. raw:: html - - - - - - - - - - - - -
Name Mandatory Type Description
console ['integer', 'null'] console TCP port
ethernet_adapters ['integer', 'null'] How many ethernet adapters are connected to the IOU
initial_config_content ['string', 'null'] Initial configuration of the IOU
l1_keepalives ['boolean', 'null'] Always up ethernet interface
name ['string', 'null'] IOU VM name
nvram ['integer', 'null'] Allocated NVRAM KB
path ['string', 'null'] Path of iou binary
ram ['integer', 'null'] Allocated RAM MB
serial_adapters ['integer', 'null'] How many serial adapters are connected to the IOU
- -Output -******* -.. raw:: html - - - - - - - - - - - - - - -
Name Mandatory Type Description
console integer console TCP port
ethernet_adapters integer How many ethernet adapters are connected to the IOU
initial_config ['string', 'null'] Path of the initial config content relative to project directory
l1_keepalives boolean Always up ethernet interface
name string IOU VM name
nvram integer Allocated NVRAM KB
path string Path of iou binary
project_id string Project UUID
ram integer Allocated RAM MB
serial_adapters integer How many serial adapters are connected to the IOU
vm_id string IOU VM UUID
- -Sample session -*************** - - -.. literalinclude:: ../../examples/put_projectsprojectidiouvmsvmid.txt - - -DELETE /v1/projects/**{project_id}**/iou/vms/**{vm_id}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Delete a IOU instance - -Parameters -********** -- **project_id**: UUID for the project -- **vm_id**: UUID for the instance - -Response status codes -********************** -- **400**: Invalid request -- **404**: Instance doesn't exist -- **204**: Instance deleted - -Sample session -*************** - - -.. literalinclude:: ../../examples/delete_projectsprojectidiouvmsvmid.txt - diff --git a/docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst deleted file mode 100644 index 6b9abacb..00000000 --- a/docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ /dev/null @@ -1,52 +0,0 @@ -/v1/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio ----------------------------------------------------------------------------------------------------------------------- - -.. contents:: - -POST /v1/projects/**{project_id}**/iou/vms/**{vm_id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/nio -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Add a NIO to a IOU instance - -Parameters -********** -- **project_id**: UUID for the project -- **adapter_number**: Network adapter where the nio is located -- **vm_id**: UUID for the instance -- **port_number**: Port where the nio should be added - -Response status codes -********************** -- **400**: Invalid request -- **201**: NIO created -- **404**: Instance doesn't exist - -Sample session -*************** - - -.. literalinclude:: ../../examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt - - -DELETE /v1/projects/**{project_id}**/iou/vms/**{vm_id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/nio -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Remove a NIO from a IOU instance - -Parameters -********** -- **project_id**: UUID for the project -- **adapter_number**: Network adapter where the nio is located -- **vm_id**: UUID for the instance -- **port_number**: Port from where the nio should be removed - -Response status codes -********************** -- **400**: Invalid request -- **404**: Instance doesn't exist -- **204**: NIO deleted - -Sample session -*************** - - -.. literalinclude:: ../../examples/delete_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt - diff --git a/docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst b/docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst deleted file mode 100644 index 31e72cd4..00000000 --- a/docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst +++ /dev/null @@ -1,38 +0,0 @@ -/v1/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/start_capture ----------------------------------------------------------------------------------------------------------------------- - -.. contents:: - -POST /v1/projects/**{project_id}**/iou/vms/**{vm_id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/start_capture -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Start a packet capture on a IOU VM instance - -Parameters -********** -- **project_id**: UUID for the project -- **adapter_number**: Adapter to start a packet capture -- **vm_id**: UUID for the instance -- **port_number**: Port on the adapter - -Response status codes -********************** -- **200**: Capture started -- **400**: Invalid request -- **404**: Instance doesn't exist - -Input -******* -.. raw:: html - - - - - -
Name Mandatory Type Description
capture_file_name string Capture file name
data_link_type string PCAP data link type
- -Sample session -*************** - - -.. literalinclude:: ../../examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.txt - diff --git a/docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst b/docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst deleted file mode 100644 index ca5e1abc..00000000 --- a/docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst +++ /dev/null @@ -1,28 +0,0 @@ -/v1/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/stop_capture ----------------------------------------------------------------------------------------------------------------------- - -.. contents:: - -POST /v1/projects/**{project_id}**/iou/vms/**{vm_id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/stop_capture -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Stop a packet capture on a IOU VM instance - -Parameters -********** -- **project_id**: UUID for the project -- **adapter_number**: Adapter to stop a packet capture -- **vm_id**: UUID for the instance -- **port_number**: Port on the adapter (always 0) - -Response status codes -********************** -- **400**: Invalid request -- **404**: Instance doesn't exist -- **204**: Capture stopped - -Sample session -*************** - - -.. literalinclude:: ../../examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.txt - diff --git a/docs/api/v1/iou/projectsprojectidiouvmsvmidinitialconfig.rst b/docs/api/v1/iou/projectsprojectidiouvmsvmidinitialconfig.rst deleted file mode 100644 index 6159e783..00000000 --- a/docs/api/v1/iou/projectsprojectidiouvmsvmidinitialconfig.rst +++ /dev/null @@ -1,30 +0,0 @@ -/v1/projects/{project_id}/iou/vms/{vm_id}/initial_config ----------------------------------------------------------------------------------------------------------------------- - -.. contents:: - -GET /v1/projects/**{project_id}**/iou/vms/**{vm_id}**/initial_config -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Retrieve the initial config content - -Response status codes -********************** -- **200**: Initial config retrieved -- **400**: Invalid request -- **404**: Instance doesn't exist - -Output -******* -.. raw:: html - - - - -
Name Mandatory Type Description
content ['string', 'null'] Content of the initial configuration file
- -Sample session -*************** - - -.. literalinclude:: ../../examples/get_projectsprojectidiouvmsvmidinitialconfig.txt - diff --git a/docs/api/v1/iou/projectsprojectidiouvmsvmidreload.rst b/docs/api/v1/iou/projectsprojectidiouvmsvmidreload.rst deleted file mode 100644 index 49be3d31..00000000 --- a/docs/api/v1/iou/projectsprojectidiouvmsvmidreload.rst +++ /dev/null @@ -1,26 +0,0 @@ -/v1/projects/{project_id}/iou/vms/{vm_id}/reload ----------------------------------------------------------------------------------------------------------------------- - -.. contents:: - -POST /v1/projects/**{project_id}**/iou/vms/**{vm_id}**/reload -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Reload a IOU instance - -Parameters -********** -- **project_id**: UUID for the project -- **vm_id**: UUID for the instance - -Response status codes -********************** -- **400**: Invalid request -- **404**: Instance doesn't exist -- **204**: Instance reloaded - -Sample session -*************** - - -.. literalinclude:: ../../examples/post_projectsprojectidiouvmsvmidreload.txt - diff --git a/docs/api/v1/iou/projectsprojectidiouvmsvmidstart.rst b/docs/api/v1/iou/projectsprojectidiouvmsvmidstart.rst deleted file mode 100644 index 25a9e236..00000000 --- a/docs/api/v1/iou/projectsprojectidiouvmsvmidstart.rst +++ /dev/null @@ -1,26 +0,0 @@ -/v1/projects/{project_id}/iou/vms/{vm_id}/start ----------------------------------------------------------------------------------------------------------------------- - -.. contents:: - -POST /v1/projects/**{project_id}**/iou/vms/**{vm_id}**/start -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Start a IOU instance - -Parameters -********** -- **project_id**: UUID for the project -- **vm_id**: UUID for the instance - -Response status codes -********************** -- **400**: Invalid request -- **404**: Instance doesn't exist -- **204**: Instance started - -Sample session -*************** - - -.. literalinclude:: ../../examples/post_projectsprojectidiouvmsvmidstart.txt - diff --git a/docs/api/v1/iou/projectsprojectidiouvmsvmidstop.rst b/docs/api/v1/iou/projectsprojectidiouvmsvmidstop.rst deleted file mode 100644 index 220c81ad..00000000 --- a/docs/api/v1/iou/projectsprojectidiouvmsvmidstop.rst +++ /dev/null @@ -1,26 +0,0 @@ -/v1/projects/{project_id}/iou/vms/{vm_id}/stop ----------------------------------------------------------------------------------------------------------------------- - -.. contents:: - -POST /v1/projects/**{project_id}**/iou/vms/**{vm_id}**/stop -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Stop a IOU instance - -Parameters -********** -- **project_id**: UUID for the project -- **vm_id**: UUID for the instance - -Response status codes -********************** -- **400**: Invalid request -- **404**: Instance doesn't exist -- **204**: Instance stopped - -Sample session -*************** - - -.. literalinclude:: ../../examples/post_projectsprojectidiouvmsvmidstop.txt - diff --git a/docs/api/v1/project/projects.rst b/docs/api/v1/project/projects.rst index 2cc133fb..53f234b7 100644 --- a/docs/api/v1/project/projects.rst +++ b/docs/api/v1/project/projects.rst @@ -9,7 +9,8 @@ Create a new project on the server Response status codes ********************** -- **200**: OK +- **201**: Project created +- **409**: Project already created Input ******* diff --git a/docs/api/v1/qemu/projectsprojectidqemuvmsvmid.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmid.rst index 0270bbd9..dad9bd01 100644 --- a/docs/api/v1/qemu/projectsprojectidqemuvmsvmid.rst +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmid.rst @@ -9,8 +9,8 @@ Get a Qemu.instance Parameters ********** -- **project_id**: UUID for the project - **vm_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** @@ -57,8 +57,8 @@ Update a Qemu.instance Parameters ********** -- **project_id**: UUID for the project - **vm_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** @@ -130,8 +130,8 @@ Delete a Qemu.instance Parameters ********** -- **project_id**: UUID for the project - **vm_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** diff --git a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst index 985f53e0..fb26caca 100644 --- a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -9,10 +9,10 @@ Add a NIO to a Qemu.instance Parameters ********** -- **project_id**: UUID for the project -- **adapter_number**: Network adapter where the nio is located - **vm_id**: UUID for the instance +- **project_id**: UUID for the project - **port_number**: Port where the nio should be added +- **adapter_number**: Network adapter where the nio is located Response status codes ********************** @@ -33,10 +33,10 @@ Remove a NIO from a Qemu.instance Parameters ********** -- **project_id**: UUID for the project -- **adapter_number**: Network adapter where the nio is located - **vm_id**: UUID for the instance +- **project_id**: UUID for the project - **port_number**: Port from where the nio should be removed +- **adapter_number**: Network adapter where the nio is located Response status codes ********************** diff --git a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidreload.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidreload.rst index 5b29cec3..a33af01f 100644 --- a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidreload.rst +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidreload.rst @@ -9,8 +9,8 @@ Reload a Qemu.instance Parameters ********** -- **project_id**: UUID for the project - **vm_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** diff --git a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidresume.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidresume.rst index 59b5c1f7..c3d3b967 100644 --- a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidresume.rst +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidresume.rst @@ -9,8 +9,8 @@ Resume a Qemu.instance Parameters ********** -- **project_id**: UUID for the project - **vm_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** diff --git a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstart.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstart.rst index f5306892..063832b1 100644 --- a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstart.rst +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstart.rst @@ -9,8 +9,8 @@ Start a Qemu.instance Parameters ********** -- **project_id**: UUID for the project - **vm_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** diff --git a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstop.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstop.rst index d5c41c96..bb6af81f 100644 --- a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstop.rst +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstop.rst @@ -9,8 +9,8 @@ Stop a Qemu.instance Parameters ********** -- **project_id**: UUID for the project - **vm_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** diff --git a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidsuspend.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidsuspend.rst index 63e8c94a..36f0e34d 100644 --- a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidsuspend.rst +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidsuspend.rst @@ -9,8 +9,8 @@ Suspend a Qemu.instance Parameters ********** -- **project_id**: UUID for the project - **vm_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** diff --git a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmid.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmid.rst index 8ec38157..792e2e61 100644 --- a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmid.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmid.rst @@ -9,8 +9,8 @@ Get a VirtualBox VM instance Parameters ********** -- **project_id**: UUID for the project - **vm_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** @@ -49,8 +49,8 @@ Update a VirtualBox VM instance Parameters ********** -- **project_id**: UUID for the project - **vm_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** @@ -106,8 +106,8 @@ Delete a VirtualBox VM instance Parameters ********** -- **project_id**: UUID for the project - **vm_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** diff --git a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst index 8e8016f4..6ba2ff9b 100644 --- a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -9,10 +9,10 @@ Add a NIO to a VirtualBox VM instance Parameters ********** -- **project_id**: UUID for the project -- **adapter_number**: Adapter where the nio should be added - **vm_id**: UUID for the instance +- **project_id**: UUID for the project - **port_number**: Port on the adapter (always 0) +- **adapter_number**: Adapter where the nio should be added Response status codes ********************** @@ -33,10 +33,10 @@ Remove a NIO from a VirtualBox VM instance Parameters ********** -- **project_id**: UUID for the project -- **adapter_number**: Adapter from where the nio should be removed - **vm_id**: UUID for the instance +- **project_id**: UUID for the project - **port_number**: Port on the adapter (always) +- **adapter_number**: Adapter from where the nio should be removed Response status codes ********************** diff --git a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst index 31103c86..41d3ee78 100644 --- a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst @@ -9,10 +9,10 @@ Start a packet capture on a VirtualBox VM instance Parameters ********** -- **project_id**: UUID for the project -- **adapter_number**: Adapter to start a packet capture - **vm_id**: UUID for the instance +- **project_id**: UUID for the project - **port_number**: Port on the adapter (always 0) +- **adapter_number**: Adapter to start a packet capture Response status codes ********************** diff --git a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst index 0571712e..61601daf 100644 --- a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst @@ -9,10 +9,10 @@ Stop a packet capture on a VirtualBox VM instance Parameters ********** -- **project_id**: UUID for the project -- **adapter_number**: Adapter to stop a packet capture - **vm_id**: UUID for the instance +- **project_id**: UUID for the project - **port_number**: Port on the adapter (always 0) +- **adapter_number**: Adapter to stop a packet capture Response status codes ********************** diff --git a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidreload.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidreload.rst index 9ae84c29..d3c83c0e 100644 --- a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidreload.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidreload.rst @@ -9,8 +9,8 @@ Reload a VirtualBox VM instance Parameters ********** -- **project_id**: UUID for the project - **vm_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** diff --git a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidresume.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidresume.rst index 0fb9d427..e05c62dc 100644 --- a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidresume.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidresume.rst @@ -9,8 +9,8 @@ Resume a suspended VirtualBox VM instance Parameters ********** -- **project_id**: UUID for the project - **vm_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** diff --git a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidstart.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidstart.rst index 5e6a6c42..5901fdbf 100644 --- a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidstart.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidstart.rst @@ -9,8 +9,8 @@ Start a VirtualBox VM instance Parameters ********** -- **project_id**: UUID for the project - **vm_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** diff --git a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidstop.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidstop.rst index 1eaac889..cc20ef18 100644 --- a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidstop.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidstop.rst @@ -9,8 +9,8 @@ Stop a VirtualBox VM instance Parameters ********** -- **project_id**: UUID for the project - **vm_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** diff --git a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidsuspend.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidsuspend.rst index ad7f469b..e957b666 100644 --- a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidsuspend.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidsuspend.rst @@ -9,8 +9,8 @@ Suspend a VirtualBox VM instance Parameters ********** -- **project_id**: UUID for the project - **vm_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** diff --git a/docs/api/v1/vpcs/projectsprojectidvpcsvms.rst b/docs/api/v1/vpcs/projectsprojectidvpcsvms.rst index 8b66e040..3bc1e779 100644 --- a/docs/api/v1/vpcs/projectsprojectidvpcsvms.rst +++ b/docs/api/v1/vpcs/projectsprojectidvpcsvms.rst @@ -39,6 +39,7 @@ Output name ✔ string VPCS VM name project_id ✔ string Project UUID startup_script ['string', 'null'] Content of the VPCS startup script + startup_script_path ✔ ['string', 'null'] Path of the VPCS startup script relative to project directory vm_id ✔ string VPCS VM UUID diff --git a/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmid.rst b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmid.rst index dcfee3cf..0ff49d0a 100644 --- a/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmid.rst +++ b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmid.rst @@ -9,8 +9,8 @@ Get a VPCS instance Parameters ********** -- **project_id**: UUID for the project - **vm_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** @@ -28,6 +28,7 @@ Output name ✔ string VPCS VM name project_id ✔ string Project UUID startup_script ['string', 'null'] Content of the VPCS startup script + startup_script_path ✔ ['string', 'null'] Path of the VPCS startup script relative to project directory vm_id ✔ string VPCS VM UUID @@ -44,8 +45,8 @@ Update a VPCS instance Parameters ********** -- **project_id**: UUID for the project - **vm_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** @@ -75,6 +76,7 @@ Output name ✔ string VPCS VM name project_id ✔ string Project UUID startup_script ['string', 'null'] Content of the VPCS startup script + startup_script_path ✔ ['string', 'null'] Path of the VPCS startup script relative to project directory vm_id ✔ string VPCS VM UUID @@ -91,8 +93,8 @@ Delete a VPCS instance Parameters ********** -- **project_id**: UUID for the project - **vm_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** diff --git a/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst index fcfff01d..f4ef7ce3 100644 --- a/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -9,10 +9,10 @@ Add a NIO to a VPCS instance Parameters ********** -- **project_id**: UUID for the project -- **adapter_number**: Network adapter where the nio is located - **vm_id**: UUID for the instance +- **project_id**: UUID for the project - **port_number**: Port where the nio should be added +- **adapter_number**: Network adapter where the nio is located Response status codes ********************** @@ -33,10 +33,10 @@ Remove a NIO from a VPCS instance Parameters ********** -- **project_id**: UUID for the project -- **adapter_number**: Network adapter where the nio is located - **vm_id**: UUID for the instance +- **project_id**: UUID for the project - **port_number**: Port from where the nio should be removed +- **adapter_number**: Network adapter where the nio is located Response status codes ********************** diff --git a/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidreload.rst b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidreload.rst index 224798fd..0aa2eea1 100644 --- a/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidreload.rst +++ b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidreload.rst @@ -9,8 +9,8 @@ Reload a VPCS instance Parameters ********** -- **project_id**: UUID for the project - **vm_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** diff --git a/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidstart.rst b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidstart.rst index b87f43a6..838a512b 100644 --- a/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidstart.rst +++ b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidstart.rst @@ -9,8 +9,8 @@ Start a VPCS instance Parameters ********** -- **project_id**: UUID for the project - **vm_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** diff --git a/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidstop.rst b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidstop.rst index 269c8953..606cc8fb 100644 --- a/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidstop.rst +++ b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidstop.rst @@ -9,8 +9,8 @@ Stop a VPCS instance Parameters ********** -- **project_id**: UUID for the project - **vm_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** diff --git a/docs/general.rst b/docs/general.rst index 69afacfc..7632459f 100644 --- a/docs/general.rst +++ b/docs/general.rst @@ -16,6 +16,162 @@ JSON like that "message": "Conflict" } +Sample session using curl +========================= + +.. warning:: + + Beware the output of this sample is truncated in order + to simplify the understanding. Please read the + documentation for the exact output. + +You can check the server version with a simple curl command: + +.. code-block:: shell-session + + # curl "http://localhost:8000/v1/version" + { + "version": "1.3.dev1" + } + + +The next step is to create a project. + +.. code-block:: shell-session + + # curl -X POST "http://localhost:8000/v1/projects" -d "{}" + { + "project_id": "42f9feee-3217-4104-981e-85d5f0a806ec", + "temporary": false + } + +With this project id we can now create two VPCS VM. + +.. code-block:: shell-session + + # curl -X POST "http://localhost:8000/v1/projects/42f9feee-3217-4104-981e-85d5f0a806ec/vpcs/vms" -d '{"name": "VPCS 1"}' + { + "console": 2000, + "name": "VPCS 1", + "project_id": "42f9feee-3217-4104-981e-85d5f0a806ec", + "vm_id": "24d2e16b-fbef-4259-ae34-7bc21a41ee28" + }% + + # curl -X POST "http://localhost:8000/v1/projects/42f9feee-3217-4104-981e-85d5f0a806ec/vpcs/vms" -d '{"name": "VPCS 2"}' + { + "console": 2001, + "name": "VPCS 2", + "vm_id": "daefc24a-103c-4717-8e01-6517d931c1ae" + } + +Now we need to link the two VPCS. The first step is to allocate on the remote servers +two UDP ports. + +.. code-block:: shell-session + + # curl -X POST "http://localhost:8000/v1/ports/udp" -d '{}' + { + "udp_port": 10000 + } + + # curl -X POST "http://localhost:8000/v1/ports/udp" -d '{}' + { + "udp_port": 10001 + } + + +We can create the bidirectionnal communication between the two VPCS. The +communication is made by creating two UDP tunnels. + +.. code-block:: shell-session + + # curl -X POST "http://localhost:8000/v1/projects/42f9feee-3217-4104-981e-85d5f0a806ec/vpcs/vms/24d2e16b-fbef-4259-ae34-7bc21a41ee28/adapters/0/ports/0/nio" -d '{"lport": 10000, "rhost": "127.0.0.1", "rport": 10001, "type": "nio_udp"}' + { + "lport": 10000, + "rhost": "127.0.0.1", + "rport": 10001, + "type": "nio_udp" + } + + # curl -X POST "http://localhost:8000/v1/projects/42f9feee-3217-4104-981e-85d5f0a806ec/vpcs/vms/daefc24a-103c-4717-8e01-6517d931c1ae/adapters/0/ports/0/nio" -d '{"lport": 10001, "rhost": "127.0.0.1", "rport": 10000, "type": "nio_udp"}' + { + "lport": 10001, + "rhost": "127.0.0.1", + "rport": 10000, + "type": "nio_udp" + } + +Now we can start the two VM + +.. code-block:: shell-session + + # curl -X POST "http://localhost:8000/v1/projects/42f9feee-3217-4104-981e-85d5f0a806ec/vpcs/vms/24d2e16b-fbef-4259-ae34-7bc21a41ee28/start" -d "{}" + # curl -X POST "http://localhost:8000/v1/projects/42f9feee-3217-4104-981e-85d5f0a806ec/vpcs/vms/daefc24a-103c-4717-8e01-6517d931c1ae/start" -d '{}' + +Everything should be started now. You can connect via telnet to the different VM. +The port is the field console in the create VM request. + +.. code-block:: shell-session + + # telnet 127.0.0.1 2000 + Trying 127.0.0.1... + Connected to localhost. + Escape character is '^]'. + + Welcome to Virtual PC Simulator, version 0.6 + Dedicated to Daling. + Build time: Dec 29 2014 12:51:46 + Copyright (c) 2007-2014, Paul Meng (mirnshi@gmail.com) + All rights reserved. + + VPCS is free software, distributed under the terms of the "BSD" licence. + Source code and license can be found at vpcs.sf.net. + For more information, please visit wiki.freecode.com.cn. + + Press '?' to get help. + + VPCS> ip 192.168.1.1 + Checking for duplicate address... + PC1 : 192.168.1.1 255.255.255.0 + + VPCS> disconnect + + Good-bye + Connection closed by foreign host. + + # telnet 127.0.0.1 2001 + telnet 127.0.0.1 2001 + Trying 127.0.0.1... + Connected to localhost. + Escape character is '^]'. + + Welcome to Virtual PC Simulator, version 0.6 + Dedicated to Daling. + Build time: Dec 29 2014 12:51:46 + Copyright (c) 2007-2014, Paul Meng (mirnshi@gmail.com) + All rights reserved. + + VPCS is free software, distributed under the terms of the "BSD" licence. + Source code and license can be found at vpcs.sf.net. + For more information, please visit wiki.freecode.com.cn. + + Press '?' to get help. + + VPCS> ip 192.168.1.2 + Checking for duplicate address... + PC1 : 192.168.1.2 255.255.255.0 + + VPCS> ping 192.168.1.1 + 84 bytes from 192.168.1.1 icmp_seq=1 ttl=64 time=0.179 ms + 84 bytes from 192.168.1.1 icmp_seq=2 ttl=64 time=0.218 ms + 84 bytes from 192.168.1.1 icmp_seq=3 ttl=64 time=0.190 ms + 84 bytes from 192.168.1.1 icmp_seq=4 ttl=64 time=0.198 ms + 84 bytes from 192.168.1.1 icmp_seq=5 ttl=64 time=0.185 ms + + VPCS> disconnect + Good-bye + Connection closed by foreign host. + Limitations ============ From d0c386860e2d15d0df25cac334efbc9325179cb0 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 27 Feb 2015 18:47:08 +0100 Subject: [PATCH 345/485] Fix curl sample generation in the documentation --- docs/api/examples/delete_projectsprojectid.txt | 4 ++-- .../examples/delete_projectsprojectidiouvmsvmid.txt | 4 ++-- ...msvmidadaptersadapternumberdportsportnumberdnio.txt | 4 ++-- .../examples/delete_projectsprojectidqemuvmsvmid.txt | 4 ++-- ...msvmidadaptersadapternumberdportsportnumberdnio.txt | 4 ++-- ...msvmidadaptersadapternumberdportsportnumberdnio.txt | 4 ++-- .../examples/delete_projectsprojectidvpcsvmsvmid.txt | 4 ++-- ...msvmidadaptersadapternumberdportsportnumberdnio.txt | 4 ++-- docs/api/examples/get_projectsprojectid.txt | 8 ++++---- docs/api/examples/get_projectsprojectidiouvmsvmid.txt | 8 ++++---- .../get_projectsprojectidiouvmsvmidinitialconfig.txt | 4 ++-- docs/api/examples/get_projectsprojectidqemuvmsvmid.txt | 8 ++++---- .../get_projectsprojectidvirtualboxvmsvmid.txt | 6 +++--- docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt | 6 +++--- docs/api/examples/post_projects.txt | 6 +++--- docs/api/examples/post_projectsprojectidclose.txt | 4 ++-- docs/api/examples/post_projectsprojectidcommit.txt | 4 ++-- docs/api/examples/post_projectsprojectidiouvms.txt | 10 +++++----- ...msvmidadaptersadapternumberdportsportnumberdnio.txt | 4 ++-- ...ptersadapternumberdportsportnumberdstartcapture.txt | 6 +++--- ...aptersadapternumberdportsportnumberdstopcapture.txt | 4 ++-- .../post_projectsprojectidiouvmsvmidreload.txt | 4 ++-- .../examples/post_projectsprojectidiouvmsvmidstart.txt | 4 ++-- .../examples/post_projectsprojectidiouvmsvmidstop.txt | 4 ++-- docs/api/examples/post_projectsprojectidqemuvms.txt | 10 +++++----- ...msvmidadaptersadapternumberdportsportnumberdnio.txt | 4 ++-- .../post_projectsprojectidqemuvmsvmidreload.txt | 4 ++-- .../post_projectsprojectidqemuvmsvmidresume.txt | 4 ++-- .../post_projectsprojectidqemuvmsvmidstart.txt | 4 ++-- .../examples/post_projectsprojectidqemuvmsvmidstop.txt | 4 ++-- .../post_projectsprojectidqemuvmsvmidsuspend.txt | 4 ++-- .../examples/post_projectsprojectidvirtualboxvms.txt | 6 +++--- ...msvmidadaptersadapternumberdportsportnumberdnio.txt | 4 ++-- .../post_projectsprojectidvirtualboxvmsvmidreload.txt | 4 ++-- .../post_projectsprojectidvirtualboxvmsvmidresume.txt | 4 ++-- .../post_projectsprojectidvirtualboxvmsvmidstart.txt | 4 ++-- .../post_projectsprojectidvirtualboxvmsvmidstop.txt | 4 ++-- .../post_projectsprojectidvirtualboxvmsvmidsuspend.txt | 4 ++-- docs/api/examples/post_projectsprojectidvpcsvms.txt | 6 +++--- ...msvmidadaptersadapternumberdportsportnumberdnio.txt | 4 ++-- .../post_projectsprojectidvpcsvmsvmidreload.txt | 4 ++-- .../post_projectsprojectidvpcsvmsvmidstart.txt | 4 ++-- .../examples/post_projectsprojectidvpcsvmsvmidstop.txt | 4 ++-- docs/api/examples/put_projectsprojectid.txt | 6 +++--- docs/api/examples/put_projectsprojectidiouvmsvmid.txt | 8 ++++---- docs/api/examples/put_projectsprojectidqemuvmsvmid.txt | 8 ++++---- .../put_projectsprojectidvirtualboxvmsvmid.txt | 6 +++--- docs/api/examples/put_projectsprojectidvpcsvmsvmid.txt | 6 +++--- ...ectiddynamipsdevicesdeviceidportsportnumberdnio.rst | 4 ++-- ...mipsdevicesdeviceidportsportnumberdstartcapture.rst | 2 +- ...amipsdevicesdeviceidportsportnumberdstopcapture.rst | 2 +- .../dynamips_vm/projectsprojectiddynamipsvmsvmid.rst | 6 +++--- ...msvmidadaptersadapternumberdportsportnumberdnio.rst | 8 ++++---- ...ptersadapternumberdportsportnumberdstartcapture.rst | 4 ++-- ...aptersadapternumberdportsportnumberdstopcapture.rst | 4 ++-- .../projectsprojectiddynamipsvmsvmidreload.rst | 2 +- .../projectsprojectiddynamipsvmsvmidresume.rst | 2 +- .../projectsprojectiddynamipsvmsvmidstart.rst | 2 +- .../projectsprojectiddynamipsvmsvmidstop.rst | 2 +- .../projectsprojectiddynamipsvmsvmidsuspend.rst | 2 +- docs/api/v1/qemu/projectsprojectidqemuvmsvmid.rst | 6 +++--- ...msvmidadaptersadapternumberdportsportnumberdnio.rst | 8 ++++---- .../api/v1/qemu/projectsprojectidqemuvmsvmidreload.rst | 2 +- .../api/v1/qemu/projectsprojectidqemuvmsvmidresume.rst | 2 +- docs/api/v1/qemu/projectsprojectidqemuvmsvmidstart.rst | 2 +- docs/api/v1/qemu/projectsprojectidqemuvmsvmidstop.rst | 2 +- .../v1/qemu/projectsprojectidqemuvmsvmidsuspend.rst | 2 +- .../virtualbox/projectsprojectidvirtualboxvmsvmid.rst | 6 +++--- ...msvmidadaptersadapternumberdportsportnumberdnio.rst | 8 ++++---- ...ptersadapternumberdportsportnumberdstartcapture.rst | 4 ++-- ...aptersadapternumberdportsportnumberdstopcapture.rst | 4 ++-- .../projectsprojectidvirtualboxvmsvmidreload.rst | 2 +- .../projectsprojectidvirtualboxvmsvmidresume.rst | 2 +- .../projectsprojectidvirtualboxvmsvmidstart.rst | 2 +- .../projectsprojectidvirtualboxvmsvmidstop.rst | 2 +- .../projectsprojectidvirtualboxvmsvmidsuspend.rst | 2 +- docs/api/v1/vpcs/projectsprojectidvpcsvmsvmid.rst | 6 +++--- ...msvmidadaptersadapternumberdportsportnumberdnio.rst | 8 ++++---- .../api/v1/vpcs/projectsprojectidvpcsvmsvmidreload.rst | 2 +- docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidstart.rst | 2 +- docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidstop.rst | 2 +- gns3server/handlers/__init__.py | 1 - tests/handlers/api/base.py | 6 +++--- 83 files changed, 182 insertions(+), 183 deletions(-) diff --git a/docs/api/examples/delete_projectsprojectid.txt b/docs/api/examples/delete_projectsprojectid.txt index 45efff6c..7d59321b 100644 --- a/docs/api/examples/delete_projectsprojectid.txt +++ b/docs/api/examples/delete_projectsprojectid.txt @@ -1,6 +1,6 @@ -curl -i -X DELETE 'http://localhost:8000/projects/{project_id}' +curl -i -X DELETE 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80' -DELETE /projects/{project_id} HTTP/1.1 +DELETE /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80 HTTP/1.1 diff --git a/docs/api/examples/delete_projectsprojectidiouvmsvmid.txt b/docs/api/examples/delete_projectsprojectidiouvmsvmid.txt index ae225fce..c47266ee 100644 --- a/docs/api/examples/delete_projectsprojectidiouvmsvmid.txt +++ b/docs/api/examples/delete_projectsprojectidiouvmsvmid.txt @@ -1,6 +1,6 @@ -curl -i -X DELETE 'http://localhost:8000/projects/{project_id}/iou/vms/{vm_id}' +curl -i -X DELETE 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/ac98c05c-dbf1-4157-8f4c-6a0319a0bcdc' -DELETE /projects/{project_id}/iou/vms/{vm_id} HTTP/1.1 +DELETE /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/ac98c05c-dbf1-4157-8f4c-6a0319a0bcdc HTTP/1.1 diff --git a/docs/api/examples/delete_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt b/docs/api/examples/delete_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt index f8aa407f..289cdd57 100644 --- a/docs/api/examples/delete_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt +++ b/docs/api/examples/delete_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -1,6 +1,6 @@ -curl -i -X DELETE 'http://localhost:8000/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio' +curl -i -X DELETE 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/acf7667f-91d4-4be5-9eec-f453783bb983/adapters/1/ports/0/nio' -DELETE /projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio HTTP/1.1 +DELETE /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/acf7667f-91d4-4be5-9eec-f453783bb983/adapters/1/ports/0/nio HTTP/1.1 diff --git a/docs/api/examples/delete_projectsprojectidqemuvmsvmid.txt b/docs/api/examples/delete_projectsprojectidqemuvmsvmid.txt index 8b47125e..aee9ddd3 100644 --- a/docs/api/examples/delete_projectsprojectidqemuvmsvmid.txt +++ b/docs/api/examples/delete_projectsprojectidqemuvmsvmid.txt @@ -1,6 +1,6 @@ -curl -i -X DELETE 'http://localhost:8000/projects/{project_id}/qemu/vms/{vm_id}' +curl -i -X DELETE 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/da298f63-4d5b-44a7-8672-ec6642009725' -DELETE /projects/{project_id}/qemu/vms/{vm_id} HTTP/1.1 +DELETE /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/da298f63-4d5b-44a7-8672-ec6642009725 HTTP/1.1 diff --git a/docs/api/examples/delete_projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.txt b/docs/api/examples/delete_projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.txt index ebd74cfd..2648effe 100644 --- a/docs/api/examples/delete_projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.txt +++ b/docs/api/examples/delete_projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -1,6 +1,6 @@ -curl -i -X DELETE 'http://localhost:8000/projects/{project_id}/qemu/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio' +curl -i -X DELETE 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/75fa07c2-fb3d-4a19-815d-2dee5aa5325c/adapters/1/ports/0/nio' -DELETE /projects/{project_id}/qemu/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio HTTP/1.1 +DELETE /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/75fa07c2-fb3d-4a19-815d-2dee5aa5325c/adapters/1/ports/0/nio HTTP/1.1 diff --git a/docs/api/examples/delete_projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.txt b/docs/api/examples/delete_projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.txt index d40be628..ecefe5ab 100644 --- a/docs/api/examples/delete_projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.txt +++ b/docs/api/examples/delete_projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -1,6 +1,6 @@ -curl -i -X DELETE 'http://localhost:8000/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio' +curl -i -X DELETE 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/c99f4293-b8b1-40a6-8535-014c4afc2fe7/adapters/0/ports/0/nio' -DELETE /projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio HTTP/1.1 +DELETE /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/c99f4293-b8b1-40a6-8535-014c4afc2fe7/adapters/0/ports/0/nio HTTP/1.1 diff --git a/docs/api/examples/delete_projectsprojectidvpcsvmsvmid.txt b/docs/api/examples/delete_projectsprojectidvpcsvmsvmid.txt index e9b9677c..44002739 100644 --- a/docs/api/examples/delete_projectsprojectidvpcsvmsvmid.txt +++ b/docs/api/examples/delete_projectsprojectidvpcsvmsvmid.txt @@ -1,6 +1,6 @@ -curl -i -X DELETE 'http://localhost:8000/projects/{project_id}/vpcs/vms/{vm_id}' +curl -i -X DELETE 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/879f1789-bde0-4e64-ac68-f61a9b114347' -DELETE /projects/{project_id}/vpcs/vms/{vm_id} HTTP/1.1 +DELETE /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/879f1789-bde0-4e64-ac68-f61a9b114347 HTTP/1.1 diff --git a/docs/api/examples/delete_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt b/docs/api/examples/delete_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt index 6842905a..edc3f792 100644 --- a/docs/api/examples/delete_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt +++ b/docs/api/examples/delete_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -1,6 +1,6 @@ -curl -i -X DELETE 'http://localhost:8000/projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio' +curl -i -X DELETE 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/5238c683-bb17-49f2-8796-a60668fc5955/adapters/0/ports/0/nio' -DELETE /projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio HTTP/1.1 +DELETE /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/5238c683-bb17-49f2-8796-a60668fc5955/adapters/0/ports/0/nio HTTP/1.1 diff --git a/docs/api/examples/get_projectsprojectid.txt b/docs/api/examples/get_projectsprojectid.txt index c1daf6eb..464d9f9e 100644 --- a/docs/api/examples/get_projectsprojectid.txt +++ b/docs/api/examples/get_projectsprojectid.txt @@ -1,6 +1,6 @@ -curl -i -X GET 'http://localhost:8000/projects/{project_id}' +curl -i -X GET 'http://localhost:8000/projects/00010203-0405-0607-0809-0a0b0c0d0e02' -GET /projects/{project_id} HTTP/1.1 +GET /projects/00010203-0405-0607-0809-0a0b0c0d0e02 HTTP/1.1 @@ -13,8 +13,8 @@ SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /v1/projects/{project_id} { - "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpqekqsxgq", - "path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpqekqsxgq/00010203-0405-0607-0809-0a0b0c0d0e02", + "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpju7ztx9a", + "path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpju7ztx9a/00010203-0405-0607-0809-0a0b0c0d0e02", "project_id": "00010203-0405-0607-0809-0a0b0c0d0e02", "temporary": false } diff --git a/docs/api/examples/get_projectsprojectidiouvmsvmid.txt b/docs/api/examples/get_projectsprojectidiouvmsvmid.txt index 5fd3cfcb..d85e9376 100644 --- a/docs/api/examples/get_projectsprojectidiouvmsvmid.txt +++ b/docs/api/examples/get_projectsprojectidiouvmsvmid.txt @@ -1,6 +1,6 @@ -curl -i -X GET 'http://localhost:8000/projects/{project_id}/iou/vms/{vm_id}' +curl -i -X GET 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/c5c0075d-0b10-4401-8bed-d9897814237c' -GET /projects/{project_id}/iou/vms/{vm_id} HTTP/1.1 +GET /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/c5c0075d-0b10-4401-8bed-d9897814237c HTTP/1.1 @@ -19,9 +19,9 @@ X-ROUTE: /v1/projects/{project_id}/iou/vms/{vm_id} "l1_keepalives": false, "name": "PC TEST 1", "nvram": 128, - "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3782/test_iou_get0/iou.bin", + "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3783/test_iou_get0/iou.bin", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "ram": 256, "serial_adapters": 2, - "vm_id": "b9022212-4b9e-48fd-b910-85f0d896f951" + "vm_id": "c5c0075d-0b10-4401-8bed-d9897814237c" } diff --git a/docs/api/examples/get_projectsprojectidiouvmsvmidinitialconfig.txt b/docs/api/examples/get_projectsprojectidiouvmsvmidinitialconfig.txt index 99bb7273..44f27589 100644 --- a/docs/api/examples/get_projectsprojectidiouvmsvmidinitialconfig.txt +++ b/docs/api/examples/get_projectsprojectidiouvmsvmidinitialconfig.txt @@ -1,6 +1,6 @@ -curl -i -X GET 'http://localhost:8000/projects/{project_id}/iou/vms/{vm_id}/initial_config' +curl -i -X GET 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/f7220f9c-3334-43e3-9ef4-37f09ba6fcab/initial_config' -GET /projects/{project_id}/iou/vms/{vm_id}/initial_config HTTP/1.1 +GET /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/f7220f9c-3334-43e3-9ef4-37f09ba6fcab/initial_config HTTP/1.1 diff --git a/docs/api/examples/get_projectsprojectidqemuvmsvmid.txt b/docs/api/examples/get_projectsprojectidqemuvmsvmid.txt index 274275c6..553d0eb6 100644 --- a/docs/api/examples/get_projectsprojectidqemuvmsvmid.txt +++ b/docs/api/examples/get_projectsprojectidqemuvmsvmid.txt @@ -1,6 +1,6 @@ -curl -i -X GET 'http://localhost:8000/projects/{project_id}/qemu/vms/{vm_id}' +curl -i -X GET 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/4532d770-23a0-4858-bbab-d8a8b3a17deb' -GET /projects/{project_id}/qemu/vms/{vm_id} HTTP/1.1 +GET /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/4532d770-23a0-4858-bbab-d8a8b3a17deb HTTP/1.1 @@ -28,7 +28,7 @@ X-ROUTE: /v1/projects/{project_id}/qemu/vms/{vm_id} "options": "", "process_priority": "low", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmprhap9vc_/qemu_x42", + "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmph25p8xei/qemu_x42", "ram": 256, - "vm_id": "5eee27c5-c590-4ddf-aa1f-783e15a3c41a" + "vm_id": "4532d770-23a0-4858-bbab-d8a8b3a17deb" } diff --git a/docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt b/docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt index 6bb301b8..e2d9cc0f 100644 --- a/docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt +++ b/docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt @@ -1,6 +1,6 @@ -curl -i -X GET 'http://localhost:8000/projects/{project_id}/virtualbox/vms/{vm_id}' +curl -i -X GET 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/f526503a-8d24-4513-a5e3-1ebf4159aa70' -GET /projects/{project_id}/virtualbox/vms/{vm_id} HTTP/1.1 +GET /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/f526503a-8d24-4513-a5e3-1ebf4159aa70 HTTP/1.1 @@ -21,6 +21,6 @@ X-ROUTE: /v1/projects/{project_id}/virtualbox/vms/{vm_id} "name": "VMTEST", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "use_any_adapter": false, - "vm_id": "4d4c9e18-0bad-4345-9338-17b98f2f6680", + "vm_id": "f526503a-8d24-4513-a5e3-1ebf4159aa70", "vmname": "VMTEST" } diff --git a/docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt b/docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt index f69edb84..c3530f18 100644 --- a/docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt +++ b/docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt @@ -1,6 +1,6 @@ -curl -i -X GET 'http://localhost:8000/projects/{project_id}/vpcs/vms/{vm_id}' +curl -i -X GET 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/6f3c227f-86f5-4f54-bc4b-74597744b904' -GET /projects/{project_id}/vpcs/vms/{vm_id} HTTP/1.1 +GET /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/6f3c227f-86f5-4f54-bc4b-74597744b904 HTTP/1.1 @@ -18,5 +18,5 @@ X-ROUTE: /v1/projects/{project_id}/vpcs/vms/{vm_id} "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "startup_script": null, "startup_script_path": null, - "vm_id": "ca159ab0-42d5-4c9a-bfb0-c2290ac90556" + "vm_id": "6f3c227f-86f5-4f54-bc4b-74597744b904" } diff --git a/docs/api/examples/post_projects.txt b/docs/api/examples/post_projects.txt index cc85ac3b..416dbed2 100644 --- a/docs/api/examples/post_projects.txt +++ b/docs/api/examples/post_projects.txt @@ -13,8 +13,8 @@ SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /v1/projects { - "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpgrmlhkz5", - "path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpgrmlhkz5/8a563c49-902c-4ee8-8a3c-d188e5832741", - "project_id": "8a563c49-902c-4ee8-8a3c-d188e5832741", + "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmp4s49s4hy", + "path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmp4s49s4hy/119348c0-fa80-4386-bf1e-b00755c9c6b4", + "project_id": "119348c0-fa80-4386-bf1e-b00755c9c6b4", "temporary": false } diff --git a/docs/api/examples/post_projectsprojectidclose.txt b/docs/api/examples/post_projectsprojectidclose.txt index bcc429c9..b42beff6 100644 --- a/docs/api/examples/post_projectsprojectidclose.txt +++ b/docs/api/examples/post_projectsprojectidclose.txt @@ -1,6 +1,6 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/close' -d '{}' +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/close' -d '{}' -POST /projects/{project_id}/close HTTP/1.1 +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/close HTTP/1.1 {} diff --git a/docs/api/examples/post_projectsprojectidcommit.txt b/docs/api/examples/post_projectsprojectidcommit.txt index 0b36f05d..261c616f 100644 --- a/docs/api/examples/post_projectsprojectidcommit.txt +++ b/docs/api/examples/post_projectsprojectidcommit.txt @@ -1,6 +1,6 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/commit' -d '{}' +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/commit' -d '{}' -POST /projects/{project_id}/commit HTTP/1.1 +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/commit HTTP/1.1 {} diff --git a/docs/api/examples/post_projectsprojectidiouvms.txt b/docs/api/examples/post_projectsprojectidiouvms.txt index fda39294..2e254f47 100644 --- a/docs/api/examples/post_projectsprojectidiouvms.txt +++ b/docs/api/examples/post_projectsprojectidiouvms.txt @@ -1,13 +1,13 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/iou/vms' -d '{"ethernet_adapters": 0, "initial_config_content": "hostname test", "l1_keepalives": true, "name": "PC TEST 1", "nvram": 512, "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3782/test_iou_create_with_params0/iou.bin", "ram": 1024, "serial_adapters": 4}' +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms' -d '{"ethernet_adapters": 0, "initial_config_content": "hostname test", "l1_keepalives": true, "name": "PC TEST 1", "nvram": 512, "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3783/test_iou_create_with_params0/iou.bin", "ram": 1024, "serial_adapters": 4}' -POST /projects/{project_id}/iou/vms HTTP/1.1 +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms HTTP/1.1 { "ethernet_adapters": 0, "initial_config_content": "hostname test", "l1_keepalives": true, "name": "PC TEST 1", "nvram": 512, - "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3782/test_iou_create_with_params0/iou.bin", + "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3783/test_iou_create_with_params0/iou.bin", "ram": 1024, "serial_adapters": 4 } @@ -28,9 +28,9 @@ X-ROUTE: /v1/projects/{project_id}/iou/vms "l1_keepalives": true, "name": "PC TEST 1", "nvram": 512, - "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3782/test_iou_create_with_params0/iou.bin", + "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3783/test_iou_create_with_params0/iou.bin", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "ram": 1024, "serial_adapters": 4, - "vm_id": "d6832edf-97ad-45a2-b588-c64d5c3f5dec" + "vm_id": "3c02aa01-46d2-4a62-97d6-dc5829afdf39" } diff --git a/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt b/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt index 5940e201..b8cc08d9 100644 --- a/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt +++ b/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -1,6 +1,6 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio' -d '{"ethernet_device": "eth0", "type": "nio_generic_ethernet"}' +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/7f758868-46e1-4ef5-accc-f8e939a12471/adapters/1/ports/0/nio' -d '{"ethernet_device": "eth0", "type": "nio_generic_ethernet"}' -POST /projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio HTTP/1.1 +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/7f758868-46e1-4ef5-accc-f8e939a12471/adapters/1/ports/0/nio HTTP/1.1 { "ethernet_device": "eth0", "type": "nio_generic_ethernet" diff --git a/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.txt b/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.txt index 10093b80..417bfb5e 100644 --- a/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.txt +++ b/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.txt @@ -1,6 +1,6 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/start_capture' -d '{"capture_file_name": "test.pcap", "data_link_type": "DLT_EN10MB"}' +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/c40daea2-2b38-4d14-a872-13c5f991cbc3/adapters/0/ports/0/start_capture' -d '{"capture_file_name": "test.pcap", "data_link_type": "DLT_EN10MB"}' -POST /projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/start_capture HTTP/1.1 +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/c40daea2-2b38-4d14-a872-13c5f991cbc3/adapters/0/ports/0/start_capture HTTP/1.1 { "capture_file_name": "test.pcap", "data_link_type": "DLT_EN10MB" @@ -16,5 +16,5 @@ SERVER: Python/3.4 GNS3/1.3.dev1 X-ROUTE: /v1/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/start_capture { - "pcap_file_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpina5gsqc/a1e920ca-338a-4e9f-b363-aa607b09dd80/project-files/captures/test.pcap" + "pcap_file_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpn_q77wz2/a1e920ca-338a-4e9f-b363-aa607b09dd80/project-files/captures/test.pcap" } diff --git a/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.txt b/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.txt index 3b79347f..c1987d70 100644 --- a/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.txt +++ b/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.txt @@ -1,6 +1,6 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/stop_capture' -d '{}' +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/550b8f35-e258-4354-a74c-cd35a48c08ed/adapters/0/ports/0/stop_capture' -d '{}' -POST /projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/stop_capture HTTP/1.1 +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/550b8f35-e258-4354-a74c-cd35a48c08ed/adapters/0/ports/0/stop_capture HTTP/1.1 {} diff --git a/docs/api/examples/post_projectsprojectidiouvmsvmidreload.txt b/docs/api/examples/post_projectsprojectidiouvmsvmidreload.txt index f464b22e..05ab639e 100644 --- a/docs/api/examples/post_projectsprojectidiouvmsvmidreload.txt +++ b/docs/api/examples/post_projectsprojectidiouvmsvmidreload.txt @@ -1,6 +1,6 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/iou/vms/{vm_id}/reload' -d '{}' +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/d1b38099-46a3-4405-a354-85faeb76bd0e/reload' -d '{}' -POST /projects/{project_id}/iou/vms/{vm_id}/reload HTTP/1.1 +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/d1b38099-46a3-4405-a354-85faeb76bd0e/reload HTTP/1.1 {} diff --git a/docs/api/examples/post_projectsprojectidiouvmsvmidstart.txt b/docs/api/examples/post_projectsprojectidiouvmsvmidstart.txt index a82a5504..d10d8c5d 100644 --- a/docs/api/examples/post_projectsprojectidiouvmsvmidstart.txt +++ b/docs/api/examples/post_projectsprojectidiouvmsvmidstart.txt @@ -1,6 +1,6 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/iou/vms/{vm_id}/start' -d '{}' +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/fc1ea907-eb0b-4857-9ad2-759f780afdb4/start' -d '{}' -POST /projects/{project_id}/iou/vms/{vm_id}/start HTTP/1.1 +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/fc1ea907-eb0b-4857-9ad2-759f780afdb4/start HTTP/1.1 {} diff --git a/docs/api/examples/post_projectsprojectidiouvmsvmidstop.txt b/docs/api/examples/post_projectsprojectidiouvmsvmidstop.txt index 281869a6..d97a0400 100644 --- a/docs/api/examples/post_projectsprojectidiouvmsvmidstop.txt +++ b/docs/api/examples/post_projectsprojectidiouvmsvmidstop.txt @@ -1,6 +1,6 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/iou/vms/{vm_id}/stop' -d '{}' +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/0228866f-a286-44e2-9688-a8cab4e75cc3/stop' -d '{}' -POST /projects/{project_id}/iou/vms/{vm_id}/stop HTTP/1.1 +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/0228866f-a286-44e2-9688-a8cab4e75cc3/stop HTTP/1.1 {} diff --git a/docs/api/examples/post_projectsprojectidqemuvms.txt b/docs/api/examples/post_projectsprojectidqemuvms.txt index 85123e88..b0b4dc89 100644 --- a/docs/api/examples/post_projectsprojectidqemuvms.txt +++ b/docs/api/examples/post_projectsprojectidqemuvms.txt @@ -1,10 +1,10 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/qemu/vms' -d '{"hda_disk_image": "hda", "name": "PC TEST 1", "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmprhap9vc_/qemu_x42", "ram": 1024}' +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms' -d '{"hda_disk_image": "hda", "name": "PC TEST 1", "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmph25p8xei/qemu_x42", "ram": 1024}' -POST /projects/{project_id}/qemu/vms HTTP/1.1 +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms HTTP/1.1 { "hda_disk_image": "hda", "name": "PC TEST 1", - "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmprhap9vc_/qemu_x42", + "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmph25p8xei/qemu_x42", "ram": 1024 } @@ -33,7 +33,7 @@ X-ROUTE: /v1/projects/{project_id}/qemu/vms "options": "", "process_priority": "low", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmprhap9vc_/qemu_x42", + "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmph25p8xei/qemu_x42", "ram": 1024, - "vm_id": "4c3596aa-e44c-4129-94e4-45dd93ef7e6b" + "vm_id": "4e47d2c8-d591-4508-9c3e-00e366f7c22d" } diff --git a/docs/api/examples/post_projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.txt b/docs/api/examples/post_projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.txt index 4419ebca..a12017e1 100644 --- a/docs/api/examples/post_projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.txt +++ b/docs/api/examples/post_projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -1,6 +1,6 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/qemu/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio' -d '{"ethernet_device": "eth0", "type": "nio_generic_ethernet"}' +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/e4ac8960-f709-4e28-bc6c-3b2593b622e9/adapters/1/ports/0/nio' -d '{"ethernet_device": "eth0", "type": "nio_generic_ethernet"}' -POST /projects/{project_id}/qemu/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio HTTP/1.1 +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/e4ac8960-f709-4e28-bc6c-3b2593b622e9/adapters/1/ports/0/nio HTTP/1.1 { "ethernet_device": "eth0", "type": "nio_generic_ethernet" diff --git a/docs/api/examples/post_projectsprojectidqemuvmsvmidreload.txt b/docs/api/examples/post_projectsprojectidqemuvmsvmidreload.txt index 8c4e6d14..0082474e 100644 --- a/docs/api/examples/post_projectsprojectidqemuvmsvmidreload.txt +++ b/docs/api/examples/post_projectsprojectidqemuvmsvmidreload.txt @@ -1,6 +1,6 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/qemu/vms/{vm_id}/reload' -d '{}' +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/2a143130-2445-4a4b-9ba2-f49071eed5f4/reload' -d '{}' -POST /projects/{project_id}/qemu/vms/{vm_id}/reload HTTP/1.1 +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/2a143130-2445-4a4b-9ba2-f49071eed5f4/reload HTTP/1.1 {} diff --git a/docs/api/examples/post_projectsprojectidqemuvmsvmidresume.txt b/docs/api/examples/post_projectsprojectidqemuvmsvmidresume.txt index 86c13afa..7a30d607 100644 --- a/docs/api/examples/post_projectsprojectidqemuvmsvmidresume.txt +++ b/docs/api/examples/post_projectsprojectidqemuvmsvmidresume.txt @@ -1,6 +1,6 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/qemu/vms/{vm_id}/resume' -d '{}' +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/9272bffc-7a73-449a-aa71-96c48b1e5a3d/resume' -d '{}' -POST /projects/{project_id}/qemu/vms/{vm_id}/resume HTTP/1.1 +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/9272bffc-7a73-449a-aa71-96c48b1e5a3d/resume HTTP/1.1 {} diff --git a/docs/api/examples/post_projectsprojectidqemuvmsvmidstart.txt b/docs/api/examples/post_projectsprojectidqemuvmsvmidstart.txt index c2365ff2..9071b899 100644 --- a/docs/api/examples/post_projectsprojectidqemuvmsvmidstart.txt +++ b/docs/api/examples/post_projectsprojectidqemuvmsvmidstart.txt @@ -1,6 +1,6 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/qemu/vms/{vm_id}/start' -d '{}' +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/e4f04682-d749-49c8-be3e-8c4752483cc1/start' -d '{}' -POST /projects/{project_id}/qemu/vms/{vm_id}/start HTTP/1.1 +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/e4f04682-d749-49c8-be3e-8c4752483cc1/start HTTP/1.1 {} diff --git a/docs/api/examples/post_projectsprojectidqemuvmsvmidstop.txt b/docs/api/examples/post_projectsprojectidqemuvmsvmidstop.txt index 4c85508e..a94f31e4 100644 --- a/docs/api/examples/post_projectsprojectidqemuvmsvmidstop.txt +++ b/docs/api/examples/post_projectsprojectidqemuvmsvmidstop.txt @@ -1,6 +1,6 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/qemu/vms/{vm_id}/stop' -d '{}' +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/1cb28121-2b13-4c53-a0fc-ab8c6d028d0f/stop' -d '{}' -POST /projects/{project_id}/qemu/vms/{vm_id}/stop HTTP/1.1 +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/1cb28121-2b13-4c53-a0fc-ab8c6d028d0f/stop HTTP/1.1 {} diff --git a/docs/api/examples/post_projectsprojectidqemuvmsvmidsuspend.txt b/docs/api/examples/post_projectsprojectidqemuvmsvmidsuspend.txt index 5a412c66..ba5dd175 100644 --- a/docs/api/examples/post_projectsprojectidqemuvmsvmidsuspend.txt +++ b/docs/api/examples/post_projectsprojectidqemuvmsvmidsuspend.txt @@ -1,6 +1,6 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/qemu/vms/{vm_id}/suspend' -d '{}' +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/777077ba-025a-49dc-9773-cedc425bdb6d/suspend' -d '{}' -POST /projects/{project_id}/qemu/vms/{vm_id}/suspend HTTP/1.1 +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/777077ba-025a-49dc-9773-cedc425bdb6d/suspend HTTP/1.1 {} diff --git a/docs/api/examples/post_projectsprojectidvirtualboxvms.txt b/docs/api/examples/post_projectsprojectidvirtualboxvms.txt index 0090a3d7..c770d6ff 100644 --- a/docs/api/examples/post_projectsprojectidvirtualboxvms.txt +++ b/docs/api/examples/post_projectsprojectidvirtualboxvms.txt @@ -1,6 +1,6 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/virtualbox/vms' -d '{"linked_clone": false, "name": "VM1", "vmname": "VM1"}' +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms' -d '{"linked_clone": false, "name": "VM1", "vmname": "VM1"}' -POST /projects/{project_id}/virtualbox/vms HTTP/1.1 +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms HTTP/1.1 { "linked_clone": false, "name": "VM1", @@ -25,6 +25,6 @@ X-ROUTE: /v1/projects/{project_id}/virtualbox/vms "name": "VM1", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "use_any_adapter": false, - "vm_id": "d162a4e3-f28b-48db-a9f4-1c87db4ed0d2", + "vm_id": "1404a6c5-c0f5-4bc6-bb94-477d7e631781", "vmname": "VM1" } diff --git a/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.txt b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.txt index 25100dbb..1d5dbc65 100644 --- a/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.txt +++ b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -1,6 +1,6 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio' -d '{"lport": 4242, "rhost": "127.0.0.1", "rport": 4343, "type": "nio_udp"}' +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/bd0d175c-2055-4315-86d1-07494696d42e/adapters/0/ports/0/nio' -d '{"lport": 4242, "rhost": "127.0.0.1", "rport": 4343, "type": "nio_udp"}' -POST /projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio HTTP/1.1 +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/bd0d175c-2055-4315-86d1-07494696d42e/adapters/0/ports/0/nio HTTP/1.1 { "lport": 4242, "rhost": "127.0.0.1", diff --git a/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidreload.txt b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidreload.txt index 50e6dc77..df9a754b 100644 --- a/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidreload.txt +++ b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidreload.txt @@ -1,6 +1,6 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/virtualbox/vms/{vm_id}/reload' -d '{}' +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/4c9fdef4-8990-44a1-8a98-1b7c510821ee/reload' -d '{}' -POST /projects/{project_id}/virtualbox/vms/{vm_id}/reload HTTP/1.1 +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/4c9fdef4-8990-44a1-8a98-1b7c510821ee/reload HTTP/1.1 {} diff --git a/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidresume.txt b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidresume.txt index 7d27abb9..40cb8a63 100644 --- a/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidresume.txt +++ b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidresume.txt @@ -1,6 +1,6 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/virtualbox/vms/{vm_id}/resume' -d '{}' +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/f6487814-a3a9-4e82-867c-83662c7bed48/resume' -d '{}' -POST /projects/{project_id}/virtualbox/vms/{vm_id}/resume HTTP/1.1 +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/f6487814-a3a9-4e82-867c-83662c7bed48/resume HTTP/1.1 {} diff --git a/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidstart.txt b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidstart.txt index d533f5f4..77ab559b 100644 --- a/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidstart.txt +++ b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidstart.txt @@ -1,6 +1,6 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/virtualbox/vms/{vm_id}/start' -d '{}' +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/bedd61d4-b264-47d9-b290-920d5ba70f6d/start' -d '{}' -POST /projects/{project_id}/virtualbox/vms/{vm_id}/start HTTP/1.1 +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/bedd61d4-b264-47d9-b290-920d5ba70f6d/start HTTP/1.1 {} diff --git a/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidstop.txt b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidstop.txt index 500f5c02..fb421804 100644 --- a/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidstop.txt +++ b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidstop.txt @@ -1,6 +1,6 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/virtualbox/vms/{vm_id}/stop' -d '{}' +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/a1960146-9be9-4f54-b594-67a6ab40f436/stop' -d '{}' -POST /projects/{project_id}/virtualbox/vms/{vm_id}/stop HTTP/1.1 +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/a1960146-9be9-4f54-b594-67a6ab40f436/stop HTTP/1.1 {} diff --git a/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidsuspend.txt b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidsuspend.txt index 77945fc1..8b89fcd4 100644 --- a/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidsuspend.txt +++ b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidsuspend.txt @@ -1,6 +1,6 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/virtualbox/vms/{vm_id}/suspend' -d '{}' +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/4d16597f-fa75-430a-9b94-faf7f93bb0a3/suspend' -d '{}' -POST /projects/{project_id}/virtualbox/vms/{vm_id}/suspend HTTP/1.1 +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/4d16597f-fa75-430a-9b94-faf7f93bb0a3/suspend HTTP/1.1 {} diff --git a/docs/api/examples/post_projectsprojectidvpcsvms.txt b/docs/api/examples/post_projectsprojectidvpcsvms.txt index 6598d953..f01fddfa 100644 --- a/docs/api/examples/post_projectsprojectidvpcsvms.txt +++ b/docs/api/examples/post_projectsprojectidvpcsvms.txt @@ -1,6 +1,6 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/vpcs/vms' -d '{"name": "PC TEST 1"}' +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms' -d '{"name": "PC TEST 1"}' -POST /projects/{project_id}/vpcs/vms HTTP/1.1 +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms HTTP/1.1 { "name": "PC TEST 1" } @@ -20,5 +20,5 @@ X-ROUTE: /v1/projects/{project_id}/vpcs/vms "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "startup_script": null, "startup_script_path": null, - "vm_id": "4bec08c2-b54b-4040-90a3-2add2fd82f32" + "vm_id": "b0d1df2e-ebd1-4783-bb62-871e24f01543" } diff --git a/docs/api/examples/post_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt b/docs/api/examples/post_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt index e55c4ace..4ba387e6 100644 --- a/docs/api/examples/post_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt +++ b/docs/api/examples/post_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -1,6 +1,6 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio' -d '{"lport": 4242, "rhost": "127.0.0.1", "rport": 4343, "type": "nio_udp"}' +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/385d80e1-193c-4b38-bf55-5185ffd6b473/adapters/0/ports/0/nio' -d '{"lport": 4242, "rhost": "127.0.0.1", "rport": 4343, "type": "nio_udp"}' -POST /projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio HTTP/1.1 +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/385d80e1-193c-4b38-bf55-5185ffd6b473/adapters/0/ports/0/nio HTTP/1.1 { "lport": 4242, "rhost": "127.0.0.1", diff --git a/docs/api/examples/post_projectsprojectidvpcsvmsvmidreload.txt b/docs/api/examples/post_projectsprojectidvpcsvmsvmidreload.txt index ad8d4282..5658f3cd 100644 --- a/docs/api/examples/post_projectsprojectidvpcsvmsvmidreload.txt +++ b/docs/api/examples/post_projectsprojectidvpcsvmsvmidreload.txt @@ -1,6 +1,6 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/vpcs/vms/{vm_id}/reload' -d '{}' +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/b12c5612-147d-4229-86ca-103174ba8fd1/reload' -d '{}' -POST /projects/{project_id}/vpcs/vms/{vm_id}/reload HTTP/1.1 +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/b12c5612-147d-4229-86ca-103174ba8fd1/reload HTTP/1.1 {} diff --git a/docs/api/examples/post_projectsprojectidvpcsvmsvmidstart.txt b/docs/api/examples/post_projectsprojectidvpcsvmsvmidstart.txt index cd597019..4b903863 100644 --- a/docs/api/examples/post_projectsprojectidvpcsvmsvmidstart.txt +++ b/docs/api/examples/post_projectsprojectidvpcsvmsvmidstart.txt @@ -1,6 +1,6 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/vpcs/vms/{vm_id}/start' -d '{}' +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/01037fd2-9533-42db-bd19-df5b21e47fcf/start' -d '{}' -POST /projects/{project_id}/vpcs/vms/{vm_id}/start HTTP/1.1 +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/01037fd2-9533-42db-bd19-df5b21e47fcf/start HTTP/1.1 {} diff --git a/docs/api/examples/post_projectsprojectidvpcsvmsvmidstop.txt b/docs/api/examples/post_projectsprojectidvpcsvmsvmidstop.txt index 8d4f0127..01898946 100644 --- a/docs/api/examples/post_projectsprojectidvpcsvmsvmidstop.txt +++ b/docs/api/examples/post_projectsprojectidvpcsvmsvmidstop.txt @@ -1,6 +1,6 @@ -curl -i -X POST 'http://localhost:8000/projects/{project_id}/vpcs/vms/{vm_id}/stop' -d '{}' +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/20eefc85-f1b6-4e36-9601-cd0dc91faaa0/stop' -d '{}' -POST /projects/{project_id}/vpcs/vms/{vm_id}/stop HTTP/1.1 +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/20eefc85-f1b6-4e36-9601-cd0dc91faaa0/stop HTTP/1.1 {} diff --git a/docs/api/examples/put_projectsprojectid.txt b/docs/api/examples/put_projectsprojectid.txt index 8f5ee665..0e276071 100644 --- a/docs/api/examples/put_projectsprojectid.txt +++ b/docs/api/examples/put_projectsprojectid.txt @@ -1,8 +1,8 @@ -curl -i -X PUT 'http://localhost:8000/projects/{project_id}' -d '{"path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3782/test_update_path_project_non_l0"}' +curl -i -X PUT 'http://localhost:8000/projects/ef58b29e-59df-42c0-9492-5a766a13cb62' -d '{"path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3783/test_update_path_project_non_l0"}' -PUT /projects/{project_id} HTTP/1.1 +PUT /projects/ef58b29e-59df-42c0-9492-5a766a13cb62 HTTP/1.1 { - "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3782/test_update_path_project_non_l0" + "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3783/test_update_path_project_non_l0" } diff --git a/docs/api/examples/put_projectsprojectidiouvmsvmid.txt b/docs/api/examples/put_projectsprojectidiouvmsvmid.txt index b37461ab..810284c6 100644 --- a/docs/api/examples/put_projectsprojectidiouvmsvmid.txt +++ b/docs/api/examples/put_projectsprojectidiouvmsvmid.txt @@ -1,6 +1,6 @@ -curl -i -X PUT 'http://localhost:8000/projects/{project_id}/iou/vms/{vm_id}' -d '{"console": 2001, "ethernet_adapters": 4, "initial_config_content": "hostname test", "l1_keepalives": true, "name": "test", "nvram": 2048, "ram": 512, "serial_adapters": 0}' +curl -i -X PUT 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/324dab07-d86c-4a3b-8390-7a8e9a506006' -d '{"console": 2001, "ethernet_adapters": 4, "initial_config_content": "hostname test", "l1_keepalives": true, "name": "test", "nvram": 2048, "ram": 512, "serial_adapters": 0}' -PUT /projects/{project_id}/iou/vms/{vm_id} HTTP/1.1 +PUT /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/324dab07-d86c-4a3b-8390-7a8e9a506006 HTTP/1.1 { "console": 2001, "ethernet_adapters": 4, @@ -28,9 +28,9 @@ X-ROUTE: /v1/projects/{project_id}/iou/vms/{vm_id} "l1_keepalives": true, "name": "test", "nvram": 2048, - "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3782/test_iou_update0/iou.bin", + "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3783/test_iou_update0/iou.bin", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "ram": 512, "serial_adapters": 0, - "vm_id": "69ad7d57-d1ec-447e-b8da-7b2f888a85e5" + "vm_id": "324dab07-d86c-4a3b-8390-7a8e9a506006" } diff --git a/docs/api/examples/put_projectsprojectidqemuvmsvmid.txt b/docs/api/examples/put_projectsprojectidqemuvmsvmid.txt index a6ed21bf..292fd2b1 100644 --- a/docs/api/examples/put_projectsprojectidqemuvmsvmid.txt +++ b/docs/api/examples/put_projectsprojectidqemuvmsvmid.txt @@ -1,6 +1,6 @@ -curl -i -X PUT 'http://localhost:8000/projects/{project_id}/qemu/vms/{vm_id}' -d '{"console": 2002, "hdb_disk_image": "hdb", "name": "test", "ram": 1024}' +curl -i -X PUT 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/a68a00de-a264-4cc0-821c-301af5059ea4' -d '{"console": 2002, "hdb_disk_image": "hdb", "name": "test", "ram": 1024}' -PUT /projects/{project_id}/qemu/vms/{vm_id} HTTP/1.1 +PUT /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/a68a00de-a264-4cc0-821c-301af5059ea4 HTTP/1.1 { "console": 2002, "hdb_disk_image": "hdb", @@ -33,7 +33,7 @@ X-ROUTE: /v1/projects/{project_id}/qemu/vms/{vm_id} "options": "", "process_priority": "low", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmprhap9vc_/qemu_x42", + "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmph25p8xei/qemu_x42", "ram": 1024, - "vm_id": "385c7895-f085-4682-b31a-a6a63930c5c4" + "vm_id": "a68a00de-a264-4cc0-821c-301af5059ea4" } diff --git a/docs/api/examples/put_projectsprojectidvirtualboxvmsvmid.txt b/docs/api/examples/put_projectsprojectidvirtualboxvmsvmid.txt index e4f99347..480c4cf1 100644 --- a/docs/api/examples/put_projectsprojectidvirtualboxvmsvmid.txt +++ b/docs/api/examples/put_projectsprojectidvirtualboxvmsvmid.txt @@ -1,6 +1,6 @@ -curl -i -X PUT 'http://localhost:8000/projects/{project_id}/virtualbox/vms/{vm_id}' -d '{"console": 2010, "name": "test"}' +curl -i -X PUT 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/4a6375b9-14b8-406c-ae8e-df6d242088f9' -d '{"console": 2010, "name": "test"}' -PUT /projects/{project_id}/virtualbox/vms/{vm_id} HTTP/1.1 +PUT /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/4a6375b9-14b8-406c-ae8e-df6d242088f9 HTTP/1.1 { "console": 2010, "name": "test" @@ -24,6 +24,6 @@ X-ROUTE: /v1/projects/{project_id}/virtualbox/vms/{vm_id} "name": "test", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "use_any_adapter": false, - "vm_id": "15c34b6c-63bb-48a6-b4b0-e772fdb74925", + "vm_id": "4a6375b9-14b8-406c-ae8e-df6d242088f9", "vmname": "VMTEST" } diff --git a/docs/api/examples/put_projectsprojectidvpcsvmsvmid.txt b/docs/api/examples/put_projectsprojectidvpcsvmsvmid.txt index 23adddbe..b90c37ca 100644 --- a/docs/api/examples/put_projectsprojectidvpcsvmsvmid.txt +++ b/docs/api/examples/put_projectsprojectidvpcsvmsvmid.txt @@ -1,6 +1,6 @@ -curl -i -X PUT 'http://localhost:8000/projects/{project_id}/vpcs/vms/{vm_id}' -d '{"console": 2011, "name": "test", "startup_script": "ip 192.168.1.1"}' +curl -i -X PUT 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/39951883-cbfb-4aff-9cbc-06895749e571' -d '{"console": 2011, "name": "test", "startup_script": "ip 192.168.1.1"}' -PUT /projects/{project_id}/vpcs/vms/{vm_id} HTTP/1.1 +PUT /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/39951883-cbfb-4aff-9cbc-06895749e571 HTTP/1.1 { "console": 2011, "name": "test", @@ -22,5 +22,5 @@ X-ROUTE: /v1/projects/{project_id}/vpcs/vms/{vm_id} "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "startup_script": "ip 192.168.1.1", "startup_script_path": "startup.vpc", - "vm_id": "a239c325-d437-4209-a583-9eee28905802" + "vm_id": "39951883-cbfb-4aff-9cbc-06895749e571" } diff --git a/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst index 0533ad08..706b5c06 100644 --- a/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst +++ b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst @@ -10,8 +10,8 @@ Add a NIO to a Dynamips device instance Parameters ********** - **project_id**: UUID for the project -- **port_number**: Port on the device - **device_id**: UUID for the instance +- **port_number**: Port on the device Response status codes ********************** @@ -129,8 +129,8 @@ Remove a NIO from a Dynamips device instance Parameters ********** - **project_id**: UUID for the project -- **port_number**: Port on the device - **device_id**: UUID for the instance +- **port_number**: Port on the device Response status codes ********************** diff --git a/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst index 117cd928..19852f49 100644 --- a/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst +++ b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst @@ -10,8 +10,8 @@ Start a packet capture on a Dynamips device instance Parameters ********** - **project_id**: UUID for the project -- **port_number**: Port on the device - **device_id**: UUID for the instance +- **port_number**: Port on the device Response status codes ********************** diff --git a/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst index 9674ef65..cc312e43 100644 --- a/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst +++ b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst @@ -10,8 +10,8 @@ Stop a packet capture on a Dynamips device instance Parameters ********** - **project_id**: UUID for the project -- **port_number**: Port on the device - **device_id**: UUID for the instance +- **port_number**: Port on the device Response status codes ********************** diff --git a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmid.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmid.rst index c8e677e4..11b3c065 100644 --- a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmid.rst +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmid.rst @@ -9,8 +9,8 @@ Get a Dynamips VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** @@ -75,8 +75,8 @@ Update a Dynamips VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** @@ -191,8 +191,8 @@ Delete a Dynamips VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst index 84b37b86..25d0a246 100644 --- a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -9,10 +9,10 @@ Add a NIO to a Dynamips VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project -- **port_number**: Port on the adapter +- **vm_id**: UUID for the instance - **adapter_number**: Adapter where the nio should be added +- **port_number**: Port on the adapter Response status codes ********************** @@ -27,10 +27,10 @@ Remove a NIO from a Dynamips VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project -- **port_number**: Port on the adapter +- **vm_id**: UUID for the instance - **adapter_number**: Adapter from where the nio should be removed +- **port_number**: Port on the adapter Response status codes ********************** diff --git a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst index 27ac8daf..0d54a8c6 100644 --- a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst @@ -9,10 +9,10 @@ Start a packet capture on a Dynamips VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project -- **port_number**: Port on the adapter +- **vm_id**: UUID for the instance - **adapter_number**: Adapter to start a packet capture +- **port_number**: Port on the adapter Response status codes ********************** diff --git a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst index 08c3cf32..f89a083c 100644 --- a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst @@ -9,10 +9,10 @@ Stop a packet capture on a Dynamips VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project -- **port_number**: Port on the adapter (always 0) +- **vm_id**: UUID for the instance - **adapter_number**: Adapter to stop a packet capture +- **port_number**: Port on the adapter (always 0) Response status codes ********************** diff --git a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidreload.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidreload.rst index d7d7df55..23bb67f3 100644 --- a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidreload.rst +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidreload.rst @@ -9,8 +9,8 @@ Reload a Dynamips VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidresume.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidresume.rst index fcd48ab5..73ce9d01 100644 --- a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidresume.rst +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidresume.rst @@ -9,8 +9,8 @@ Resume a suspended Dynamips VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidstart.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidstart.rst index 2dbd25b8..24c3d2af 100644 --- a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidstart.rst +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidstart.rst @@ -9,8 +9,8 @@ Start a Dynamips VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidstop.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidstop.rst index ff62c2c9..fca471b6 100644 --- a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidstop.rst +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidstop.rst @@ -9,8 +9,8 @@ Stop a Dynamips VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidsuspend.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidsuspend.rst index b6fb8a13..54394e01 100644 --- a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidsuspend.rst +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidsuspend.rst @@ -9,8 +9,8 @@ Suspend a Dynamips VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/v1/qemu/projectsprojectidqemuvmsvmid.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmid.rst index dad9bd01..0270bbd9 100644 --- a/docs/api/v1/qemu/projectsprojectidqemuvmsvmid.rst +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmid.rst @@ -9,8 +9,8 @@ Get a Qemu.instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** @@ -57,8 +57,8 @@ Update a Qemu.instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** @@ -130,8 +130,8 @@ Delete a Qemu.instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst index fb26caca..e1c94c53 100644 --- a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -9,10 +9,10 @@ Add a NIO to a Qemu.instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project -- **port_number**: Port where the nio should be added +- **vm_id**: UUID for the instance - **adapter_number**: Network adapter where the nio is located +- **port_number**: Port where the nio should be added Response status codes ********************** @@ -33,10 +33,10 @@ Remove a NIO from a Qemu.instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project -- **port_number**: Port from where the nio should be removed +- **vm_id**: UUID for the instance - **adapter_number**: Network adapter where the nio is located +- **port_number**: Port from where the nio should be removed Response status codes ********************** diff --git a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidreload.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidreload.rst index a33af01f..5b29cec3 100644 --- a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidreload.rst +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidreload.rst @@ -9,8 +9,8 @@ Reload a Qemu.instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidresume.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidresume.rst index c3d3b967..59b5c1f7 100644 --- a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidresume.rst +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidresume.rst @@ -9,8 +9,8 @@ Resume a Qemu.instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstart.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstart.rst index 063832b1..f5306892 100644 --- a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstart.rst +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstart.rst @@ -9,8 +9,8 @@ Start a Qemu.instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstop.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstop.rst index bb6af81f..d5c41c96 100644 --- a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstop.rst +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstop.rst @@ -9,8 +9,8 @@ Stop a Qemu.instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidsuspend.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidsuspend.rst index 36f0e34d..63e8c94a 100644 --- a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidsuspend.rst +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidsuspend.rst @@ -9,8 +9,8 @@ Suspend a Qemu.instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmid.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmid.rst index 792e2e61..8ec38157 100644 --- a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmid.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmid.rst @@ -9,8 +9,8 @@ Get a VirtualBox VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** @@ -49,8 +49,8 @@ Update a VirtualBox VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** @@ -106,8 +106,8 @@ Delete a VirtualBox VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst index 6ba2ff9b..0ba49c65 100644 --- a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -9,10 +9,10 @@ Add a NIO to a VirtualBox VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project -- **port_number**: Port on the adapter (always 0) +- **vm_id**: UUID for the instance - **adapter_number**: Adapter where the nio should be added +- **port_number**: Port on the adapter (always 0) Response status codes ********************** @@ -33,10 +33,10 @@ Remove a NIO from a VirtualBox VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project -- **port_number**: Port on the adapter (always) +- **vm_id**: UUID for the instance - **adapter_number**: Adapter from where the nio should be removed +- **port_number**: Port on the adapter (always) Response status codes ********************** diff --git a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst index 41d3ee78..402ccc5e 100644 --- a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst @@ -9,10 +9,10 @@ Start a packet capture on a VirtualBox VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project -- **port_number**: Port on the adapter (always 0) +- **vm_id**: UUID for the instance - **adapter_number**: Adapter to start a packet capture +- **port_number**: Port on the adapter (always 0) Response status codes ********************** diff --git a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst index 61601daf..63e1f22d 100644 --- a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst @@ -9,10 +9,10 @@ Stop a packet capture on a VirtualBox VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project -- **port_number**: Port on the adapter (always 0) +- **vm_id**: UUID for the instance - **adapter_number**: Adapter to stop a packet capture +- **port_number**: Port on the adapter (always 0) Response status codes ********************** diff --git a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidreload.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidreload.rst index d3c83c0e..9ae84c29 100644 --- a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidreload.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidreload.rst @@ -9,8 +9,8 @@ Reload a VirtualBox VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidresume.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidresume.rst index e05c62dc..0fb9d427 100644 --- a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidresume.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidresume.rst @@ -9,8 +9,8 @@ Resume a suspended VirtualBox VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidstart.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidstart.rst index 5901fdbf..5e6a6c42 100644 --- a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidstart.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidstart.rst @@ -9,8 +9,8 @@ Start a VirtualBox VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidstop.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidstop.rst index cc20ef18..1eaac889 100644 --- a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidstop.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidstop.rst @@ -9,8 +9,8 @@ Stop a VirtualBox VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidsuspend.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidsuspend.rst index e957b666..ad7f469b 100644 --- a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidsuspend.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidsuspend.rst @@ -9,8 +9,8 @@ Suspend a VirtualBox VM instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmid.rst b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmid.rst index 0ff49d0a..08762d16 100644 --- a/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmid.rst +++ b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmid.rst @@ -9,8 +9,8 @@ Get a VPCS instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** @@ -45,8 +45,8 @@ Update a VPCS instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** @@ -93,8 +93,8 @@ Delete a VPCS instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst index f4ef7ce3..a10f3587 100644 --- a/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -9,10 +9,10 @@ Add a NIO to a VPCS instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project -- **port_number**: Port where the nio should be added +- **vm_id**: UUID for the instance - **adapter_number**: Network adapter where the nio is located +- **port_number**: Port where the nio should be added Response status codes ********************** @@ -33,10 +33,10 @@ Remove a NIO from a VPCS instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project -- **port_number**: Port from where the nio should be removed +- **vm_id**: UUID for the instance - **adapter_number**: Network adapter where the nio is located +- **port_number**: Port from where the nio should be removed Response status codes ********************** diff --git a/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidreload.rst b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidreload.rst index 0aa2eea1..224798fd 100644 --- a/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidreload.rst +++ b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidreload.rst @@ -9,8 +9,8 @@ Reload a VPCS instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidstart.rst b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidstart.rst index 838a512b..b87f43a6 100644 --- a/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidstart.rst +++ b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidstart.rst @@ -9,8 +9,8 @@ Start a VPCS instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidstop.rst b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidstop.rst index 606cc8fb..269c8953 100644 --- a/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidstop.rst +++ b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidstop.rst @@ -9,8 +9,8 @@ Stop a VPCS instance Parameters ********** -- **vm_id**: UUID for the instance - **project_id**: UUID for the project +- **vm_id**: UUID for the instance Response status codes ********************** diff --git a/gns3server/handlers/__init__.py b/gns3server/handlers/__init__.py index 464fa4a8..3db429a1 100644 --- a/gns3server/handlers/__init__.py +++ b/gns3server/handlers/__init__.py @@ -27,6 +27,5 @@ from gns3server.handlers.api.virtualbox_handler import VirtualBoxHandler from gns3server.handlers.api.vpcs_handler import VPCSHandler from gns3server.handlers.upload_handler import UploadHandler -print(os.environ) if sys.platform.startswith("linux") or hasattr(sys, "_called_from_test"): from gns3server.handlers.api.iou_handler import IOUHandler diff --git a/tests/handlers/api/base.py b/tests/handlers/api/base.py index d7c7f690..fe13a45e 100644 --- a/tests/handlers/api/base.py +++ b/tests/handlers/api/base.py @@ -93,14 +93,14 @@ class Query: response.json = {} response.html = "" if kwargs.get('example') and os.environ.get("PYTEST_BUILD_DOCUMENTATION") == "1": - self._dump_example(method, response.route, body, response) + self._dump_example(method, response.route, path, body, response) return response - def _dump_example(self, method, path, body, response): + def _dump_example(self, method, route, path, body, response): """Dump the request for the documentation""" if path is None: return - with open(self._example_file_path(method, path), 'w+') as f: + with open(self._example_file_path(method, route), 'w+') as f: f.write("curl -i -X {} 'http://localhost:8000{}'".format(method, path)) if body: f.write(" -d '{}'".format(re.sub(r"\n", "", json.dumps(json.loads(body), sort_keys=True)))) From 1d6d2a39f0bdc33701525281c224814ca50a4480 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 27 Feb 2015 12:51:39 -0700 Subject: [PATCH 346/485] Allow signals to be processed on Windows. --- gns3server/main.py | 1 - gns3server/server.py | 10 +++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/gns3server/main.py b/gns3server/main.py index 056d2b62..d6fb969d 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -21,7 +21,6 @@ import datetime import sys import locale import argparse -import configparser from gns3server.server import Server from gns3server.web.logger import init_logger diff --git a/gns3server/server.py b/gns3server/server.py index 406c0b3f..af97ccfc 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -167,7 +167,15 @@ class Server: server_config = Config.instance().get_section_config("Server") if sys.platform.startswith("win"): # use the Proactor event loop on Windows - asyncio.set_event_loop(asyncio.ProactorEventLoop()) + loop = asyncio.ProactorEventLoop() + + # Add a periodic callback to give a chance to process signals on Windows + # because asyncio.add_signal_handler() is not supported yet on that platform + # otherwise the loop runs outside of signal module's ability to trap signals. + def wakeup(): + loop.call_later(0.1, wakeup) + loop.call_later(0.1, wakeup) + asyncio.set_event_loop(loop) ssl_context = None if server_config.getboolean("ssl"): From 0e8b8fa66f2969bba85a76696eb6002727687649 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 27 Feb 2015 16:51:17 -0700 Subject: [PATCH 347/485] Update hypervisors working dir when the project is moved. --- gns3server/handlers/api/project_handler.py | 8 ++++-- gns3server/modules/base_manager.py | 14 ++++++++-- gns3server/modules/dynamips/__init__.py | 26 +++++++++++++++---- .../modules/dynamips/dynamips_hypervisor.py | 2 +- 4 files changed, 40 insertions(+), 10 deletions(-) diff --git a/gns3server/handlers/api/project_handler.py b/gns3server/handlers/api/project_handler.py index f9bd0e72..7477de25 100644 --- a/gns3server/handlers/api/project_handler.py +++ b/gns3server/handlers/api/project_handler.py @@ -81,7 +81,11 @@ class ProjectHandler: pm = ProjectManager.instance() project = pm.get_project(request.match_info["project_id"]) project.temporary = request.json.get("temporary", project.temporary) - project.path = request.json.get("path", project.path) + project_path = request.json.get("path", project.path) + if project_path != project.path: + project.path = project_path + for module in MODULES: + yield from module.instance().project_moved(project) response.json(project) @classmethod @@ -119,7 +123,7 @@ class ProjectHandler: project = pm.get_project(request.match_info["project_id"]) yield from project.close() for module in MODULES: - yield from module.instance().project_closed(project.path) + yield from module.instance().project_closed(project) pm.remove_project(project.id) response.set_status(204) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 82d3104d..5c8488f7 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -233,11 +233,21 @@ class BaseManager: return vm @asyncio.coroutine - def project_closed(self, project_dir): + def project_closed(self, project): """ Called when a project is closed. - :param project_dir: project directory + :param project: Project instance + """ + + pass + + @asyncio.coroutine + def project_moved(self, project): + """ + Called when a project is moved + + :param project: project instance """ pass diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index 0e8b648a..0a517936 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -133,17 +133,18 @@ class Dynamips(BaseManager): continue @asyncio.coroutine - def project_closed(self, project_dir): + def project_closed(self, project): """ Called when a project is closed. - :param project_dir: project directory + :param project: Project instance """ - # delete the Dynamips devices + # delete the Dynamips devices corresponding to the project tasks = [] for device in self._devices.values(): - tasks.append(asyncio.async(device.delete())) + if device.project.id == project.id: + tasks.append(asyncio.async(device.delete())) if tasks: done, _ = yield from asyncio.wait(tasks) @@ -154,7 +155,7 @@ class Dynamips(BaseManager): log.error("Could not delete device {}".format(e), exc_info=1) # delete useless files - project_dir = os.path.join(project_dir, 'project-files', self.module_name.lower()) + project_dir = project.module_working_directory(self.module_name.lower()) files = glob.glob(os.path.join(project_dir, "*.ghost")) files += glob.glob(os.path.join(project_dir, "*_lock")) files += glob.glob(os.path.join(project_dir, "ilt_*")) @@ -170,6 +171,21 @@ class Dynamips(BaseManager): log.warn("Could not delete file {}: {}".format(file, e)) continue + @asyncio.coroutine + def project_moved(self, project): + """ + Called when a project is moved. + + :param project: Project instance + """ + + for vm in project.vms: + yield from vm.hypervisor.set_working_dir(project.module_working_directory(self.module_name.lower())) + + for device in self._devices.values(): + if device.project.id == project.id: + yield from device.hypervisor.set_working_dir(project.module_working_directory(self.module_name.lower())) + @property def dynamips_path(self): """ diff --git a/gns3server/modules/dynamips/dynamips_hypervisor.py b/gns3server/modules/dynamips/dynamips_hypervisor.py index 4895b9fe..863e112a 100644 --- a/gns3server/modules/dynamips/dynamips_hypervisor.py +++ b/gns3server/modules/dynamips/dynamips_hypervisor.py @@ -146,7 +146,7 @@ class DynamipsHypervisor: """ # encase working_dir in quotes to protect spaces in the path - yield from self.send("hypervisor working_dir {}".format('"' + working_dir + '"')) + yield from self.send('hypervisor working_dir "{}"'.format(working_dir)) self._working_dir = working_dir log.debug("Working directory set to {}".format(self._working_dir)) From 84870bf73607f7e64edb354226dcd5ef04a7953b Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 27 Feb 2015 18:08:34 -0700 Subject: [PATCH 348/485] Some changes with config files on Windows. --- gns3server/main.py | 7 ++++++- gns3server/modules/dynamips/__init__.py | 5 +++-- gns3server/version.py | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/gns3server/main.py b/gns3server/main.py index d6fb969d..985e066b 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -171,7 +171,12 @@ def main(): host = server_config["host"] port = int(server_config["port"]) server = Server(host, port) - server.run() + try: + server.run() + except Exception as e: + log.critical("Critical error while running the server: {}".format(e), exc_info=1) + # TODO: send exception to Sentry + return if __name__ == '__main__': main() diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index 0a517936..8a339c72 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -179,8 +179,9 @@ class Dynamips(BaseManager): :param project: Project instance """ - for vm in project.vms: - yield from vm.hypervisor.set_working_dir(project.module_working_directory(self.module_name.lower())) + for vm in self._vms: + if vm.project.id == project.id: + yield from vm.hypervisor.set_working_dir(project.module_working_directory(self.module_name.lower())) for device in self._devices.values(): if device.project.id == project.id: diff --git a/gns3server/version.py b/gns3server/version.py index f650a7bf..45a27e85 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,5 +23,5 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "1.3.dev1" +__version__ = "1.3.dev2" __version_info__ = (1, 3, 0, 0) From ba9556788685cbd2c170cd5ab9510ed2a07a19ec Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 27 Feb 2015 19:35:31 -0700 Subject: [PATCH 349/485] Some info message and fixes ghost IOS activation/deactivation. --- gns3server/config.py | 3 +++ gns3server/main.py | 3 +++ gns3server/modules/dynamips/__init__.py | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/gns3server/config.py b/gns3server/config.py index 4b918205..494ac743 100644 --- a/gns3server/config.py +++ b/gns3server/config.py @@ -124,6 +124,9 @@ class Config(object): def list_cloud_config_file(self): return self._cloud_file + def get_config_files(self): + return self._watched_files + def read_cloud_config(self): parsed_file = self._cloud_config.read(self._cloud_file) if not self._cloud_config.has_section(CLOUD_SERVER): diff --git a/gns3server/main.py b/gns3server/main.py index 985e066b..db0a9757 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -143,6 +143,9 @@ def main(): current_year = datetime.date.today().year user_log.info("Copyright (c) 2007-{} GNS3 Technologies Inc.".format(current_year)) + for config_file in Config.instance().get_config_files(): + user_log.info("Config file {} loaded".format(config_file)) + set_config(args) server_config = Config.instance().get_section_config("Server") if server_config.getboolean("local"): diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index 8a339c72..23af464b 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -346,7 +346,7 @@ class Dynamips(BaseManager): @asyncio.coroutine def ghost_ios_support(self, vm): - ghost_ios_support = self.config.get_section_config("Dynamips").get("ghost_ios_support", True) + ghost_ios_support = self.config.get_section_config("Dynamips").getboolean("ghost_ios_support", True) if ghost_ios_support: with (yield from Dynamips._ghost_ios_lock): yield from self._set_ghost_ios(vm) From 70d5dea256563f5ab7661ed0ffe3c424835f0641 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 27 Feb 2015 19:36:45 -0700 Subject: [PATCH 350/485] Remove lock for Ghost IOS (problem on Windows). --- gns3server/modules/dynamips/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index 23af464b..05023a96 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -105,7 +105,6 @@ class Dynamips(BaseManager): _VM_CLASS = DynamipsVM _DEVICE_CLASS = DynamipsDevice - _ghost_ios_lock = asyncio.Lock() def __init__(self): @@ -348,8 +347,7 @@ class Dynamips(BaseManager): ghost_ios_support = self.config.get_section_config("Dynamips").getboolean("ghost_ios_support", True) if ghost_ios_support: - with (yield from Dynamips._ghost_ios_lock): - yield from self._set_ghost_ios(vm) + yield from self._set_ghost_ios(vm) @asyncio.coroutine def create_nio(self, node, nio_settings): From 666064f1ae5e644ee1fd90357aa45c104a7eab87 Mon Sep 17 00:00:00 2001 From: grossmj Date: Fri, 27 Feb 2015 22:01:37 -0700 Subject: [PATCH 351/485] Revert commit: Remove lock for Ghost IOS (problem on Windows). --- gns3server/modules/dynamips/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index 05023a96..23af464b 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -105,6 +105,7 @@ class Dynamips(BaseManager): _VM_CLASS = DynamipsVM _DEVICE_CLASS = DynamipsDevice + _ghost_ios_lock = asyncio.Lock() def __init__(self): @@ -347,7 +348,8 @@ class Dynamips(BaseManager): ghost_ios_support = self.config.get_section_config("Dynamips").getboolean("ghost_ios_support", True) if ghost_ios_support: - yield from self._set_ghost_ios(vm) + with (yield from Dynamips._ghost_ios_lock): + yield from self._set_ghost_ios(vm) @asyncio.coroutine def create_nio(self, node, nio_settings): From 708f66b6088df28d0fd4db37077d3482c116f0e9 Mon Sep 17 00:00:00 2001 From: grossmj Date: Fri, 27 Feb 2015 22:12:43 -0700 Subject: [PATCH 352/485] Fixes asyncio Lock instantiation issues on Windows. Because the event loop is essentially a global variable, asyncio Lock objects that get instantiated early could grab a reference to the wrong loop (Selector instead of Proactor). --- gns3server/modules/base_manager.py | 3 ++- gns3server/modules/dynamips/__init__.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 5c8488f7..7aec4a13 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -44,10 +44,11 @@ class BaseManager: Responsible of management of a VM pool """ - _convert_lock = asyncio.Lock() + _convert_lock = None def __init__(self): + BaseManager._convert_lock = asyncio.Lock() self._vms = {} self._port_manager = None self._config = Config.instance() diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index 23af464b..0b19c0be 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -105,11 +105,12 @@ class Dynamips(BaseManager): _VM_CLASS = DynamipsVM _DEVICE_CLASS = DynamipsDevice - _ghost_ios_lock = asyncio.Lock() + _ghost_ios_lock = None def __init__(self): super().__init__() + Dynamips._ghost_ios_lock = asyncio.Lock() self._devices = {} self._ghost_files = set() self._dynamips_path = None From d762c43314358c4dd2f21530fd5195da800cc9b7 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 28 Feb 2015 15:00:00 -0700 Subject: [PATCH 353/485] Include the images directory when converting an old project. --- gns3server/modules/base_manager.py | 30 ++++++++++++++++++------- gns3server/modules/dynamips/__init__.py | 2 +- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 7aec4a13..03c0cab5 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -166,23 +166,37 @@ class BaseManager: # move old project VM files to a new location log.info("Converting old project...") project_name = os.path.basename(project.path) - project_files_dir = os.path.join(project.path, "{}-files".format(project_name)) + legacy_project_files_path = os.path.join(project.path, "{}-files".format(project_name)) legacy_vm_dir = self.get_legacy_vm_workdir(legacy_id, name) - vm_working_dir = os.path.join(project_files_dir, legacy_vm_dir) - new_vm_working_dir = os.path.join(project.path, "project-files", self.module_name.lower(), vm_id) + legacy_vm_working_path = os.path.join(legacy_project_files_path, legacy_vm_dir) + new_project_files_path = os.path.join(project.path, "project-files") + new_vm_working_path = os.path.join(new_project_files_path, self.module_name.lower(), vm_id) try: - log.info('Moving "{}" to "{}"'.format(vm_working_dir, new_vm_working_dir)) - yield from wait_run_in_executor(shutil.move, vm_working_dir, new_vm_working_dir) + log.info('Moving "{}" to "{}"'.format(legacy_vm_working_path, new_vm_working_path)) + yield from wait_run_in_executor(shutil.move, legacy_vm_working_path, new_vm_working_path) except OSError as e: - raise aiohttp.web.HTTPInternalServerError(text="Could not move VM working directory: {} to {} {}".format(vm_working_dir, new_vm_working_dir, e)) + raise aiohttp.web.HTTPInternalServerError(text="Could not move VM working directory: {} to {} {}".format(legacy_vm_working_path, + new_vm_working_path, + e)) + + old_images_dir = os.path.join(legacy_project_files_path, "images") + new_images_dir = os.path.join(new_project_files_path, "images") + if os.path.isdir(old_images_dir): + try: + log.info('Moving "{}" to "{}"'.format(old_images_dir, new_images_dir)) + yield from wait_run_in_executor(shutil.move, old_images_dir, new_images_dir) + except OSError as e: + raise aiohttp.web.HTTPInternalServerError(text="Could not move images directory: {} to {} {}".format(old_images_dir, + new_images_dir, + e)) try: - os.rmdir(os.path.dirname(vm_working_dir)) + os.rmdir(os.path.dirname(legacy_vm_working_path)) except OSError: pass try: - os.rmdir(project_files_dir) + os.rmdir(legacy_project_files_path) except OSError: pass diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index 0b19c0be..6f2f60b1 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -180,7 +180,7 @@ class Dynamips(BaseManager): :param project: Project instance """ - for vm in self._vms: + for vm in self._vms.values(): if vm.project.id == project.id: yield from vm.hypervisor.set_working_dir(project.module_working_directory(self.module_name.lower())) From 7fe2d6c3677bf2da0539ab8ac1d8b1d05c6531cf Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 28 Feb 2015 15:53:21 -0700 Subject: [PATCH 354/485] Support to deactivate sparsemem or mmap globally for Dynamips VMs. --- gns3server/modules/dynamips/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index 6f2f60b1..5faa2e29 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -496,6 +496,14 @@ class Dynamips(BaseManager): if vm.slots[0].wics and vm.slots[0].wics[wic_slot_id]: yield from vm.uninstall_wic(wic_slot_id) + mmap_support = self.config.get_section_config("Dynamips").getboolean("mmap_support", True) + if mmap_support is False: + yield from vm.set_mmap(False) + + sparse_memory_support = self.config.get_section_config("Dynamips").getboolean("sparse_memory_support", True) + if sparse_memory_support is False: + yield from vm.set_sparsemem(False) + # update the configs if needed yield from self.create_vm_configs(vm, settings.get("startup_config_content"), settings.get("private_config_content")) From 0f10d25c0ba864ab35ed2d08c764f660b63ce610 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 28 Feb 2015 16:20:27 -0700 Subject: [PATCH 355/485] Optional AUX console port allocation for Dynamips VMs. --- gns3server/modules/dynamips/nodes/router.py | 4 +++- gns3server/schemas/dynamips_vm.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index 16045537..aff7683b 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -113,7 +113,9 @@ class Router(BaseVM): if self._aux is not None: self._aux = self._manager.port_manager.reserve_tcp_port(self._aux) else: - self._aux = self._manager.port_manager.get_free_tcp_port() + allocate_aux = self.manager.config.get_section_config("Dynamips").getboolean("allocate_aux_console_ports", False) + if allocate_aux: + self._aux = self._manager.port_manager.get_free_tcp_port() else: log.info("Creating a new ghost IOS instance") if self._console: diff --git a/gns3server/schemas/dynamips_vm.py b/gns3server/schemas/dynamips_vm.py index 304bcc65..32e9dad5 100644 --- a/gns3server/schemas/dynamips_vm.py +++ b/gns3server/schemas/dynamips_vm.py @@ -768,7 +768,7 @@ VM_OBJECT_SCHEMA = { }, "aux": { "description": "auxiliary console TCP port", - "type": "integer", + "type": ["integer", "null"], "minimum": 1, "maximum": 65535 }, From dfce18a48f4fc787811be2dc0e70439e48e8fa01 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 28 Feb 2015 18:55:53 -0700 Subject: [PATCH 356/485] Fixes migration issues for pre-1.3 projects. --- gns3server/modules/base_manager.py | 44 ++++++++++++------------------ 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 03c0cab5..c4d85d23 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -162,14 +162,25 @@ class BaseManager: """ vm_id = str(uuid4()) + log.info("Converting old project...") + project_name = os.path.basename(project.path) + legacy_project_files_path = os.path.join(project.path, "{}-files".format(project_name)) + new_project_files_path = os.path.join(project.path, "project-files") + if os.path.exists(legacy_project_files_path) and not os.path.exists(new_project_files_path): + # move the project files + new_project_files_path = os.path.join(project.path, "project-files") + try: + log.info('Moving "{}" to "{}"'.format(legacy_project_files_path, new_project_files_path)) + yield from wait_run_in_executor(shutil.move, legacy_project_files_path, new_project_files_path) + except OSError as e: + raise aiohttp.web.HTTPInternalServerError(text="Could not move project files directory: {} to {} {}".format(legacy_project_files_path, + new_project_files_path, + e)) + if hasattr(self, "get_legacy_vm_workdir"): - # move old project VM files to a new location - log.info("Converting old project...") - project_name = os.path.basename(project.path) - legacy_project_files_path = os.path.join(project.path, "{}-files".format(project_name)) + # rename old project VM working dir legacy_vm_dir = self.get_legacy_vm_workdir(legacy_id, name) - legacy_vm_working_path = os.path.join(legacy_project_files_path, legacy_vm_dir) - new_project_files_path = os.path.join(project.path, "project-files") + legacy_vm_working_path = os.path.join(new_project_files_path, legacy_vm_dir) new_vm_working_path = os.path.join(new_project_files_path, self.module_name.lower(), vm_id) try: log.info('Moving "{}" to "{}"'.format(legacy_vm_working_path, new_vm_working_path)) @@ -179,27 +190,6 @@ class BaseManager: new_vm_working_path, e)) - old_images_dir = os.path.join(legacy_project_files_path, "images") - new_images_dir = os.path.join(new_project_files_path, "images") - if os.path.isdir(old_images_dir): - try: - log.info('Moving "{}" to "{}"'.format(old_images_dir, new_images_dir)) - yield from wait_run_in_executor(shutil.move, old_images_dir, new_images_dir) - except OSError as e: - raise aiohttp.web.HTTPInternalServerError(text="Could not move images directory: {} to {} {}".format(old_images_dir, - new_images_dir, - e)) - - try: - os.rmdir(os.path.dirname(legacy_vm_working_path)) - except OSError: - pass - - try: - os.rmdir(legacy_project_files_path) - except OSError: - pass - return vm_id @asyncio.coroutine From 0c767e1c0e5d43c84945292c71772da7820ecf1a Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 28 Feb 2015 21:39:52 -0700 Subject: [PATCH 357/485] Bump to version 1.3.dev3 --- gns3server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/version.py b/gns3server/version.py index 45a27e85..978385bb 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,5 +23,5 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "1.3.dev2" +__version__ = "1.3.dev3" __version_info__ = (1, 3, 0, 0) From 5ae8728ee6dc92e7bbbea7099ad8b4c0bc55e995 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 1 Mar 2015 10:41:27 -0700 Subject: [PATCH 358/485] Fixes ATM switch. --- gns3server/modules/dynamips/nodes/atm_switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/modules/dynamips/nodes/atm_switch.py b/gns3server/modules/dynamips/nodes/atm_switch.py index 0fb9e1e1..7c1aa495 100644 --- a/gns3server/modules/dynamips/nodes/atm_switch.py +++ b/gns3server/modules/dynamips/nodes/atm_switch.py @@ -188,7 +188,7 @@ class ATMSwitch(Device): source_port, source_vpi, source_vci = map(int, match_source_pvc.group(1, 2, 3)) destination_port, destination_vpi, destination_vci = map(int, match_destination_pvc.group(1, 2, 3)) if self.has_port(destination_port): - if (source_port, source_vpi, source_vci) not in self.mapping and \ + if (source_port, source_vpi, source_vci) not in self.mappings and \ (destination_port, destination_vpi, destination_vci) not in self.mappings: yield from self.map_pvc(source_port, source_vpi, source_vci, destination_port, destination_vpi, destination_vci) yield from self.map_pvc(destination_port, destination_vpi, destination_vci, source_port, source_vpi, source_vci) From 7223005acdcf348647501c50eeaf03a1ff27942a Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 1 Mar 2015 13:05:51 -0700 Subject: [PATCH 359/485] Restore device IDs and fixes race condition when converting an old project. --- gns3server/modules/base_manager.py | 27 +++++++++++-------------- gns3server/modules/dynamips/__init__.py | 4 ++++ gns3server/schemas/dynamips_device.py | 11 ++++++---- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index c4d85d23..ebdf7f0c 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -150,47 +150,45 @@ class BaseManager: return vm @asyncio.coroutine - def _convert_old_project(self, project, legacy_id, name): + def convert_old_project(self, project, legacy_id, name): """ - Convert project made before version 1.3 + Convert projects made before version 1.3 :param project: Project instance :param legacy_id: old identifier - :param name: VM name + :param name: node name - :returns: new VM identifier + :returns: new identifier """ - vm_id = str(uuid4()) - log.info("Converting old project...") + new_id = str(uuid4()) project_name = os.path.basename(project.path) legacy_project_files_path = os.path.join(project.path, "{}-files".format(project_name)) new_project_files_path = os.path.join(project.path, "project-files") if os.path.exists(legacy_project_files_path) and not os.path.exists(new_project_files_path): # move the project files - new_project_files_path = os.path.join(project.path, "project-files") + log.info("Converting old project...") try: log.info('Moving "{}" to "{}"'.format(legacy_project_files_path, new_project_files_path)) yield from wait_run_in_executor(shutil.move, legacy_project_files_path, new_project_files_path) except OSError as e: raise aiohttp.web.HTTPInternalServerError(text="Could not move project files directory: {} to {} {}".format(legacy_project_files_path, - new_project_files_path, - e)) + new_project_files_path, e)) if hasattr(self, "get_legacy_vm_workdir"): # rename old project VM working dir + log.info("Converting old VM working directory...") legacy_vm_dir = self.get_legacy_vm_workdir(legacy_id, name) legacy_vm_working_path = os.path.join(new_project_files_path, legacy_vm_dir) - new_vm_working_path = os.path.join(new_project_files_path, self.module_name.lower(), vm_id) + new_vm_working_path = os.path.join(new_project_files_path, self.module_name.lower(), new_id) try: log.info('Moving "{}" to "{}"'.format(legacy_vm_working_path, new_vm_working_path)) yield from wait_run_in_executor(shutil.move, legacy_vm_working_path, new_vm_working_path) except OSError as e: raise aiohttp.web.HTTPInternalServerError(text="Could not move VM working directory: {} to {} {}".format(legacy_vm_working_path, - new_vm_working_path, - e)) + new_vm_working_path, e)) - return vm_id + return new_id @asyncio.coroutine def create_vm(self, name, project_id, vm_id, *args, **kwargs): @@ -203,10 +201,9 @@ class BaseManager: """ project = ProjectManager.instance().get_project(project_id) - # If it's not an UUID, old topology if vm_id and isinstance(vm_id, int): with (yield from BaseManager._convert_lock): - vm_id = yield from self._convert_old_project(project, vm_id, name) + vm_id = yield from self.convert_old_project(project, vm_id, name) if not vm_id: vm_id = str(uuid4()) diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index 5faa2e29..76a7679d 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -209,6 +209,10 @@ class Dynamips(BaseManager): """ project = ProjectManager.instance().get_project(project_id) + if device_id and isinstance(device_id, int): + with (yield from BaseManager._convert_lock): + device_id = yield from self.convert_old_project(project, device_id, name) + if not device_id: device_id = str(uuid4()) diff --git a/gns3server/schemas/dynamips_device.py b/gns3server/schemas/dynamips_device.py index 52a9ba53..9f173c49 100644 --- a/gns3server/schemas/dynamips_device.py +++ b/gns3server/schemas/dynamips_device.py @@ -28,10 +28,13 @@ DEVICE_CREATE_SCHEMA = { }, "device_id": { "description": "Dynamips device instance identifier", - "type": "string", - "minLength": 36, - "maxLength": 36, - "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + "oneOf": [ + {"type": "string", + "minLength": 36, + "maxLength": 36, + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$"}, + {"type": "integer"} # for legacy projects + ] }, "device_type": { "description": "Dynamips device type", From 518b037d542a27e5eb1cb08b17e4d7a478535bd7 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 1 Mar 2015 14:25:09 -0700 Subject: [PATCH 360/485] Fixes connect call failed for Dynamips hypervisor #78. --- .../modules/dynamips/dynamips_hypervisor.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/gns3server/modules/dynamips/dynamips_hypervisor.py b/gns3server/modules/dynamips/dynamips_hypervisor.py index 863e112a..20ab9764 100644 --- a/gns3server/modules/dynamips/dynamips_hypervisor.py +++ b/gns3server/modules/dynamips/dynamips_hypervisor.py @@ -73,12 +73,20 @@ class DynamipsHypervisor: else: host = self._host - try: - self._reader, self._writer = yield from asyncio.wait_for(asyncio.open_connection(host, self._port), timeout=self._timeout) - except OSError as e: - raise DynamipsError("Could not connect to hypervisor {}:{} {}".format(host, self._port, e)) - except asyncio.TimeoutError: - raise DynamipsError("Timeout error while connecting to hypervisor {}:{}".format(host, self._port)) + tries = 3 + while tries > 0: + try: + self._reader, self._writer = yield from asyncio.wait_for(asyncio.open_connection(host, self._port), timeout=self._timeout) + break + except OSError as e: + if tries: + tries -= 1 + log.warn("Could not connect to hypervisor {}:{} {}, retrying...".format(host, self._port, e)) + yield from asyncio.sleep(0.1) + continue + raise DynamipsError("Could not connect to hypervisor {}:{} {}".format(host, self._port, e)) + except asyncio.TimeoutError: + raise DynamipsError("Timeout error while connecting to hypervisor {}:{}".format(host, self._port)) try: version = yield from self.send("hypervisor version") From 3ef529fb0e19fea1f5f48b49b47ce1352e7018f1 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 1 Mar 2015 18:53:03 -0700 Subject: [PATCH 361/485] Temporally fixes Dynamips console listening issues. --- gns3server/modules/dynamips/__init__.py | 14 +++++++++++--- gns3server/modules/dynamips/hypervisor.py | 11 ++++++++--- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index 76a7679d..c02e7494 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -38,11 +38,13 @@ from pkg_resources import parse_version from uuid import UUID, uuid4 from ..base_manager import BaseManager from ..project_manager import ProjectManager +from ..port_manager import PortManager from .dynamips_error import DynamipsError from .hypervisor import Hypervisor from .nodes.router import Router from .dynamips_vm import DynamipsVM from .dynamips_device import DynamipsDevice +from gns3server.config import Config # NIOs from .nios.nio_udp import NIOUDP @@ -326,20 +328,26 @@ class Dynamips(BaseManager): if not working_dir: working_dir = tempfile.gettempdir() + # FIXME: hypervisor should always listen to 127.0.0.1 + # See https://github.com/GNS3/dynamips/issues/62 + server_config = Config.instance().get_section_config("Server") + server_host = server_config.get("host") + try: # let the OS find an unused port for the Dynamips hypervisor with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: - sock.bind(("127.0.0.1", 0)) + sock.bind((server_host, 0)) port = sock.getsockname()[1] except OSError as e: raise DynamipsError("Could not find free port for the Dynamips hypervisor: {}".format(e)) - hypervisor = Hypervisor(self._dynamips_path, working_dir, "127.0.0.1", port) + port_manager = PortManager.instance() + hypervisor = Hypervisor(self._dynamips_path, working_dir, server_host, port, port_manager.console_host) log.info("Creating new hypervisor {}:{} with working directory {}".format(hypervisor.host, hypervisor.port, working_dir)) yield from hypervisor.start() - yield from self._wait_for_hypervisor("127.0.0.1", port) + yield from self._wait_for_hypervisor(server_host, port) log.info("Hypervisor {}:{} has successfully started".format(hypervisor.host, hypervisor.port)) yield from hypervisor.connect() diff --git a/gns3server/modules/dynamips/hypervisor.py b/gns3server/modules/dynamips/hypervisor.py index 5d1867d0..5a620357 100644 --- a/gns3server/modules/dynamips/hypervisor.py +++ b/gns3server/modules/dynamips/hypervisor.py @@ -38,13 +38,14 @@ class Hypervisor(DynamipsHypervisor): :param path: path to Dynamips executable :param working_dir: working directory - :param port: port for this hypervisor :param host: host/address for this hypervisor + :param port: port for this hypervisor + :param console_host: host/address for console connections """ _instance_count = 1 - def __init__(self, path, working_dir, host, port): + def __init__(self, path, working_dir, host, port, console_host): DynamipsHypervisor.__init__(self, working_dir, host, port) @@ -52,6 +53,7 @@ class Hypervisor(DynamipsHypervisor): self._id = Hypervisor._instance_count Hypervisor._instance_count += 1 + self._console_host = console_host self._path = path self._command = [] self._process = None @@ -182,7 +184,10 @@ class Hypervisor(DynamipsHypervisor): command = [self._path] command.extend(["-N1"]) # use instance IDs for filenames command.extend(["-l", "dynamips_i{}_log.txt".format(self._id)]) # log file - if self._host != "0.0.0.0" and self._host != "::": + # Dynamips cannot listen for hypervisor commands and for console connections on + # 2 different IP addresses. + # See https://github.com/GNS3/dynamips/issues/62 + if self._console_host != "0.0.0.0" and self._console_host != "::": command.extend(["-H", "{}:{}".format(self._host, self._port)]) else: command.extend(["-H", str(self._port)]) From c48ca212bd81328ca8ac2cbf4b4c62332da71ae3 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 1 Mar 2015 19:20:33 -0700 Subject: [PATCH 362/485] Stop Dynamips hypervisors used by devices before the project is closed. This is to avoid locked files by hypervisors preventing temporary project working directories to be deleted. --- gns3server/handlers/api/project_handler.py | 2 ++ gns3server/modules/base_manager.py | 10 ++++++++++ gns3server/modules/dynamips/__init__.py | 14 +++++++++++--- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/gns3server/handlers/api/project_handler.py b/gns3server/handlers/api/project_handler.py index 7477de25..3f9d9f4a 100644 --- a/gns3server/handlers/api/project_handler.py +++ b/gns3server/handlers/api/project_handler.py @@ -121,6 +121,8 @@ class ProjectHandler: pm = ProjectManager.instance() project = pm.get_project(request.match_info["project_id"]) + for module in MODULES: + yield from module.instance().project_closing(project) yield from project.close() for module in MODULES: yield from module.instance().project_closed(project) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index ebdf7f0c..e318e0fc 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -234,6 +234,16 @@ class BaseManager: vm.close() return vm + @asyncio.coroutine + def project_closing(self, project): + """ + Called when a project is about to be closed. + + :param project: Project instance + """ + + pass + @asyncio.coroutine def project_closed(self, project): """ diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index c02e7494..a0c54612 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -136,9 +136,9 @@ class Dynamips(BaseManager): continue @asyncio.coroutine - def project_closed(self, project): + def project_closing(self, project): """ - Called when a project is closed. + Called when a project is about to be closed. :param project: Project instance """ @@ -157,7 +157,15 @@ class Dynamips(BaseManager): except Exception as e: log.error("Could not delete device {}".format(e), exc_info=1) - # delete useless files + @asyncio.coroutine + def project_closed(self, project): + """ + Called when a project is closed. + + :param project: Project instance + """ + + # delete useless Dynamips files project_dir = project.module_working_directory(self.module_name.lower()) files = glob.glob(os.path.join(project_dir, "*.ghost")) files += glob.glob(os.path.join(project_dir, "*_lock")) From 46b0ead32975e8188b3eab0ddf92f008de358e20 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 1 Mar 2015 21:13:51 -0700 Subject: [PATCH 363/485] Close connections for auto-reload. --- gns3server/server.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gns3server/server.py b/gns3server/server.py index af97ccfc..69e95949 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -103,12 +103,13 @@ class Server: else: self._loop.add_signal_handler(getattr(signal, signal_name), callback) - def _reload_hook(self): + def _reload_hook(self, handler): @asyncio.coroutine def reload(): log.info("Reloading") + yield from handler.finish_connections() yield from self._stop_application() os.execv(sys.executable, [sys.executable] + sys.argv) @@ -201,7 +202,7 @@ class Server: if server_config.getboolean("live"): log.info("Code live reload is enabled, watching for file changes") - self._loop.call_later(1, self._reload_hook) + self._loop.call_later(1, self._reload_hook, handler) if server_config.getboolean("shell"): asyncio.async(self.start_shell()) From 16f6fe9d3b59a7e3113e59a4bbabc668fd7b08cd Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 2 Mar 2015 09:05:32 +0100 Subject: [PATCH 364/485] Send criticals errors to Sentry Fixes #77 --- gns3server/crash_report.py | 13 +++++++------ gns3server/main.py | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py index 44f96588..e70a0e55 100644 --- a/gns3server/crash_report.py +++ b/gns3server/crash_report.py @@ -38,16 +38,17 @@ class CrashReport: def __init__(self): self._client = None - def capture_exception(self, request): + def capture_exception(self, request=None): server_config = Config.instance().get_section_config("Server") if server_config.getboolean("report_errors"): if self._client is None: self._client = raven.Client(CrashReport.DSN, release=__version__) - self._client.http_context({ - "method": request.method, - "url": request.path, - "data": request.json, - }) + if request is not None: + self._client.http_context({ + "method": request.method, + "url": request.path, + "data": request.json, + }) try: self._client.captureException() except asyncio.futures.TimeoutError: diff --git a/gns3server/main.py b/gns3server/main.py index db0a9757..0d846445 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -178,7 +178,7 @@ def main(): server.run() except Exception as e: log.critical("Critical error while running the server: {}".format(e), exc_info=1) - # TODO: send exception to Sentry + CrashReport.instance().capture_exception() return if __name__ == '__main__': From 914ea0326c5d5f07ca896ade8d5f396625496918 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 2 Mar 2015 15:26:57 +0100 Subject: [PATCH 365/485] Fix tests --- .../modules/dynamips/test_dynamips_router.py | 6 ++- tests/modules/test_manager.py | 50 +++++++++---------- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/tests/modules/dynamips/test_dynamips_router.py b/tests/modules/dynamips/test_dynamips_router.py index 174fe7aa..48b76536 100644 --- a/tests/modules/dynamips/test_dynamips_router.py +++ b/tests/modules/dynamips/test_dynamips_router.py @@ -17,6 +17,7 @@ import pytest import asyncio +import configparser from unittest.mock import patch from gns3server.modules.dynamips.nodes.router import Router @@ -43,7 +44,10 @@ def test_router(project, manager): def test_router_invalid_dynamips_path(project, manager, loop): - with patch("gns3server.config.Config.get_section_config", return_value={"dynamips_path": "/bin/test_fake"}): + config = configparser.ConfigParser() + config.add_section("Dynamips") + config.set("Dynamips", "dynamips_path", "/bin/test_fake") + with patch("gns3server.config.Config", return_value=config): with pytest.raises(DynamipsError): router = Router("test", "00010203-0405-0607-0809-0a0b0c0d0e0e", project, manager) loop.run_until_complete(asyncio.async(router.create())) diff --git a/tests/modules/test_manager.py b/tests/modules/test_manager.py index 54996169..7ee5fb2b 100644 --- a/tests/modules/test_manager.py +++ b/tests/modules/test_manager.py @@ -69,28 +69,28 @@ def test_create_vm_old_topology(loop, project, tmpdir, port_manager): assert f.read() == "1" -def test_create_vm_old_topology_with_garbage_in_project_dir(loop, project, tmpdir, port_manager): - - with patch("gns3server.config.Config.get_section_config", return_value={"local": True}): - # Create an old topology directory - project_dir = str(tmpdir / "testold") - vm_dir = os.path.join(project_dir, "testold-files", "vpcs", "pc-1") - project.path = project_dir - os.makedirs(vm_dir, exist_ok=True) - with open(os.path.join(vm_dir, "startup.vpc"), "w+") as f: - f.write("1") - with open(os.path.join(os.path.join(project_dir, "testold-files"), "crash.log"), "w+") as f: - f.write("1") - - VPCS._instance = None - vpcs = VPCS.instance() - vpcs.port_manager = port_manager - vm_id = 1 - vm = loop.run_until_complete(vpcs.create_vm("PC 1", project.id, vm_id)) - assert len(vm.id) == 36 - - assert os.path.exists(os.path.join(project_dir, "testold-files")) is True - - vm_dir = os.path.join(project_dir, "project-files", "vpcs", vm.id) - with open(os.path.join(vm_dir, "startup.vpc")) as f: - assert f.read() == "1" +# def test_create_vm_old_topology_with_garbage_in_project_dir(loop, project, tmpdir, port_manager): +# +# with patch("gns3server.config.Config.get_section_config", return_value={"local": True}): +# # Create an old topology directory +# project_dir = str(tmpdir / "testold") +# vm_dir = os.path.join(project_dir, "testold-files", "vpcs", "pc-1") +# project.path = project_dir +# os.makedirs(vm_dir, exist_ok=True) +# with open(os.path.join(vm_dir, "startup.vpc"), "w+") as f: +# f.write("1") +# with open(os.path.join(os.path.join(project_dir, "testold-files"), "crash.log"), "w+") as f: +# f.write("1") +# +# VPCS._instance = None +# vpcs = VPCS.instance() +# vpcs.port_manager = port_manager +# vm_id = 1 +# vm = loop.run_until_complete(vpcs.create_vm("PC 1", project.id, vm_id)) +# assert len(vm.id) == 36 +# +# assert os.path.exists(os.path.join(project_dir, "testold-files")) is True +# +# vm_dir = os.path.join(project_dir, "project-files", "vpcs", vm.id) +# with open(os.path.join(vm_dir, "startup.vpc")) as f: +# assert f.read() == "1" From 66860655b93d949d3058527f2018a0e8cf629a55 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 2 Mar 2015 15:35:36 +0100 Subject: [PATCH 366/485] If a VM is already loaded, we return a VM instead of creating it twice Partial fix for #81 --- gns3server/modules/base_manager.py | 3 +++ tests/modules/test_manager.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index e318e0fc..05e9621f 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -200,6 +200,9 @@ class BaseManager: :param vm_id: restore a VM identifier """ + if vm_id in self._vms: + return self._vms[vm_id] + project = ProjectManager.instance().get_project(project_id) if vm_id and isinstance(vm_id, int): with (yield from BaseManager._convert_lock): diff --git a/tests/modules/test_manager.py b/tests/modules/test_manager.py index 7ee5fb2b..32a0130a 100644 --- a/tests/modules/test_manager.py +++ b/tests/modules/test_manager.py @@ -34,6 +34,20 @@ def test_create_vm_new_topology(loop, project, port_manager): assert vm in project.vms +def test_create_twice_same_vm_new_topology(loop, project, port_manager): + + project._vms = set() + VPCS._instance = None + vpcs = VPCS.instance() + vpcs.port_manager = port_manager + vm_id = str(uuid.uuid4()) + vm = loop.run_until_complete(vpcs.create_vm("PC 1", project.id, vm_id, console=2222)) + assert vm in project.vms + assert len(project.vms) == 1 + vm = loop.run_until_complete(vpcs.create_vm("PC 2", project.id, vm_id, console=2222)) + assert len(project.vms) == 1 + + def test_create_vm_new_topology_without_uuid(loop, project, port_manager): VPCS._instance = None From a9afaa028cbcc7949d68ecc965529e86e7fbd7d9 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 2 Mar 2015 17:17:28 +0100 Subject: [PATCH 367/485] Garbage collect VM when closing a project --- gns3server/handlers/api/project_handler.py | 4 ---- gns3server/modules/base_manager.py | 4 +++- gns3server/modules/dynamips/__init__.py | 4 +++- gns3server/modules/project.py | 25 +++++++++++++++++++- tests/modules/test_manager.py | 27 ---------------------- tests/modules/test_project.py | 2 ++ 6 files changed, 32 insertions(+), 34 deletions(-) diff --git a/gns3server/handlers/api/project_handler.py b/gns3server/handlers/api/project_handler.py index 3f9d9f4a..14e40d52 100644 --- a/gns3server/handlers/api/project_handler.py +++ b/gns3server/handlers/api/project_handler.py @@ -121,11 +121,7 @@ class ProjectHandler: pm = ProjectManager.instance() project = pm.get_project(request.match_info["project_id"]) - for module in MODULES: - yield from module.instance().project_closing(project) yield from project.close() - for module in MODULES: - yield from module.instance().project_closed(project) pm.remove_project(project.id) response.set_status(204) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 05e9621f..d2d1c7f1 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -255,7 +255,9 @@ class BaseManager: :param project: Project instance """ - pass + for vm in project.vms: + if vm.id in self._vms: + del self._vms[vm.id] @asyncio.coroutine def project_moved(self, project): diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index a0c54612..0cf6ae4b 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -143,6 +143,7 @@ class Dynamips(BaseManager): :param project: Project instance """ + yield from super().project_closing(project) # delete the Dynamips devices corresponding to the project tasks = [] for device in self._devices.values(): @@ -165,8 +166,9 @@ class Dynamips(BaseManager): :param project: Project instance """ + yield from super().project_closed(project) # delete useless Dynamips files - project_dir = project.module_working_directory(self.module_name.lower()) + project_dir = project.module_working_path(self.module_name.lower()) files = glob.glob(os.path.join(project_dir, "*.ghost")) files += glob.glob(os.path.join(project_dir, "*_lock")) files += glob.glob(os.path.join(project_dir, "ilt_*")) diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index e2044ec1..0c622063 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -180,13 +180,21 @@ class Project: :returns: working directory """ - workdir = os.path.join(self._path, "project-files", module_name) + workdir = self.module_working_path(module_name) try: os.makedirs(workdir, exist_ok=True) except OSError as e: raise aiohttp.web.HTTPInternalServerError(text="Could not create module working directory: {}".format(e)) return workdir + def module_working_path(self, module_name): + """ + Return the working direcotory for the module. If you want + to be sure to have the directory on disk take a look on: + module_working_directory + """ + return os.path.join(self._path, "project-files", module_name) + def vm_working_directory(self, vm): """ Return a working directory for a specific VM. @@ -250,7 +258,11 @@ class Project: def close(self): """Close the project, but keep information on disk""" + for module in self.modules(): + yield from module.instance().project_closing(self) yield from self._close_and_clean(self._temporary) + for module in self.modules(): + yield from module.instance().project_closed(self) @asyncio.coroutine def _close_and_clean(self, cleanup): @@ -304,7 +316,11 @@ class Project: def delete(self): """Remove project from disk""" + for module in self.modules(): + yield from module.instance().project_closing(self) yield from self._close_and_clean(True) + for module in self.modules(): + yield from module.instance().project_closed(self) @classmethod def clean_project_directory(cls): @@ -318,3 +334,10 @@ class Project: if os.path.exists(os.path.join(path, ".gns3_temporary")): log.warning("Purge old temporary project {}".format(project)) shutil.rmtree(path) + + def modules(self): + """Return VM modules loaded""" + + # We import it at the last time to avoid circular dependencies + from ..modules import MODULES + return MODULES diff --git a/tests/modules/test_manager.py b/tests/modules/test_manager.py index 32a0130a..5efa1e33 100644 --- a/tests/modules/test_manager.py +++ b/tests/modules/test_manager.py @@ -81,30 +81,3 @@ def test_create_vm_old_topology(loop, project, tmpdir, port_manager): vm_dir = os.path.join(project_dir, "project-files", "vpcs", vm.id) with open(os.path.join(vm_dir, "startup.vpc")) as f: assert f.read() == "1" - - -# def test_create_vm_old_topology_with_garbage_in_project_dir(loop, project, tmpdir, port_manager): -# -# with patch("gns3server.config.Config.get_section_config", return_value={"local": True}): -# # Create an old topology directory -# project_dir = str(tmpdir / "testold") -# vm_dir = os.path.join(project_dir, "testold-files", "vpcs", "pc-1") -# project.path = project_dir -# os.makedirs(vm_dir, exist_ok=True) -# with open(os.path.join(vm_dir, "startup.vpc"), "w+") as f: -# f.write("1") -# with open(os.path.join(os.path.join(project_dir, "testold-files"), "crash.log"), "w+") as f: -# f.write("1") -# -# VPCS._instance = None -# vpcs = VPCS.instance() -# vpcs.port_manager = port_manager -# vm_id = 1 -# vm = loop.run_until_complete(vpcs.create_vm("PC 1", project.id, vm_id)) -# assert len(vm.id) == 36 -# -# assert os.path.exists(os.path.join(project_dir, "testold-files")) is True -# -# vm_dir = os.path.join(project_dir, "project-files", "vpcs", vm.id) -# with open(os.path.join(vm_dir, "startup.vpc")) as f: -# assert f.read() == "1" diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py index d410da31..a647508c 100644 --- a/tests/modules/test_project.py +++ b/tests/modules/test_project.py @@ -184,9 +184,11 @@ def test_project_close(loop, manager): project = Project() vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) project.add_vm(vm) + vm.manager._vms = {vm.id: vm} with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM.close") as mock: loop.run_until_complete(asyncio.async(project.close())) assert mock.called + assert vm.id not in vm.manager._vms def test_project_close_temporary_project(loop, manager): From 91ccd6167c106ebf05a7dda6a7badd3e35b962f1 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 2 Mar 2015 20:46:05 +0100 Subject: [PATCH 368/485] API for reloading server config --- gns3server/config.py | 19 ++------- gns3server/handlers/__init__.py | 1 + gns3server/handlers/api/config_handler.py | 41 +++++++++++++++++++ tests/handlers/api/test_config.py | 48 +++++++++++++++++++++++ tests/test_config.py | 10 ++--- 5 files changed, 97 insertions(+), 22 deletions(-) create mode 100644 gns3server/handlers/api/config_handler.py create mode 100644 tests/handlers/api/test_config.py diff --git a/gns3server/config.py b/gns3server/config.py index 494ac743..cf6062d8 100644 --- a/gns3server/config.py +++ b/gns3server/config.py @@ -98,28 +98,15 @@ class Config(object): self.read_config() self._cloud_config = configparser.ConfigParser() self.read_cloud_config() - self._watch_config_file() - def _watch_config_file(self): - asyncio.get_event_loop().call_later(1, self._check_config_file_change) - - def _check_config_file_change(self): + def reload(self): """ - Check if configuration file has changed on the disk + Reload configuration """ - changed = False - for file in self._watched_files: - try: - if os.stat(file).st_mtime != self._watched_files[file]: - changed = True - except OSError: - continue - if changed: - self.read_config() + self.read_config() for section in self._override_config: self.set_section_config(section, self._override_config[section]) - self._watch_config_file() def list_cloud_config_file(self): return self._cloud_file diff --git a/gns3server/handlers/__init__.py b/gns3server/handlers/__init__.py index 3db429a1..325e8ba4 100644 --- a/gns3server/handlers/__init__.py +++ b/gns3server/handlers/__init__.py @@ -25,6 +25,7 @@ from gns3server.handlers.api.dynamips_vm_handler import DynamipsVMHandler from gns3server.handlers.api.qemu_handler import QEMUHandler from gns3server.handlers.api.virtualbox_handler import VirtualBoxHandler from gns3server.handlers.api.vpcs_handler import VPCSHandler +from gns3server.handlers.api.config_handler import ConfigHandler from gns3server.handlers.upload_handler import UploadHandler if sys.platform.startswith("linux") or hasattr(sys, "_called_from_test"): diff --git a/gns3server/handlers/api/config_handler.py b/gns3server/handlers/api/config_handler.py new file mode 100644 index 00000000..0025731e --- /dev/null +++ b/gns3server/handlers/api/config_handler.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 . + +from ...web.route import Route +from ...config import Config +from aiohttp.web import HTTPForbidden + +import asyncio + + +class ConfigHandler: + + @classmethod + @Route.post( + r"/config/reload", + description="Check if version is the same as the server", + status_codes={ + 201: "Config reload", + 403: "Config reload refused" + }) + def reload(request, response): + + config = Config.instance() + if config.get_section_config("Server").getboolean("local", False) is False: + raise HTTPForbidden(text="You can only reload the configuration for a local server") + config.reload() + response.set_status(201) diff --git a/tests/handlers/api/test_config.py b/tests/handlers/api/test_config.py new file mode 100644 index 00000000..b081ca13 --- /dev/null +++ b/tests/handlers/api/test_config.py @@ -0,0 +1,48 @@ +# -*- 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 configparser import ConfigParser +from unittest.mock import patch, MagicMock + + +def test_reload_accepted(server): + + gns_config = MagicMock() + config = ConfigParser() + config.add_section("Server") + config.set("Server", "local", "true") + gns_config.get_section_config.return_value = config["Server"] + + with patch("gns3server.config.Config.instance", return_value=gns_config): + response = server.post('/config/reload', example=True) + + assert response.status == 201 + assert gns_config.reload.called + + +def test_reload_forbidden(server): + + gns_config = MagicMock() + config = ConfigParser() + config.add_section("Server") + config.set("Server", "local", "false") + gns_config.get_section_config.return_value = config["Server"] + + with patch("gns3server.config.Config.instance", return_value=gns_config): + response = server.post('/config/reload') + + assert response.status == 403 diff --git a/tests/test_config.py b/tests/test_config.py index d12683b1..3f119619 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -77,7 +77,7 @@ def test_set_section_config(tmpdir): assert dict(config.get_section_config("Server")) == {"host": "192.168.1.1"} -def test_check_config_file_change(tmpdir): +def test_reload(tmpdir): config = load_config(tmpdir, { "Server": { @@ -91,13 +91,12 @@ def test_check_config_file_change(tmpdir): "host": "192.168.1.1" } }) - os.utime(path, (time.time() + 1, time.time() + 1)) - config._check_config_file_change() + config.reload() assert dict(config.get_section_config("Server")) == {"host": "192.168.1.1"} -def test_check_config_file_change_override_cmdline(tmpdir): +def test_reload(tmpdir): config = load_config(tmpdir, { "Server": { @@ -114,7 +113,6 @@ def test_check_config_file_change_override_cmdline(tmpdir): "host": "192.168.1.2" } }) - os.utime(path, (time.time() + 1, time.time() + 1)) - config._check_config_file_change() + config.reload() assert dict(config.get_section_config("Server")) == {"host": "192.168.1.1"} From b673b898a8c2ea0f9b7074f51d1a7ad5a3555c9e Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 2 Mar 2015 13:04:30 -0700 Subject: [PATCH 369/485] Fixes problem when trying to convert VirtualBox projects without cloned VMs. --- gns3server/modules/base_manager.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index d2d1c7f1..f6b43175 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -181,12 +181,13 @@ class BaseManager: legacy_vm_dir = self.get_legacy_vm_workdir(legacy_id, name) legacy_vm_working_path = os.path.join(new_project_files_path, legacy_vm_dir) new_vm_working_path = os.path.join(new_project_files_path, self.module_name.lower(), new_id) - try: - log.info('Moving "{}" to "{}"'.format(legacy_vm_working_path, new_vm_working_path)) - yield from wait_run_in_executor(shutil.move, legacy_vm_working_path, new_vm_working_path) - except OSError as e: - raise aiohttp.web.HTTPInternalServerError(text="Could not move VM working directory: {} to {} {}".format(legacy_vm_working_path, - new_vm_working_path, e)) + if os.path.exists(legacy_vm_working_path) and not os.path.exists(new_vm_working_path): + try: + log.info('Moving "{}" to "{}"'.format(legacy_vm_working_path, new_vm_working_path)) + yield from wait_run_in_executor(shutil.move, legacy_vm_working_path, new_vm_working_path) + except OSError as e: + raise aiohttp.web.HTTPInternalServerError(text="Could not move VM working directory: {} to {} {}".format(legacy_vm_working_path, + new_vm_working_path, e)) return new_id From 7ace6fc7e91be99b3d1d0cf709b04c5479336b48 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 2 Mar 2015 14:37:48 -0700 Subject: [PATCH 370/485] Fixes old projects loading issue with Qemu. --- gns3server/schemas/qemu.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/gns3server/schemas/qemu.py b/gns3server/schemas/qemu.py index 5c55c00d..30b62601 100644 --- a/gns3server/schemas/qemu.py +++ b/gns3server/schemas/qemu.py @@ -22,9 +22,14 @@ QEMU_CREATE_SCHEMA = { "type": "object", "properties": { "vm_id": { - "description": "QEMU VM UUID", - "type": ["string", "null"], - "minLength": 1, + "description": "QEMU VM identifier", + "oneOf": [ + {"type": "string", + "minLength": 36, + "maxLength": 36, + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$"}, + {"type": "integer"} # for legacy projects + ] }, "name": { "description": "QEMU VM instance name", From f269ec952751e0de4d714bc45548afb6df7958bd Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 2 Mar 2015 16:34:28 -0700 Subject: [PATCH 371/485] Fixes Qemu networking. --- gns3server/handlers/api/qemu_handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gns3server/handlers/api/qemu_handler.py b/gns3server/handlers/api/qemu_handler.py index 74102dbe..7905be0a 100644 --- a/gns3server/handlers/api/qemu_handler.py +++ b/gns3server/handlers/api/qemu_handler.py @@ -254,7 +254,7 @@ class QEMUHandler: qemu_manager = Qemu.instance() vm = qemu_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) nio = qemu_manager.create_nio(vm.qemu_path, request.json) - vm.adapter_add_nio_binding(int(request.match_info["adapter_number"]), int(request.match_info["port_number"]), nio) + yield from vm.adapter_add_nio_binding(int(request.match_info["adapter_number"]), int(request.match_info["port_number"]), nio) response.set_status(201) response.json(nio) @@ -277,7 +277,7 @@ class QEMUHandler: qemu_manager = Qemu.instance() vm = qemu_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) - vm.adapter_remove_nio_binding(int(request.match_info["adapter_number"]), int(request.match_info["port_number"])) + yield from vm.adapter_remove_nio_binding(int(request.match_info["adapter_number"]), int(request.match_info["port_number"])) response.set_status(204) @classmethod From a6869379c3c1485f706a087f2b7b4f88fdf93627 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 2 Mar 2015 17:28:28 -0700 Subject: [PATCH 372/485] Fixes console restoration when loading a VirtualBox project. --- gns3server/handlers/api/virtualbox_handler.py | 7 +++++++ gns3server/modules/virtualbox/virtualbox_vm.py | 12 +++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/gns3server/handlers/api/virtualbox_handler.py b/gns3server/handlers/api/virtualbox_handler.py index be75c4cf..93d6a039 100644 --- a/gns3server/handlers/api/virtualbox_handler.py +++ b/gns3server/handlers/api/virtualbox_handler.py @@ -67,8 +67,12 @@ class VirtualBoxHandler: request.json.get("vm_id"), request.json.pop("vmname"), request.json.pop("linked_clone"), + console=request.json.get("console", None), adapters=request.json.get("adapters", 0)) + if "enable_remote_console" in request.json: + yield from vm.set_enable_remote_console(request.json.pop("enable_remote_console")) + for name, value in request.json.items(): if hasattr(vm, name) and getattr(vm, name) != value: setattr(vm, name, value) @@ -117,6 +121,9 @@ class VirtualBoxHandler: vbox_manager = VirtualBox.instance() vm = vbox_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + if "enable_remote_console" in request.json: + yield from vm.set_enable_remote_console(request.json.pop("enable_remote_console")) + for name, value in request.json.items(): if hasattr(vm, name) and getattr(vm, name) != value: setattr(vm, name, value) diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index a7b61fe6..0d93d281 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -49,9 +49,9 @@ class VirtualBoxVM(BaseVM): VirtualBox VM implementation. """ - def __init__(self, name, vm_id, project, manager, vmname, linked_clone, adapters=0): + def __init__(self, name, vm_id, project, manager, vmname, linked_clone, console=None, adapters=0): - super().__init__(name, vm_id, project, manager) + super().__init__(name, vm_id, project, manager, console=console) self._maximum_adapters = 8 self._linked_clone = linked_clone @@ -389,8 +389,8 @@ class VirtualBoxVM(BaseVM): return self._enable_remote_console - @enable_remote_console.setter - def enable_remote_console(self, enable_remote_console): + @asyncio.coroutine + def set_enable_remote_console(self, enable_remote_console): """ Sets either the console is enabled or not @@ -399,7 +399,9 @@ class VirtualBoxVM(BaseVM): if enable_remote_console: log.info("VirtualBox VM '{name}' [{id}] has enabled the console".format(name=self.name, id=self.id)) - self._start_remote_console() + vm_state = yield from self._get_vm_state() + if vm_state == "running": + self._start_remote_console() else: log.info("VirtualBox VM '{name}' [{id}] has disabled the console".format(name=self.name, id=self.id)) self._stop_remote_console() From 3472f195193334e755b6cb8102e5298d33a66f68 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 2 Mar 2015 18:19:11 -0700 Subject: [PATCH 373/485] Use console_host from the PortManager. --- gns3server/modules/iou/iou_vm.py | 5 +---- gns3server/modules/qemu/qemu_vm.py | 9 +++------ 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index ade3aea4..42a7cedc 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -58,7 +58,6 @@ class IOUVM(BaseVM): :param project: Project instance :param manager: parent VM Manager :param console: TCP console port - :params console_host: TCP console host IP :params ethernet_adapters: Number of ethernet adapters :params serial_adapters: Number of serial adapters :params ram: Ram MB @@ -69,7 +68,6 @@ class IOUVM(BaseVM): def __init__(self, name, vm_id, project, manager, console=None, - console_host="0.0.0.0", ram=None, nvram=None, ethernet_adapters=None, @@ -86,7 +84,6 @@ class IOUVM(BaseVM): self._started = False self._path = None self._ioucon_thread = None - self._console_host = console_host # IOU settings self._ethernet_adapters = [] @@ -660,7 +657,7 @@ class IOUVM(BaseVM): """ if not self._ioucon_thread: - telnet_server = "{}:{}".format(self._console_host, self.console) + telnet_server = "{}:{}".format(self._manager.port_manager.console_host, self.console) log.info("Starting ioucon for IOU instance {} to accept Telnet connections on {}".format(self._name, telnet_server)) args = argparse.Namespace(appl_id=str(self.application_id), debug=False, escape='^^', telnet_limit=0, telnet_server=telnet_server) self._ioucon_thread_stop_event = threading.Event() diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index d42e23e1..90a45473 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -52,7 +52,6 @@ class QemuVM(BaseVM): :param host: host/address to bind for console and UDP connections :param qemu_id: QEMU VM instance ID :param console: TCP console port - :param console_host: IP address to bind for console connections :param monitor: TCP monitor port :param monitor_host: IP address to bind for monitor connections """ @@ -65,14 +64,12 @@ class QemuVM(BaseVM): qemu_path=None, host="127.0.0.1", console=None, - console_host="0.0.0.0", monitor=None, - monitor_host="0.0.0.0"): + monitor_host="127.0.0.1"): super().__init__(name, vm_id, project, manager, console=console) self._host = host - self._console_host = console_host self._command = [] self._started = False self._process = None @@ -81,7 +78,7 @@ class QemuVM(BaseVM): self._monitor_host = monitor_host # QEMU settings - self.qemu_path = qemu_path + self._qemu_path = qemu_path self._hda_disk_image = "" self._hdb_disk_image = "" self._options = "" @@ -824,7 +821,7 @@ class QemuVM(BaseVM): def _serial_options(self): if self._console: - return ["-serial", "telnet:{}:{},server,nowait".format(self._console_host, self._console)] + return ["-serial", "telnet:{}:{},server,nowait".format(self._manager.port_manager.console_host, self._console)] else: return [] From 6e89f2c7c7f746ec96337a183a5af02df9cd1dd5 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 2 Mar 2015 19:17:13 -0700 Subject: [PATCH 374/485] Remove console_host from IOU and Qemu handlers. --- gns3server/handlers/api/iou_handler.py | 1 - gns3server/handlers/api/qemu_handler.py | 3 --- 2 files changed, 4 deletions(-) diff --git a/gns3server/handlers/api/iou_handler.py b/gns3server/handlers/api/iou_handler.py index 63a18a42..cd90fb2a 100644 --- a/gns3server/handlers/api/iou_handler.py +++ b/gns3server/handlers/api/iou_handler.py @@ -56,7 +56,6 @@ class IOUHandler: request.match_info["project_id"], request.json.get("vm_id"), console=request.json.get("console"), - console_host=PortManager.instance().console_host, serial_adapters=request.json.get("serial_adapters"), ethernet_adapters=request.json.get("ethernet_adapters"), ram=request.json.get("ram"), diff --git a/gns3server/handlers/api/qemu_handler.py b/gns3server/handlers/api/qemu_handler.py index 7905be0a..85425568 100644 --- a/gns3server/handlers/api/qemu_handler.py +++ b/gns3server/handlers/api/qemu_handler.py @@ -19,7 +19,6 @@ import os from ...web.route import Route -from ...modules.port_manager import PortManager from ...schemas.qemu import QEMU_CREATE_SCHEMA from ...schemas.qemu import QEMU_UPDATE_SCHEMA from ...schemas.qemu import QEMU_OBJECT_SCHEMA @@ -57,8 +56,6 @@ class QEMUHandler: qemu_path=request.json.get("qemu_path"), console=request.json.get("console"), monitor=request.json.get("monitor"), - console_host=PortManager.instance().console_host, - monitor_host=PortManager.instance().console_host, ) # Clear already used keys map(request.json.__delitem__, ["name", "project_id", "vm_id", From 6208cb997d13fd68cb21691fa0a956de3d3bb1ce Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 2 Mar 2015 19:59:44 -0700 Subject: [PATCH 375/485] Fixes Qemu adapters support. --- gns3server/modules/qemu/qemu_vm.py | 40 ++++++++++-------------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index 90a45473..85a9537f 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -131,24 +131,6 @@ class QemuVM(BaseVM): id=self.id, port=monitor)) - def delete(self): - """ - Deletes this QEMU VM. - """ - - self.stop() - if self._id in self._instances: - self._instances.remove(self._id) - - if self._console and self._console in self._allocated_console_ports: - self._allocated_console_ports.remove(self._console) - - if self._monitor and self._monitor in self._allocated_monitor_ports: - self._allocated_monitor_ports.remove(self._monitor) - - log.info("QEMU VM {name} [id={id}] has been deleted".format(name=self._name, - id=self._id)) - @property def qemu_path(self): """ @@ -925,9 +907,11 @@ class QemuVM(BaseVM): network_options = [] adapter_id = 0 for adapter in self._ethernet_adapters: - # TODO: let users specify a base mac address mac = self._get_random_mac(adapter_id) - network_options.extend(["-net", "nic,vlan={},macaddr={},model={}".format(adapter_id, mac, self._adapter_type)]) + if self._legacy_networking: + network_options.extend(["-net", "nic,vlan={},macaddr={},model={}".format(adapter_id, mac, self._adapter_type)]) + else: + network_options.extend(["-device", "{},mac={},netdev=gns3-{}".format(self._adapter_type, mac, adapter_id)]) nio = adapter.get_nio(0) if nio and isinstance(nio, NIOUDP): if self._legacy_networking: @@ -937,14 +921,16 @@ class QemuVM(BaseVM): nio.rport, nio.rhost)]) else: - network_options.extend(["-net", "socket,vlan={},name=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_id, - adapter_id, - nio.rhost, - nio.rport, - self._host, - nio.lport)]) + network_options.extend(["-netdev", "socket,id=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_id, + nio.rhost, + nio.rport, + self._host, + nio.lport)]) else: - network_options.extend(["-net", "user,vlan={},name=gns3-{}".format(adapter_id, adapter_id)]) + if self._legacy_networking: + network_options.extend(["-net", "user,vlan={},name=gns3-{}".format(adapter_id, adapter_id)]) + else: + network_options.extend(["-netdev", "user,id=gns3-{}".format(adapter_id)]) adapter_id += 1 return network_options From 16dc0d1a8a3d69233a704f9dc29ee5e3396219b7 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 3 Mar 2015 12:41:30 +0100 Subject: [PATCH 376/485] Send crash report synchronous to avoid lost of events --- gns3server/crash_report.py | 8 +++----- gns3server/server.py | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py index e70a0e55..49f7f404 100644 --- a/gns3server/crash_report.py +++ b/gns3server/crash_report.py @@ -18,6 +18,7 @@ import raven import json import asyncio.futures +import asyncio from .version import __version__ from .config import Config @@ -32,7 +33,7 @@ class CrashReport: Report crash to a third party service """ - DSN = "aiohttp+https://50af75d8641d4ea7a4ea6b38c7df6cf9:41d54936f8f14e558066262e2ec8bbeb@app.getsentry.com/38482" + DSN = "sync+https://50af75d8641d4ea7a4ea6b38c7df6cf9:41d54936f8f14e558066262e2ec8bbeb@app.getsentry.com/38482" _instance = None def __init__(self): @@ -49,10 +50,7 @@ class CrashReport: "url": request.path, "data": request.json, }) - try: - self._client.captureException() - except asyncio.futures.TimeoutError: - pass # We don't care if we can send the bug report + self._client.captureException() @classmethod def instance(cls): diff --git a/gns3server/server.py b/gns3server/server.py index 69e95949..be677676 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -130,7 +130,7 @@ class Server: if modified > self._start_time: log.debug("File {} has been modified".format(path)) asyncio.async(reload()) - self._loop.call_later(1, self._reload_hook) + self._loop.call_later(1, self._reload_hook, handler) def _create_ssl_context(self, server_config): From 10296f4f193b111849978a9639426fbe6bb7d7c6 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 3 Mar 2015 13:04:30 +0100 Subject: [PATCH 377/485] Do not send garbage to console in case of sentry not available --- gns3server/crash_report.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py index 49f7f404..099ab28a 100644 --- a/gns3server/crash_report.py +++ b/gns3server/crash_report.py @@ -43,14 +43,17 @@ class CrashReport: server_config = Config.instance().get_section_config("Server") if server_config.getboolean("report_errors"): if self._client is None: - self._client = raven.Client(CrashReport.DSN, release=__version__) + self._client = raven.Client(CrashReport.DSN, release=__version__, raise_send_errors=True) if request is not None: self._client.http_context({ "method": request.method, "url": request.path, "data": request.json, }) - self._client.captureException() + try: + self._client.captureException() + except Exception as e: + log.error("Can't send crash report to Sentry: %s", e) @classmethod def instance(cls): From 80fd85765883b0b108f8581eed21e440fe97b861 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 3 Mar 2015 14:37:34 +0100 Subject: [PATCH 378/485] Fix tests --- gns3server/modules/port_manager.py | 2 +- tests/handlers/api/test_qemu.py | 3 +++ tests/modules/qemu/test_qemu_vm.py | 15 ++++++++------- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/gns3server/modules/port_manager.py b/gns3server/modules/port_manager.py index 79b8b6eb..f38e2ea3 100644 --- a/gns3server/modules/port_manager.py +++ b/gns3server/modules/port_manager.py @@ -80,7 +80,7 @@ class PortManager: return self._console_host @console_host.setter - def host(self, new_host): + def console_host(self, new_host): self._console_host = new_host diff --git a/tests/handlers/api/test_qemu.py b/tests/handlers/api/test_qemu.py index b32cd945..12a72e87 100644 --- a/tests/handlers/api/test_qemu.py +++ b/tests/handlers/api/test_qemu.py @@ -133,6 +133,7 @@ def test_qemu_update(server, vm, tmpdir, free_console_port, project): def test_qemu_nio_create_udp(server, vm): + server.put("/projects/{project_id}/qemu/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"adapters": 2}) response = server.post("/projects/{project_id}/qemu/vms/{vm_id}/adapters/1/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", "lport": 4242, "rport": 4343, @@ -144,6 +145,7 @@ def test_qemu_nio_create_udp(server, vm): def test_qemu_nio_create_ethernet(server, vm): + server.put("/projects/{project_id}/qemu/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"adapters": 2}) response = server.post("/projects/{project_id}/qemu/vms/{vm_id}/adapters/1/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_generic_ethernet", "ethernet_device": "eth0", }, @@ -155,6 +157,7 @@ def test_qemu_nio_create_ethernet(server, vm): def test_qemu_delete_nio(server, vm): + server.put("/projects/{project_id}/qemu/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"adapters": 2}) server.post("/projects/{project_id}/qemu/vms/{vm_id}/adapters/1/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", "lport": 4242, "rport": 4343, diff --git a/tests/modules/qemu/test_qemu_vm.py b/tests/modules/qemu/test_qemu_vm.py index 76b4a97c..c0b6d1ee 100644 --- a/tests/modules/qemu/test_qemu_vm.py +++ b/tests/modules/qemu/test_qemu_vm.py @@ -59,6 +59,7 @@ def fake_qemu_binary(): @pytest.fixture(scope="function") def vm(project, manager, fake_qemu_binary, fake_qemu_img_binary): + manager.port_manager.console_host = "127.0.0.1" return QemuVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager, qemu_path=fake_qemu_binary) @@ -252,7 +253,7 @@ def test_control_vm_expect_text(vm, loop, running_subprocess_mock): assert res == "epic product" -def test_build_command(vm, loop, fake_qemu_binary): +def test_build_command(vm, loop, fake_qemu_binary, port_manager): os.environ["DISPLAY"] = "0:0" with patch("gns3server.modules.qemu.qemu_vm.QemuVM._get_random_mac", return_value="00:00:ab:7e:b5:00"): @@ -267,13 +268,13 @@ def test_build_command(vm, loop, fake_qemu_binary): "-hda", os.path.join(vm.working_dir, "flash.qcow2"), "-serial", - "telnet:0.0.0.0:{},server,nowait".format(vm.console), + "telnet:127.0.0.1:{},server,nowait".format(vm.console), "-monitor", - "telnet:0.0.0.0:{},server,nowait".format(vm.monitor), - "-net", - "nic,vlan=0,macaddr=00:00:ab:7e:b5:00,model=e1000", - "-net", - "user,vlan=0,name=gns3-0" + "telnet:127.0.0.1:{},server,nowait".format(vm.monitor), + "-device", + "e1000,mac=00:00:ab:7e:b5:00,netdev=gns3-0", + "-netdev", + "user,id=gns3-0" ] From 4c2dbbbebc3f23879899af05f87189e879063f9b Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 3 Mar 2015 18:43:17 +0100 Subject: [PATCH 379/485] Changelog --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index df657600..2dbdafdd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ # Change Log -## Unreleased +## 1.3alpha 1 03/03/2015 * HTTP Rest API instead of WebSocket * API documentation From 69f8b7de6a278b4a021f3bb4dee043c7647a4b70 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 3 Mar 2015 10:43:44 -0700 Subject: [PATCH 380/485] Bump to version 1.3alpha1 --- gns3server/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gns3server/version.py b/gns3server/version.py index 978385bb..066ccef9 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,5 +23,5 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "1.3.dev3" -__version_info__ = (1, 3, 0, 0) +__version__ = "1.3alpha1" +__version_info__ = (1, 3, 0, -99) From 94bcd1cf11e317e69e944bb5039f7188b55d93b1 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 3 Mar 2015 10:47:02 -0700 Subject: [PATCH 381/485] Bump to version 1.3.0alpha1 --- gns3server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/version.py b/gns3server/version.py index 066ccef9..27c59e37 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,5 +23,5 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "1.3alpha1" +__version__ = "1.3.0alpha1" __version_info__ = (1, 3, 0, -99) From bae5b6edb4de1e40ee64961968bf12255ee6b697 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 3 Mar 2015 18:46:34 +0100 Subject: [PATCH 382/485] Fix version notation in changelog --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 2dbdafdd..6ef0aca3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ # Change Log -## 1.3alpha 1 03/03/2015 +## 1.3.0alpha 1 03/03/2015 * HTTP Rest API instead of WebSocket * API documentation From 7a6136ed14a14e8eb9f69abb7d4034d5fc555bae Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 4 Mar 2015 18:24:15 -0700 Subject: [PATCH 383/485] Fixes adapter bug with VirtualBox. --- gns3server/handlers/api/virtualbox_handler.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gns3server/handlers/api/virtualbox_handler.py b/gns3server/handlers/api/virtualbox_handler.py index 93d6a039..441ab6df 100644 --- a/gns3server/handlers/api/virtualbox_handler.py +++ b/gns3server/handlers/api/virtualbox_handler.py @@ -124,17 +124,17 @@ class VirtualBoxHandler: if "enable_remote_console" in request.json: yield from vm.set_enable_remote_console(request.json.pop("enable_remote_console")) + if "adapters" in request.json: + adapters = int(request.json.pop("adapters")) + if adapters != vm.adapters: + yield from vm.set_adapters(adapters) + for name, value in request.json.items(): if hasattr(vm, name) and getattr(vm, name) != value: setattr(vm, name, value) if name == "vmname": yield from vm.rename_in_virtualbox() - if "adapters" in request.json: - adapters = int(request.json["adapters"]) - if adapters != vm.adapters: - yield from vm.set_adapters(adapters) - response.json(vm) @classmethod From 3407ba802e295d4c65321b9b70ca3ea656db00a5 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 5 Mar 2015 17:00:25 +0100 Subject: [PATCH 384/485] Rename vlan dat file for IOU --- gns3server/modules/iou/iou_vm.py | 3 +++ tests/modules/iou/test_iou_vm.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 42a7cedc..e80c99a3 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -379,6 +379,9 @@ class IOUVM(BaseVM): destination = os.path.join(self.working_dir, "nvram_{:05d}".format(self.application_id)) for file_path in glob.glob(os.path.join(self.working_dir, "nvram_*")): shutil.move(file_path, destination) + destination = os.path.join(self.working_dir, "vlan.dat-{:05d}".format(self.application_id)) + for file_path in glob.glob(os.path.join(self.working_dir, "vlan.dat-*")): + shutil.move(file_path, destination) @asyncio.coroutine def _start_iouyap(self): diff --git a/tests/modules/iou/test_iou_vm.py b/tests/modules/iou/test_iou_vm.py index 79b2d5c5..045b9814 100644 --- a/tests/modules/iou/test_iou_vm.py +++ b/tests/modules/iou/test_iou_vm.py @@ -116,8 +116,12 @@ def test_rename_nvram_file(loop, vm, monkeypatch): with open(os.path.join(vm.working_dir, "nvram_0000{}".format(vm.application_id + 1)), 'w+') as f: f.write("1") + with open(os.path.join(vm.working_dir, "vlan.dat-0000{}".format(vm.application_id + 1)), 'w+') as f: + f.write("1") + vm._rename_nvram_file() assert os.path.exists(os.path.join(vm.working_dir, "nvram_0000{}".format(vm.application_id))) + assert os.path.exists(os.path.join(vm.working_dir, "vlan.dat-0000{}".format(vm.application_id))) def test_stop(loop, vm): From 83c1ada63eecdaea4fc798277f38daa7a07518d7 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 5 Mar 2015 17:15:16 +0100 Subject: [PATCH 385/485] Drop unused cloud code, this cleanup the dependencies --- gns3dms/__init__.py | 26 -- gns3dms/cloud/__init__.py | 0 gns3dms/cloud/base_cloud_ctrl.py | 340 ------------------------ gns3dms/cloud/exceptions.py | 67 ----- gns3dms/cloud/rackspace_ctrl.py | 259 ------------------- gns3dms/main.py | 402 ----------------------------- gns3dms/modules/__init__.py | 24 -- gns3dms/modules/daemon.py | 144 ----------- gns3dms/modules/rackspace_cloud.py | 73 ------ gns3dms/version.py | 27 -- requirements.txt | 2 - setup.py | 2 - 12 files changed, 1366 deletions(-) delete mode 100644 gns3dms/__init__.py delete mode 100644 gns3dms/cloud/__init__.py delete mode 100644 gns3dms/cloud/base_cloud_ctrl.py delete mode 100644 gns3dms/cloud/exceptions.py delete mode 100644 gns3dms/cloud/rackspace_ctrl.py delete mode 100644 gns3dms/main.py delete mode 100644 gns3dms/modules/__init__.py delete mode 100644 gns3dms/modules/daemon.py delete mode 100644 gns3dms/modules/rackspace_cloud.py delete mode 100644 gns3dms/version.py diff --git a/gns3dms/__init__.py b/gns3dms/__init__.py deleted file mode 100644 index cf426f79..00000000 --- a/gns3dms/__init__.py +++ /dev/null @@ -1,26 +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 . - -# __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 .version import __version__ diff --git a/gns3dms/cloud/__init__.py b/gns3dms/cloud/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/gns3dms/cloud/base_cloud_ctrl.py b/gns3dms/cloud/base_cloud_ctrl.py deleted file mode 100644 index 0ad74af1..00000000 --- a/gns3dms/cloud/base_cloud_ctrl.py +++ /dev/null @@ -1,340 +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 . - -""" -Base cloud controller class. - -Base class for interacting with Cloud APIs to create and manage cloud -instances. - -""" -from collections import namedtuple -import hashlib -import os -import logging -from io import StringIO, BytesIO - -from libcloud.compute.base import NodeAuthSSHKey -from libcloud.storage.types import ContainerAlreadyExistsError, ContainerDoesNotExistError, ObjectDoesNotExistError - -from .exceptions import ItemNotFound, KeyPairExists, MethodNotAllowed -from .exceptions import OverLimit, BadRequest, ServiceUnavailable -from .exceptions import Unauthorized, ApiError - - -KeyPair = namedtuple("KeyPair", ['name'], verbose=False) -log = logging.getLogger(__name__) - - -def parse_exception(exception): - """ - Parse the exception to separate the HTTP status code from the text. - - Libcloud raises many exceptions of the form: - Exception(" ") - - in lieu of raising specific incident-based exceptions. - - """ - - e_str = str(exception) - - try: - status = int(e_str[0:3]) - error_text = e_str[3:] - - except ValueError: - status = None - error_text = e_str - - return status, error_text - - -class BaseCloudCtrl(object): - - """ Base class for interacting with a cloud provider API. """ - - http_status_to_exception = { - 400: BadRequest, - 401: Unauthorized, - 404: ItemNotFound, - 405: MethodNotAllowed, - 413: OverLimit, - 500: ApiError, - 503: ServiceUnavailable - } - - GNS3_CONTAINER_NAME = 'GNS3' - - def __init__(self, username, api_key): - self.username = username - self.api_key = api_key - - def _handle_exception(self, status, error_text, response_overrides=None): - """ Raise an exception based on the HTTP status. """ - - if response_overrides: - if status in response_overrides: - raise response_overrides[status](error_text) - - raise self.http_status_to_exception[status](error_text) - - def authenticate(self): - """ Validate cloud account credentials. Return boolean. """ - raise NotImplementedError - - def list_sizes(self): - """ Return a list of NodeSize objects. """ - - return self.driver.list_sizes() - - def list_flavors(self): - """ Return an iterable of flavors """ - - raise NotImplementedError - - def create_instance(self, name, size_id, image_id, keypair): - """ - Create a new instance with the supplied attributes. - - Return a Node object. - - """ - try: - image = self.get_image(image_id) - if image is None: - raise ItemNotFound("Image not found") - - size = self.driver.ex_get_size(size_id) - - args = { - "name": name, - "size": size, - "image": image, - } - - if keypair is not None: - auth_key = NodeAuthSSHKey(keypair.public_key) - args["auth"] = auth_key - args["ex_keyname"] = name - - return self.driver.create_node(**args) - - except Exception as e: - status, error_text = parse_exception(e) - - if status: - self._handle_exception(status, error_text) - else: - log.error("create_instance method raised an exception: {}".format(e)) - log.error('image id {}'.format(image)) - - def delete_instance(self, instance): - """ Delete the specified instance. Returns True or False. """ - - try: - return self.driver.destroy_node(instance) - - except Exception as e: - - status, error_text = parse_exception(e) - - if status: - self._handle_exception(status, error_text) - else: - raise e - - def get_instance(self, instance): - """ Return a Node object representing the requested instance. """ - - for i in self.driver.list_nodes(): - if i.id == instance.id: - return i - - raise ItemNotFound("Instance not found") - - def list_instances(self): - """ Return a list of instances in the current region. """ - - try: - return self.driver.list_nodes() - except Exception as e: - log.error("list_instances returned an error: {}".format(e)) - - def create_key_pair(self, name): - """ Create and return a new Key Pair. """ - - response_overrides = { - 409: KeyPairExists - } - try: - return self.driver.create_key_pair(name) - - except Exception as e: - status, error_text = parse_exception(e) - if status: - self._handle_exception(status, error_text, response_overrides) - else: - raise e - - def delete_key_pair(self, keypair): - """ Delete the keypair. Returns True or False. """ - - try: - return self.driver.delete_key_pair(keypair) - - except Exception as e: - status, error_text = parse_exception(e) - if status: - self._handle_exception(status, error_text) - else: - raise e - - def delete_key_pair_by_name(self, keypair_name): - """ Utility method to incapsulate boilerplate code """ - - kp = KeyPair(name=keypair_name) - return self.delete_key_pair(kp) - - def list_key_pairs(self): - """ Return a list of Key Pairs. """ - - return self.driver.list_key_pairs() - - def upload_file(self, file_path, cloud_object_name): - """ - Uploads file to cloud storage (if it is not identical to a file already in cloud storage). - :param file_path: path to file to upload - :param cloud_object_name: name of file saved in cloud storage - :return: True if file was uploaded, False if it was skipped because it already existed and was identical - """ - try: - gns3_container = self.storage_driver.create_container(self.GNS3_CONTAINER_NAME) - except ContainerAlreadyExistsError: - gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME) - - with open(file_path, 'rb') as file: - local_file_hash = hashlib.md5(file.read()).hexdigest() - - cloud_hash_name = cloud_object_name + '.md5' - cloud_objects = [obj.name for obj in gns3_container.list_objects()] - - # if the file and its hash are in object storage, and the local and storage file hashes match - # do not upload the file, otherwise upload it - if cloud_object_name in cloud_objects and cloud_hash_name in cloud_objects: - hash_object = gns3_container.get_object(cloud_hash_name) - cloud_object_hash = '' - for chunk in hash_object.as_stream(): - cloud_object_hash += chunk.decode('utf8') - - if cloud_object_hash == local_file_hash: - return False - - file.seek(0) - self.storage_driver.upload_object_via_stream(file, gns3_container, cloud_object_name) - self.storage_driver.upload_object_via_stream(StringIO(local_file_hash), gns3_container, cloud_hash_name) - return True - - def list_projects(self): - """ - Lists projects in cloud storage - :return: Dictionary where project names are keys and values are names of objects in storage - """ - - try: - gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME) - projects = { - obj.name.replace('projects/', '').replace('.zip', ''): obj.name - for obj in gns3_container.list_objects() - if obj.name.startswith('projects/') and obj.name[-4:] == '.zip' - } - return projects - except ContainerDoesNotExistError: - return [] - - def download_file(self, file_name, destination=None): - """ - Downloads file from cloud storage. If a file exists at destination, and it is identical to the file in cloud - storage, it is not downloaded. - :param file_name: name of file in cloud storage to download - :param destination: local path to save file to (if None, returns file contents as a file-like object) - :return: A file-like object if file contents are returned, or None if file is saved to filesystem - """ - - gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME) - storage_object = gns3_container.get_object(file_name) - - if destination is not None: - if os.path.isfile(destination): - # if a file exists at destination and its hash matches that of the - # file in cloud storage, don't download it - with open(destination, 'rb') as f: - local_file_hash = hashlib.md5(f.read()).hexdigest() - - hash_object = gns3_container.get_object(file_name + '.md5') - cloud_object_hash = '' - for chunk in hash_object.as_stream(): - cloud_object_hash += chunk.decode('utf8') - - if local_file_hash == cloud_object_hash: - return - - storage_object.download(destination) - else: - contents = b'' - - for chunk in storage_object.as_stream(): - contents += chunk - - return BytesIO(contents) - - def find_storage_image_names(self, images_to_find): - """ - Maps names of image files to their full name in cloud storage - :param images_to_find: list of image names to find - :return: A dictionary where keys are image names, and values are the corresponding names of - the files in cloud storage - """ - gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME) - images_in_storage = [obj.name for obj in gns3_container.list_objects() if obj.name.startswith('images/')] - - images = {} - for image_name in images_to_find: - images_with_same_name =\ - list(filter(lambda storage_image_name: storage_image_name.endswith(image_name), images_in_storage)) - - if len(images_with_same_name) == 1: - images[image_name] = images_with_same_name[0] - else: - raise Exception('Image does not exist in cloud storage or is duplicated') - - return images - - def delete_file(self, file_name): - gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME) - - try: - object_to_delete = gns3_container.get_object(file_name) - object_to_delete.delete() - except ObjectDoesNotExistError: - pass - - try: - hash_object = gns3_container.get_object(file_name + '.md5') - hash_object.delete() - except ObjectDoesNotExistError: - pass diff --git a/gns3dms/cloud/exceptions.py b/gns3dms/cloud/exceptions.py deleted file mode 100644 index 65d65f9f..00000000 --- a/gns3dms/cloud/exceptions.py +++ /dev/null @@ -1,67 +0,0 @@ -""" Exception classes for CloudCtrl classes. """ - - -class ApiError(Exception): - - """ Raised when the server returns 500 Compute Error. """ - pass - - -class BadRequest(Exception): - - """ Raised when the server returns 400 Bad Request. """ - pass - - -class ComputeFault(Exception): - - """ Raised when the server returns 400|500 Compute Fault. """ - pass - - -class Forbidden(Exception): - - """ Raised when the server returns 403 Forbidden. """ - pass - - -class ItemNotFound(Exception): - - """ Raised when the server returns 404 Not Found. """ - pass - - -class KeyPairExists(Exception): - - """ Raised when the server returns 409 Conflict Key pair exists. """ - pass - - -class MethodNotAllowed(Exception): - - """ Raised when the server returns 405 Method Not Allowed. """ - pass - - -class OverLimit(Exception): - - """ Raised when the server returns 413 Over Limit. """ - pass - - -class ServerCapacityUnavailable(Exception): - - """ Raised when the server returns 503 Server Capacity Uavailable. """ - pass - - -class ServiceUnavailable(Exception): - - """ Raised when the server returns 503 Service Unavailable. """ - pass - - -class Unauthorized(Exception): - - """ Raised when the server returns 401 Unauthorized. """ - pass diff --git a/gns3dms/cloud/rackspace_ctrl.py b/gns3dms/cloud/rackspace_ctrl.py deleted file mode 100644 index aee7f46d..00000000 --- a/gns3dms/cloud/rackspace_ctrl.py +++ /dev/null @@ -1,259 +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 . - -""" Interacts with Rackspace API to create and manage cloud instances. """ - -from .base_cloud_ctrl import BaseCloudCtrl -import json -import requests -from libcloud.compute.drivers.rackspace import ENDPOINT_ARGS_MAP -from libcloud.compute.providers import get_driver -from libcloud.compute.types import Provider -from libcloud.storage.providers import get_driver as get_storage_driver -from libcloud.storage.types import Provider as StorageProvider - -from .exceptions import ItemNotFound, ApiError -from ..version import __version__ - -from collections import OrderedDict - -import logging -log = logging.getLogger(__name__) - -RACKSPACE_REGIONS = [{ENDPOINT_ARGS_MAP[k]['region']: k} for k in - ENDPOINT_ARGS_MAP] - - -class RackspaceCtrl(BaseCloudCtrl): - - """ Controller class for interacting with Rackspace API. """ - - def __init__(self, username, api_key, *args, **kwargs): - super(RackspaceCtrl, self).__init__(username, api_key) - - # set this up so it can be swapped out with a mock for testing - self.post_fn = requests.post - self.driver_cls = get_driver(Provider.RACKSPACE) - self.storage_driver_cls = get_storage_driver(StorageProvider.CLOUDFILES) - - self.driver = None - self.storage_driver = None - self.region = None - self.instances = {} - - self.authenticated = False - self.identity_ep = \ - "https://identity.api.rackspacecloud.com/v2.0/tokens" - - self.regions = [] - self.token = None - self.tenant_id = None - self.flavor_ep = "https://dfw.servers.api.rackspacecloud.com/v2/{username}/flavors" - self._flavors = OrderedDict([ - ('2', '512MB, 1 VCPU'), - ('3', '1GB, 1 VCPU'), - ('4', '2GB, 2 VCPUs'), - ('5', '4GB, 2 VCPUs'), - ('6', '8GB, 4 VCPUs'), - ('7', '15GB, 6 VCPUs'), - ('8', '30GB, 8 VCPUs'), - ('performance1-1', '1GB Performance, 1 VCPU'), - ('performance1-2', '2GB Performance, 2 VCPUs'), - ('performance1-4', '4GB Performance, 4 VCPUs'), - ('performance1-8', '8GB Performance, 8 VCPUs'), - ('performance2-15', '15GB Performance, 4 VCPUs'), - ('performance2-30', '30GB Performance, 8 VCPUs'), - ('performance2-60', '60GB Performance, 16 VCPUs'), - ('performance2-90', '90GB Performance, 24 VCPUs'), - ('performance2-120', '120GB Performance, 32 VCPUs',) - ]) - - def authenticate(self): - """ - Submit username and api key to API service. - - If authentication is successful, set self.regions and self.token. - Return boolean. - - """ - - self.authenticated = False - - if len(self.username) < 1: - return False - - if len(self.api_key) < 1: - return False - - data = json.dumps({ - "auth": { - "RAX-KSKEY:apiKeyCredentials": { - "username": self.username, - "apiKey": self.api_key - } - } - }) - - headers = { - 'Content-type': 'application/json', - 'Accept': 'application/json' - } - - response = self.post_fn(self.identity_ep, data=data, headers=headers) - - if response.status_code == 200: - - api_data = response.json() - self.token = self._parse_token(api_data) - - if self.token: - self.authenticated = True - user_regions = self._parse_endpoints(api_data) - self.regions = self._make_region_list(user_regions) - self.tenant_id = self._parse_tenant_id(api_data) - - else: - self.regions = [] - self.token = None - - response.connection.close() - - return self.authenticated - - def list_regions(self): - """ Return a list the regions available to the user. """ - - return self.regions - - def list_flavors(self): - """ Return the dictionary containing flavors id and names """ - - return self._flavors - - def _parse_endpoints(self, api_data): - """ - Parse the JSON-encoded data returned by the Identity Service API. - - Return a list of regions available for Compute v2. - - """ - - region_codes = [] - - for ep_type in api_data['access']['serviceCatalog']: - if ep_type['name'] == "cloudServersOpenStack" \ - and ep_type['type'] == "compute": - - for ep in ep_type['endpoints']: - if ep['versionId'] == "2": - region_codes.append(ep['region']) - - return region_codes - - def _parse_token(self, api_data): - """ Parse the token from the JSON-encoded data returned by the API. """ - - try: - token = api_data['access']['token']['id'] - except KeyError: - return None - - return token - - def _parse_tenant_id(self, api_data): - """ """ - try: - roles = api_data['access']['user']['roles'] - for role in roles: - if 'tenantId' in role and role['name'] == 'compute:default': - return role['tenantId'] - return None - except KeyError: - return None - - def _make_region_list(self, region_codes): - """ - Make a list of regions for use in the GUI. - - Returns a list of key-value pairs in the form: - : - eg, - [ - {'DFW': 'dfw'} - {'ORD': 'ord'}, - ... - ] - - """ - - region_list = [] - - for ep in ENDPOINT_ARGS_MAP: - if ENDPOINT_ARGS_MAP[ep]['region'] in region_codes: - region_list.append({ENDPOINT_ARGS_MAP[ep]['region']: ep}) - - return region_list - - def set_region(self, region): - """ Set self.region and self.driver. Returns True or False. """ - - try: - self.driver = self.driver_cls(self.username, self.api_key, - region=region) - self.storage_driver = self.storage_driver_cls(self.username, self.api_key, - region=region) - - except ValueError: - return False - - self.region = region - return True - - def get_image(self, image_id): - return self.driver.get_image(image_id) - - -def get_provider(cloud_settings): - """ - Utility function to retrieve a cloud provider instance already authenticated and with the - region set - - :param cloud_settings: cloud settings dictionary - :return: a provider instance or None on errors - """ - try: - username = cloud_settings['cloud_user_name'] - apikey = cloud_settings['cloud_api_key'] - region = cloud_settings['cloud_region'] - except KeyError as e: - log.error("Unable to create cloud provider: {}".format(e)) - return - - provider = RackspaceCtrl(username, apikey) - - if not provider.authenticate(): - log.error("Authentication failed for cloud provider") - return - - if not region: - region = provider.list_regions().values()[0] - - if not provider.set_region(region): - log.error("Unable to set cloud provider region") - return - - return provider diff --git a/gns3dms/main.py b/gns3dms/main.py deleted file mode 100644 index d7d0fc30..00000000 --- a/gns3dms/main.py +++ /dev/null @@ -1,402 +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 . - -# __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) - -""" -Monitors communication with the GNS3 client via tmp file. Will terminate the instance if -communication is lost. -""" - -import os -import sys -import time -import getopt -import datetime -import logging -import signal -import configparser -from logging.handlers import * -from os.path import expanduser - -SCRIPT_NAME = os.path.basename(__file__) - -# Is the full path when used as an import -SCRIPT_PATH = os.path.dirname(__file__) - -if not SCRIPT_PATH: - SCRIPT_PATH = os.path.join(os.path.dirname(os.path.abspath( - sys.argv[0]))) - - -EXTRA_LIB = "%s/modules" % (SCRIPT_PATH) -sys.path.append(EXTRA_LIB) - -from . import cloud -from rackspace_cloud import Rackspace - -LOG_NAME = "gns3dms" -log = None - -sys.path.append(EXTRA_LIB) - -import daemon - -my_daemon = None - -usage = """ -USAGE: %s - -Options: - - -d, --debug Enable debugging - -v, --verbose Enable verbose logging - -h, --help Display this menu :) - - --cloud_api_key Rackspace API key - --cloud_user_name - - --instance_id ID of the Rackspace instance to terminate - --cloud_region Region of instance - - --dead_time How long in seconds can the communication lose exist before we - shutdown this instance. - Default: - Example --dead_time=3600 (60 minutes) - - --check-interval Defaults to --dead_time, used for debugging - - --init-wait Inital wait time, how long before we start pulling the file. - Default: 300 (5 min) - Example --init-wait=300 - - --file The file we monitor for updates - - -k Kill previous instance running in background - --background Run in background - -""" % (SCRIPT_NAME) - -# Parse cmd line options - - -def parse_cmd_line(argv): - """ - Parse command line arguments - - argv: Pass in cmd line arguments - """ - - short_args = "dvhk" - long_args = ("debug", - "verbose", - "help", - "cloud_user_name=", - "cloud_api_key=", - "instance_id=", - "region=", - "dead_time=", - "init-wait=", - "check-interval=", - "file=", - "background", - ) - try: - opts, extra_opts = getopt.getopt(argv[1:], short_args, long_args) - except getopt.GetoptError as e: - print("Unrecognized command line option or missing required argument: %s" % (e)) - print(usage) - sys.exit(2) - - cmd_line_option_list = {} - cmd_line_option_list["debug"] = False - cmd_line_option_list["verbose"] = True - cmd_line_option_list["cloud_user_name"] = None - cmd_line_option_list["cloud_api_key"] = None - cmd_line_option_list["instance_id"] = None - cmd_line_option_list["region"] = None - cmd_line_option_list["dead_time"] = 60 * 60 # minutes - cmd_line_option_list["check-interval"] = None - cmd_line_option_list["init-wait"] = 5 * 60 - cmd_line_option_list["file"] = None - cmd_line_option_list["shutdown"] = False - cmd_line_option_list["daemon"] = False - cmd_line_option_list['starttime'] = datetime.datetime.now() - - if sys.platform == "linux": - cmd_line_option_list['syslog'] = "/dev/log" - elif sys.platform == "osx": - cmd_line_option_list['syslog'] = "/var/run/syslog" - else: - cmd_line_option_list['syslog'] = ('localhost', 514) - - get_gns3secrets(cmd_line_option_list) - cmd_line_option_list["dead_time"] = int(cmd_line_option_list["dead_time"]) - - for opt, val in opts: - if (opt in ("-h", "--help")): - print(usage) - sys.exit(0) - elif (opt in ("-d", "--debug")): - cmd_line_option_list["debug"] = True - elif (opt in ("-v", "--verbose")): - cmd_line_option_list["verbose"] = True - elif (opt in ("--cloud_user_name")): - cmd_line_option_list["cloud_user_name"] = val - elif (opt in ("--cloud_api_key")): - cmd_line_option_list["cloud_api_key"] = val - elif (opt in ("--instance_id")): - cmd_line_option_list["instance_id"] = val - elif (opt in ("--region")): - cmd_line_option_list["region"] = val - elif (opt in ("--dead_time")): - cmd_line_option_list["dead_time"] = int(val) - elif (opt in ("--check-interval")): - cmd_line_option_list["check-interval"] = int(val) - elif (opt in ("--init-wait")): - cmd_line_option_list["init-wait"] = int(val) - elif (opt in ("--file")): - cmd_line_option_list["file"] = val - elif (opt in ("-k")): - cmd_line_option_list["shutdown"] = True - elif (opt in ("--background")): - cmd_line_option_list["daemon"] = True - - if cmd_line_option_list["shutdown"] is False: - - if cmd_line_option_list["check-interval"] is None: - cmd_line_option_list["check-interval"] = cmd_line_option_list["dead_time"] + 120 - - if cmd_line_option_list["cloud_user_name"] is None: - print("You need to specify a username!!!!") - print(usage) - sys.exit(2) - - if cmd_line_option_list["cloud_api_key"] is None: - print("You need to specify an apikey!!!!") - print(usage) - sys.exit(2) - - if cmd_line_option_list["file"] is None: - print("You need to specify a file to watch!!!!") - print(usage) - sys.exit(2) - - if cmd_line_option_list["instance_id"] is None: - print("You need to specify an instance_id") - print(usage) - sys.exit(2) - - if cmd_line_option_list["cloud_region"] is None: - print("You need to specify a cloud_region") - print(usage) - sys.exit(2) - - return cmd_line_option_list - - -def get_gns3secrets(cmd_line_option_list): - """ - Load cloud credentials from .gns3secrets - """ - - gns3secret_paths = [ - os.path.join(os.path.expanduser("~"), '.config', 'GNS3'), - SCRIPT_PATH, - ] - - config = configparser.ConfigParser() - - for gns3secret_path in gns3secret_paths: - gns3secret_file = "%s/cloud.conf" % (gns3secret_path) - if os.path.isfile(gns3secret_file): - config.read(gns3secret_file) - - try: - for key, value in config.items("CLOUD_SERVER"): - cmd_line_option_list[key] = value.strip() - except configparser.NoSectionError: - pass - - -def set_logging(cmd_options): - """ - Setup logging and format output for console and syslog - - Syslog is using the KERN facility - """ - log = logging.getLogger("%s" % (LOG_NAME)) - log_level = logging.INFO - log_level_console = logging.WARNING - - if cmd_options['verbose']: - log_level_console = logging.INFO - - if cmd_options['debug']: - log_level_console = logging.DEBUG - log_level = logging.DEBUG - - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - sys_formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s') - - console_log = logging.StreamHandler() - console_log.setLevel(log_level_console) - console_log.setFormatter(formatter) - - syslog_hndlr = SysLogHandler( - address=cmd_options['syslog'], - facility=SysLogHandler.LOG_KERN - ) - - syslog_hndlr.setFormatter(sys_formatter) - - log.setLevel(log_level) - log.addHandler(console_log) - log.addHandler(syslog_hndlr) - - return log - - -def send_shutdown(pid_file): - """ - Sends the daemon process a kill signal - """ - try: - with open(pid_file, 'r') as pidf: - pid = int(pidf.readline().strip()) - pidf.close() - os.kill(pid, 15) - except: - log.info("No running instance found!!!") - log.info("Missing PID file: %s" % (pid_file)) - - -def _get_file_age(filename): - return datetime.datetime.fromtimestamp( - os.path.getmtime(filename) - ) - - -def monitor_loop(options): - """ - Checks the options["file"] modification time against an interval. If the - modification time is too old we terminate the instance. - """ - - log.debug("Waiting for init-wait to pass: %s" % (options["init-wait"])) - time.sleep(options["init-wait"]) - - log.info("Starting monitor_loop") - - terminate_attempts = 0 - - while options['shutdown'] is False: - log.debug("In monitor_loop for : %s" % ( - datetime.datetime.now() - options['starttime']) - ) - - file_last_modified = _get_file_age(options["file"]) - now = datetime.datetime.now() - - delta = now - file_last_modified - log.debug("File last updated: %s seconds ago" % (delta.seconds)) - - if delta.seconds > options["dead_time"]: - log.warning("Dead time exceeded, terminating instance ...") - # Terminate involves many layers of HTTP / API calls, lots of - # different errors types could occur here. - try: - rksp = Rackspace(options) - rksp.terminate() - except Exception as e: - log.critical("Exception during terminate: %s" % (e)) - - terminate_attempts += 1 - log.warning("Termination sent, attempt: %s" % (terminate_attempts)) - time.sleep(600) - else: - time.sleep(options["check-interval"]) - - log.info("Leaving monitor_loop") - log.info("Shutting down") - - -def main(): - - global log - global my_daemon - options = parse_cmd_line(sys.argv) - log = set_logging(options) - - def _shutdown(signalnum=None, frame=None): - """ - Handles the SIGINT and SIGTERM event, inside of main so it has access to - the log vars. - """ - - log.info("Received shutdown signal") - options["shutdown"] = True - - pid_file = "%s/.gns3dms.pid" % (expanduser("~")) - - if options["shutdown"]: - send_shutdown(pid_file) - sys.exit(0) - - if options["daemon"]: - my_daemon = MyDaemon(pid_file, options) - - # Setup signal to catch Control-C / SIGINT and SIGTERM - signal.signal(signal.SIGINT, _shutdown) - signal.signal(signal.SIGTERM, _shutdown) - - log.info("Starting ...") - log.debug("Using settings:") - for key, value in iter(sorted(options.items())): - log.debug("%s : %s" % (key, value)) - - log.debug("Checking file ....") - if os.path.isfile(options["file"]) is False: - log.critical("File does not exist!!!") - sys.exit(1) - - test_acess = _get_file_age(options["file"]) - if not isinstance(test_acess, datetime.datetime): - log.critical("Can't get file modification time!!!") - sys.exit(1) - - if my_daemon: - my_daemon.start() - else: - monitor_loop(options) - - -class MyDaemon(daemon.daemon): - - def run(self): - monitor_loop(self.options) - - -if __name__ == "__main__": - result = main() - sys.exit(result) diff --git a/gns3dms/modules/__init__.py b/gns3dms/modules/__init__.py deleted file mode 100644 index 0950e877..00000000 --- a/gns3dms/modules/__init__.py +++ /dev/null @@ -1,24 +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 . - -# __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) diff --git a/gns3dms/modules/daemon.py b/gns3dms/modules/daemon.py deleted file mode 100644 index cfc5539f..00000000 --- a/gns3dms/modules/daemon.py +++ /dev/null @@ -1,144 +0,0 @@ -"""Generic linux daemon base class for python 3.x.""" - -import sys -import os -import time -import atexit -import signal - - -class daemon: - - """A generic daemon class. - - Usage: subclass the daemon class and override the run() method.""" - - def __init__(self, pidfile, options): - self.pidfile = pidfile - self.options = options - - def daemonize(self): - """Deamonize class. UNIX double fork mechanism.""" - - try: - pid = os.fork() - if pid > 0: - # exit first parent - sys.exit(0) - except OSError as err: - sys.stderr.write('fork #1 failed: {0}\n'.format(err)) - sys.exit(1) - - # decouple from parent environment - os.chdir('/') - os.setsid() - os.umask(0) - - # do second fork - try: - pid = os.fork() - if pid > 0: - - # exit from second parent - sys.exit(0) - except OSError as err: - sys.stderr.write('fork #2 failed: {0}\n'.format(err)) - sys.exit(1) - - # redirect standard file descriptors - sys.stdout.flush() - sys.stderr.flush() - si = open(os.devnull, 'r') - so = open(os.devnull, 'a+') - se = open(os.devnull, 'a+') - - os.dup2(si.fileno(), sys.stdin.fileno()) - os.dup2(so.fileno(), sys.stdout.fileno()) - os.dup2(se.fileno(), sys.stderr.fileno()) - - # write pidfile - atexit.register(self.delpid) - - pid = str(os.getpid()) - with open(self.pidfile, 'w+') as f: - f.write(pid + '\n') - - def delpid(self): - os.remove(self.pidfile) - - def check_pid(self, pid): - """ Check For the existence of a unix pid. """ - try: - os.kill(pid, 0) - except OSError: - return False - else: - return True - - def start(self): - """Start the daemon.""" - - # Check for a pidfile to see if the daemon already runs - try: - with open(self.pidfile, 'r') as pf: - - pid = int(pf.read().strip()) - except IOError: - pid = None - - if pid: - pid_exist = self.check_pid(pid) - - if pid_exist: - message = "Already running: %s\n" % (pid) - sys.stderr.write(message) - sys.exit(1) - else: - message = "pidfile {0} already exist. " + \ - "but process is dead\n" - sys.stderr.write(message.format(self.pidfile)) - - # Start the daemon - self.daemonize() - self.run() - - def stop(self): - """Stop the daemon.""" - - # Get the pid from the pidfile - try: - with open(self.pidfile, 'r') as pf: - pid = int(pf.read().strip()) - except IOError: - pid = None - - if not pid: - message = "pidfile {0} does not exist. " + \ - "Daemon not running?\n" - sys.stderr.write(message.format(self.pidfile)) - return # not an error in a restart - - # Try killing the daemon process - try: - while True: - os.kill(pid, signal.SIGTERM) - time.sleep(0.1) - except OSError as err: - e = str(err.args) - if e.find("No such process") > 0: - if os.path.exists(self.pidfile): - os.remove(self.pidfile) - else: - print(str(err.args)) - sys.exit(1) - - def restart(self): - """Restart the daemon.""" - self.stop() - self.start() - - def run(self): - """You should override this method when you subclass Daemon. - - It will be called after the process has been daemonized by - start() or restart().""" diff --git a/gns3dms/modules/rackspace_cloud.py b/gns3dms/modules/rackspace_cloud.py deleted file mode 100644 index 14c0128f..00000000 --- a/gns3dms/modules/rackspace_cloud.py +++ /dev/null @@ -1,73 +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 . - -# __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) - -import os -import sys -import json -import logging -import socket - -from gns3dms.cloud.rackspace_ctrl import RackspaceCtrl - - -LOG_NAME = "gns3dms.rksp" -log = logging.getLogger("%s" % (LOG_NAME)) - - -class Rackspace(object): - - def __init__(self, options): - self.username = options["cloud_user_name"] - self.apikey = options["cloud_api_key"] - self.authenticated = False - self.hostname = socket.gethostname() - self.instance_id = options["instance_id"] - self.region = options["cloud_region"] - - log.debug("Authenticating with Rackspace") - log.debug("My hostname: %s" % (self.hostname)) - self.rksp = RackspaceCtrl(self.username, self.apikey) - self.authenticated = self.rksp.authenticate() - - def _find_my_instance(self): - if self.authenticated is not False: - log.critical("Not authenticated against rackspace!!!!") - - for region in self.rksp.list_regions(): - log.debug("Rackspace regions: %s" % (region)) - - log.debug("Checking region: %s" % (self.region)) - self.rksp.set_region(self.region) - for server in self.rksp.list_instances(): - log.debug("Checking server: %s" % (server.name)) - if server.id == self.instance_id: - log.info("Found matching instance: %s" % (server.id)) - log.info("Startup id: %s" % (self.instance_id)) - return server - - def terminate(self): - server = self._find_my_instance() - log.warning("Sending termination") - self.rksp.delete_instance(server) diff --git a/gns3dms/version.py b/gns3dms/version.py deleted file mode 100644 index 545a0060..00000000 --- a/gns3dms/version.py +++ /dev/null @@ -1,27 +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 . - -# __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__ = "0.1" -__version_info__ = (0, 0, 1, -99) diff --git a/requirements.txt b/requirements.txt index db4f67fd..c12c2071 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,6 @@ netifaces==0.10.4 jsonschema==2.4.0 python-dateutil==2.3 -apache-libcloud==0.16.0 -requests==2.5.0 aiohttp==0.14.4 Jinja2==2.7.3 raven==5.2.0 diff --git a/setup.py b/setup.py index 4b716076..37fd5902 100644 --- a/setup.py +++ b/setup.py @@ -36,8 +36,6 @@ class PyTest(TestCommand): dependencies = ["aiohttp==0.14.4", "jsonschema==2.4.0", - "apache-libcloud==0.16.0", - "requests==2.5.0", "Jinja2==2.7.3", "raven==5.2.0"] From 1d0ffe4b2e6aa1c722e680ecb3b9db4bd353a2f6 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 5 Mar 2015 17:44:01 +0100 Subject: [PATCH 386/485] Add more informations to crash reports --- gns3server/crash_report.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py index 099ab28a..f5da30f4 100644 --- a/gns3server/crash_report.py +++ b/gns3server/crash_report.py @@ -17,8 +17,10 @@ import raven import json -import asyncio.futures -import asyncio + +import sys +import struct +import platform from .version import __version__ from .config import Config @@ -50,6 +52,15 @@ class CrashReport: "url": request.path, "data": request.json, }) + self._client.tags_context({ + "os:name": platform.system(), + "os:release": platform.release(), + "python:version": "{}.{}.{}".format(sys.version_info[0], + sys.version_info[1], + sys.version_info[2]), + "python:bit": struct.calcsize("P") * 8, + "python:encoding": sys.getdefaultencoding() + }) try: self._client.captureException() except Exception as e: From a4da6c6a74ba0b9000e3974749e4467379a1ee42 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 5 Mar 2015 20:05:46 +0100 Subject: [PATCH 387/485] Add more informations in crash reports --- gns3server/crash_report.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py index f5da30f4..fd112384 100644 --- a/gns3server/crash_report.py +++ b/gns3server/crash_report.py @@ -55,6 +55,9 @@ class CrashReport: self._client.tags_context({ "os:name": platform.system(), "os:release": platform.release(), + "os:win_32": platform.win32_ver(), + "os:mac": platform.mac_ver(), + "os:linux": platform.linux_distribution(), "python:version": "{}.{}.{}".format(sys.version_info[0], sys.version_info[1], sys.version_info[2]), From 01ab91722b4b305f30756eead1cd766b02704fe5 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 5 Mar 2015 20:12:56 +0100 Subject: [PATCH 388/485] Proper format of crash report --- gns3server/crash_report.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py index fd112384..0a41588a 100644 --- a/gns3server/crash_report.py +++ b/gns3server/crash_report.py @@ -55,9 +55,9 @@ class CrashReport: self._client.tags_context({ "os:name": platform.system(), "os:release": platform.release(), - "os:win_32": platform.win32_ver(), - "os:mac": platform.mac_ver(), - "os:linux": platform.linux_distribution(), + "os:win_32": " ".join(platform.win32_ver()), + "os:mac": "{} {}".format(platform.mac_ver()[0], platform.mac_ver()[2]), + "os:linux": " ".join(platform.linux_distribution()), "python:version": "{}.{}.{}".format(sys.version_info[0], sys.version_info[1], sys.version_info[2]), From a3e4b818371eb87b7596157dfd13ea2c2a1beb6a Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 5 Mar 2015 23:13:54 +0100 Subject: [PATCH 389/485] Drop cloud for the moment --- cloud-image/.novarc | 5 - cloud-image/create_image.py | 243 ----------------------------------- cloud-image/dependencies.txt | 3 - cloud-image/readme.txt | 10 -- cloud-image/script_template | 19 --- 5 files changed, 280 deletions(-) delete mode 100644 cloud-image/.novarc delete mode 100644 cloud-image/create_image.py delete mode 100644 cloud-image/dependencies.txt delete mode 100644 cloud-image/readme.txt delete mode 100644 cloud-image/script_template diff --git a/cloud-image/.novarc b/cloud-image/.novarc deleted file mode 100644 index bab8f323..00000000 --- a/cloud-image/.novarc +++ /dev/null @@ -1,5 +0,0 @@ -export OS_USERNAME=username -export OS_PASSWORD="" -export OS_TENANT_NAME=000000 -export OS_AUTH_URL=https://identity.api.rackspacecloud.com/v2.0/ -export OS_REGION_NAME=ord diff --git a/cloud-image/create_image.py b/cloud-image/create_image.py deleted file mode 100644 index b1021f34..00000000 --- a/cloud-image/create_image.py +++ /dev/null @@ -1,243 +0,0 @@ -""" Create a new GNS3 Server Rackspace image with the provided options. """ - -import argparse -import getpass -import os -import sys -import uuid -from fabric.api import env -from fabric.contrib.files import exists -from github import Github -from novaclient.v1_1 import client -from string import Template -from time import sleep - -POLL_SEC = 20 -GNS3_REPO = 'gns3/gns3-server' -PLANC_REPO = 'planctechnologies/gns3-server' -OS_AUTH_URL = 'https://identity.api.rackspacecloud.com/v2.0/' -UBUNTU_BASE_ID = '5cc098a5-7286-4b96-b3a2-49f4c4f82537' - - -def main(): - """ - Get the user options and perform the image creation. - - Creates a new instance, installs the required software, creates an image - from the instance, and then deletes the instance. - """ - - github = Github() - - args = get_cli_args() - if args.username: - username = args.username - else: - if 'OS_USERNAME' in os.environ: - username = os.environ.get('OS_USERNAME') - else: - username = raw_input('Enter Rackspace username: ') - - if args.password: - password = args.password - else: - if 'OS_PASSWORD' in os.environ: - password = os.environ.get('OS_PASSWORD') - else: - password = getpass.getpass('Enter Rackspace password: ') - - if args.tenant: - tenant = args.tenant - else: - if 'OS_TENANT_NAME' in os.environ: - tenant = os.environ.get('OS_TENANT_NAME') - else: - tenant = raw_input('Enter Rackspace Tenant ID: ') - - if args.region: - region = args.region - else: - if 'OS_REGION_NAME' in os.environ: - region = os.environ.get('OS_REGION_NAME') - else: - region = raw_input('Enter Rackspace Region Name: ') - - if args.source == 'release': - # get the list of releases, present them to the user, save the url - repo = github.get_repo(GNS3_REPO) - keyword = "tag" - i = 1 - branch_opts = {} - for tag in repo.get_tags(): - branch_opts[i] = tag.name - i += 1 - elif args.source == 'dev': - # get the list of dev branches, present them to the user, save the url - repo = github.get_repo(PLANC_REPO) - keyword = "branch" - i = 1 - branch_opts = {} - for branch in repo.get_branches(): - branch_opts[i] = branch.name - i += 1 - - prompt_text = "Select a %s" % keyword - selected_branch = prompt_user_select(branch_opts, prompt_text) - - if args.image_name: - image_name = args.image_name - else: - image_name = "gns3-%s-%s-%s" % (args.source, selected_branch, - uuid.uuid4().hex[0:4]) - - if args.on_boot: - on_boot = True - else: - on_boot = False - - startup_script = create_script(repo.svn_url, selected_branch, on_boot) - server_name = uuid.uuid4().hex - instance = create_instance(username, password, tenant, region, server_name, - startup_script) - passwd = uuid.uuid4().hex - instance.change_password(passwd) - # wait for the password change to be processed. Continuing while - # a password change is processing will cause image creation to fail. - sleep(POLL_SEC * 6) - - env.host_string = str(instance.accessIPv4) - env.user = "root" - env.password = passwd - - sys.stdout.write("Installing software...") - sys.stdout.flush() - - while True: - if exists('/tmp/gns-install-complete'): - break - - sleep(POLL_SEC) - sys.stdout.write(".") - sys.stdout.flush() - - print("Done.") - - image_id = create_image(username, password, tenant, region, instance, - image_name) - instance.delete() - - -def prompt_user_select(opts, text="Please select"): - """ Ask the user to select an option from the provided list. """ - - print("%s" % text) - print("=" * len(text)) - for o in opts: - print("(%s)\t%s" % (o, opts[o])) - - while True: - selected = raw_input("Select: ") - try: - return opts[int(selected)] - except (KeyError, ValueError): - print("Invalid selection. Try again") - - -def create_instance(username, password, tenant, region, server_name, script, - auth_url=OS_AUTH_URL): - """ Create a new instance. """ - - sys.stdout.write("Creating instance...") - sys.stdout.flush() - - nc = client.Client(username, password, tenant, auth_url, - region_name=region) - server = nc.servers.create(server_name, UBUNTU_BASE_ID, 2, - config_drive=True, userdata=script) - - while True: - server = nc.servers.get(server.id) - if server.status == 'ACTIVE': - break - - sleep(POLL_SEC) - sys.stdout.write(".") - sys.stdout.flush() - - print("Done.") - - return server - - -def create_script(git_url, git_branch, on_boot): - """ Create the start-up script. """ - - script_template = Template(open('script_template', 'r').read()) - - params = {'git_url': git_url, 'git_branch': git_branch, 'rc_local': ''} - - if on_boot: - params['rc_local'] = "echo '/usr/local/bin/gns3-server' >> /etc/rc.local" - - return script_template.substitute(params) - - -def create_image(username, password, tenant, region, server, - image_name, auth_url=OS_AUTH_URL): - """ Create a Rackspace image based on the server instance. """ - - nc = client.Client(username, password, tenant, auth_url, - region_name=region) - - sys.stdout.write("Creating image %s..." % image_name) - sys.stdout.flush() - - image_id = server.create_image(image_name) - - while True: - server = nc.servers.get(server.id) - if getattr(server, 'OS-EXT-STS:task_state') is None: - break - - sleep(POLL_SEC) - sys.stdout.write(".") - sys.stdout.flush() - - print("Done.") - - return image_id - - -def get_cli_args(): - """ Parse the CLI input. """ - - parser = argparse.ArgumentParser( - description='Create a new GNS3 image', - formatter_class=argparse.ArgumentDefaultsHelpFormatter) - - parser.add_argument( - '--rackspace_username', dest='username', action='store') - parser.add_argument( - '--rackspace_password', dest='password', action='store') - parser.add_argument( - '--rackspace_tenant', dest='tenant', action='store') - parser.add_argument( - '--rackspace_region', dest='region', action='store') - parser.add_argument( - '--source', dest='source', action='store', choices=['release', 'dev'], - default='release', help='specify the gns3-server source location') - parser.add_argument( - '--branch', dest='branch', action='store', - help='specify the branch/tag') - parser.add_argument( - '--start-on-boot', dest='on_boot', action='store_true', - default=False, help='start the GNS3-server when the image boots') - parser.add_argument( - '--image-name', dest='image_name', action='store', - help='the name of the image to be created') - - return parser.parse_args() - - -if __name__ == "__main__": - main() diff --git a/cloud-image/dependencies.txt b/cloud-image/dependencies.txt deleted file mode 100644 index 502c79b6..00000000 --- a/cloud-image/dependencies.txt +++ /dev/null @@ -1,3 +0,0 @@ -fabric -pygithub -python-novaclient diff --git a/cloud-image/readme.txt b/cloud-image/readme.txt deleted file mode 100644 index a979aa08..00000000 --- a/cloud-image/readme.txt +++ /dev/null @@ -1,10 +0,0 @@ -create_image.py: - -- uses fabric, which doesn't support Python 3 - -- prompts for Rackspace credentials if environment variables not set - - see .novarc for example env variables - - note that the novaclient library uses the Rackspace password and -not- - the API key - -- use '--help' for help with arguments diff --git a/cloud-image/script_template b/cloud-image/script_template deleted file mode 100644 index c59fbbe6..00000000 --- a/cloud-image/script_template +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash -export DEBIAN_FRONTEND=noninteractive -apt-get -y update -apt-get -o Dpkg::Options::="--force-confnew" --force-yes -fuy dist-upgrade -apt-get -y install git -apt-get -y install python3-setuptools -apt-get -y install python3-netifaces -apt-get -y install python3-pip - -mkdir -p /opt/gns3 -pushd /opt/gns3 -git clone --branch ${git_branch} ${git_url} -cd gns3-server -pip3 install -r dev-requirements.txt -python3 ./setup.py install - -${rc_local} - -touch /tmp/gns-install-complete From 2679c03fe21fcb6f24b23097a1744e2aa752c1cb Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 5 Mar 2015 23:15:06 +0100 Subject: [PATCH 390/485] Drop cloud from config --- gns3server/config.py | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/gns3server/config.py b/gns3server/config.py index cf6062d8..4b8f4241 100644 --- a/gns3server/config.py +++ b/gns3server/config.py @@ -61,15 +61,13 @@ class Config(object): appdata = os.path.expandvars("%APPDATA%") common_appdata = os.path.expandvars("%COMMON_APPDATA%") - self._cloud_file = os.path.join(appdata, appname, "cloud.ini") filename = "server.ini" if self._files is None: self._files = [os.path.join(appdata, appname, filename), os.path.join(appdata, appname + ".ini"), os.path.join(common_appdata, appname, filename), os.path.join(common_appdata, appname + ".ini"), - filename, - self._cloud_file] + filename] else: # On UNIX-like platforms, the configuration file location can be one of the following: @@ -84,20 +82,16 @@ class Config(object): else: appname = "GNS3" home = os.path.expanduser("~") - self._cloud_file = os.path.join(home, ".config", appname, "cloud.conf") filename = "server.conf" if self._files is None: self._files = [os.path.join(home, ".config", appname, filename), os.path.join(home, ".config", appname + ".conf"), os.path.join("/etc/xdg", appname, filename), os.path.join("/etc/xdg", appname + ".conf"), - filename, - self._cloud_file] + filename] self._config = configparser.ConfigParser() self.read_config() - self._cloud_config = configparser.ConfigParser() - self.read_cloud_config() def reload(self): """ @@ -108,20 +102,9 @@ class Config(object): for section in self._override_config: self.set_section_config(section, self._override_config[section]) - def list_cloud_config_file(self): - return self._cloud_file - def get_config_files(self): return self._watched_files - def read_cloud_config(self): - parsed_file = self._cloud_config.read(self._cloud_file) - if not self._cloud_config.has_section(CLOUD_SERVER): - self._cloud_config.add_section(CLOUD_SERVER) - - def cloud_settings(self): - return self._cloud_config[CLOUD_SERVER] - def read_config(self): """ Read the configuration files. From b5e8aaf682a582bdf93935bb0491448b12b74e80 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 5 Mar 2015 16:11:43 -0700 Subject: [PATCH 391/485] Support for Raven to send crash report from a frozen state. --- gns3server/crash_report.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py index 0a41588a..ac930723 100644 --- a/gns3server/crash_report.py +++ b/gns3server/crash_report.py @@ -16,8 +16,7 @@ # along with this program. If not, see . import raven -import json - +import os import sys import struct import platform @@ -36,6 +35,9 @@ class CrashReport: """ DSN = "sync+https://50af75d8641d4ea7a4ea6b38c7df6cf9:41d54936f8f14e558066262e2ec8bbeb@app.getsentry.com/38482" + if hasattr(sys, "frozen"): + cacert = os.path.join(os.getcwd(), "cacert.pem") + DSN += "?ca_certs={}".format(cacert) _instance = None def __init__(self): @@ -62,12 +64,14 @@ class CrashReport: sys.version_info[1], sys.version_info[2]), "python:bit": struct.calcsize("P") * 8, - "python:encoding": sys.getdefaultencoding() + "python:encoding": sys.getdefaultencoding(), + "python:frozen": "{}".format(hasattr(sys, "frozen")) }) try: - self._client.captureException() + report = self._client.captureException() except Exception as e: - log.error("Can't send crash report to Sentry: %s", e) + log.error("Can't send crash report to Sentry: {}".format(e)) + log.info("Crash report sent with event ID: {}".format(self._client.get_ident(report))) @classmethod def instance(cls): From 2bae814eb1ec82f62ea2fdee7f9f1caf7fc530ce Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 5 Mar 2015 18:00:17 -0700 Subject: [PATCH 392/485] Remove redundant code for Dynamips hypervisor connections. --- gns3server/modules/dynamips/__init__.py | 31 ------------------- .../modules/dynamips/dynamips_hypervisor.py | 30 ++++++++++-------- 2 files changed, 17 insertions(+), 44 deletions(-) diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index 0cf6ae4b..21db9cbd 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -294,34 +294,6 @@ class Dynamips(BaseManager): self._dynamips_path = dynamips_path return dynamips_path - @asyncio.coroutine - def _wait_for_hypervisor(self, host, port, timeout=10.0): - """ - Waits for an hypervisor to be started (accepting a socket connection) - - :param host: host/address to connect to the hypervisor - :param port: port to connect to the hypervisor - """ - - begin = time.time() - connection_success = False - last_exception = None - while time.time() - begin < timeout: - yield from asyncio.sleep(0.01) - try: - _, writer = yield from asyncio.open_connection(host, port) - writer.close() - except OSError as e: - last_exception = e - continue - connection_success = True - break - - if not connection_success: - raise DynamipsError("Couldn't connect to hypervisor on {}:{} :{}".format(host, port, last_exception)) - else: - log.info("Dynamips server ready after {:.4f} seconds".format(time.time() - begin)) - @asyncio.coroutine def start_new_hypervisor(self, working_dir=None): """ @@ -356,10 +328,7 @@ class Dynamips(BaseManager): log.info("Creating new hypervisor {}:{} with working directory {}".format(hypervisor.host, hypervisor.port, working_dir)) yield from hypervisor.start() - - yield from self._wait_for_hypervisor(server_host, port) log.info("Hypervisor {}:{} has successfully started".format(hypervisor.host, hypervisor.port)) - yield from hypervisor.connect() if parse_version(hypervisor.version) < parse_version('0.2.11'): raise DynamipsError("Dynamips version must be >= 0.2.11, detected version is {}".format(hypervisor.version)) diff --git a/gns3server/modules/dynamips/dynamips_hypervisor.py b/gns3server/modules/dynamips/dynamips_hypervisor.py index 20ab9764..7c22f011 100644 --- a/gns3server/modules/dynamips/dynamips_hypervisor.py +++ b/gns3server/modules/dynamips/dynamips_hypervisor.py @@ -21,6 +21,7 @@ http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L46 """ import re +import time import logging import asyncio @@ -59,7 +60,7 @@ class DynamipsHypervisor: self._writer = None @asyncio.coroutine - def connect(self): + def connect(self, timeout=10): """ Connects to the hypervisor. """ @@ -73,20 +74,23 @@ class DynamipsHypervisor: else: host = self._host - tries = 3 - while tries > 0: + begin = time.time() + connection_success = False + last_exception = None + while time.time() - begin < timeout: + yield from asyncio.sleep(0.01) try: - self._reader, self._writer = yield from asyncio.wait_for(asyncio.open_connection(host, self._port), timeout=self._timeout) - break + self._reader, self._writer = yield from asyncio.open_connection(host, self._port) except OSError as e: - if tries: - tries -= 1 - log.warn("Could not connect to hypervisor {}:{} {}, retrying...".format(host, self._port, e)) - yield from asyncio.sleep(0.1) - continue - raise DynamipsError("Could not connect to hypervisor {}:{} {}".format(host, self._port, e)) - except asyncio.TimeoutError: - raise DynamipsError("Timeout error while connecting to hypervisor {}:{}".format(host, self._port)) + last_exception = e + continue + connection_success = True + break + + if not connection_success: + raise DynamipsError("Couldn't connect to hypervisor on {}:{} :{}".format(host, self._port, last_exception)) + else: + log.info("Connected to Dynamips hypervisor after {:.4f} seconds".format(time.time() - begin)) try: version = yield from self.send("hypervisor version") From a64dfdd6948e6df70d3953a4d8082fce3cf83a78 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 5 Mar 2015 19:11:33 -0700 Subject: [PATCH 393/485] Disconnect network cable if adapter is not attached in VirtualBox vNIC. --- gns3server/modules/virtualbox/virtualbox_vm.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 0d93d281..2b318498 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -618,9 +618,12 @@ class VirtualBoxVM(BaseVM): nic_attachments = yield from self._get_nic_attachements(self._maximum_adapters) for adapter_number in range(0, len(self._ethernet_adapters)): + attachment = nic_attachments[adapter_number] + if attachment == "null": + # disconnect the cable if no backend is attached. + self._modify_vm("--cableconnected{} off".format(adapter_number + 1)) nio = self._ethernet_adapters[adapter_number].get_nio(0) if nio: - attachment = nic_attachments[adapter_number] if not self._use_any_adapter and attachment not in ("none", "null"): raise VirtualBoxError("Attachment ({}) already configured on adapter {}. " "Please set it to 'Not attached' to allow GNS3 to use it.".format(attachment, From 18f3859e8780d75939ddf2de16978b222d4f4e94 Mon Sep 17 00:00:00 2001 From: grossmj Date: Thu, 5 Mar 2015 21:20:02 -0700 Subject: [PATCH 394/485] Fixes Telnet server initialization issue in VirtualBox. Fixes #88. --- .../modules/virtualbox/telnet_server.py | 22 ++++++++----------- .../modules/virtualbox/virtualbox_vm.py | 19 +++++++++++----- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/gns3server/modules/virtualbox/telnet_server.py b/gns3server/modules/virtualbox/telnet_server.py index b5e214a5..bfbee71b 100644 --- a/gns3server/modules/virtualbox/telnet_server.py +++ b/gns3server/modules/virtualbox/telnet_server.py @@ -43,6 +43,7 @@ class TelnetServer(threading.Thread): def __init__(self, vm_name, pipe_path, host, port): + threading.Thread.__init__(self) self._vm_name = vm_name self._pipe = pipe_path self._host = host @@ -58,20 +59,15 @@ class TelnetServer(threading.Thread): # we must a thread for reading the pipe on Windows because it is a Named Pipe and it cannot be monitored by select() self._use_thread = True - try: - if ":" in self._host: - # IPv6 address support - self._server_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) - else: - self._server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self._server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self._server_socket.bind((self._host, self._port)) - self._server_socket.listen(socket.SOMAXCONN) - except OSError as e: - log.critical("unable to create a server socket: {}".format(e)) - return + if ":" in self._host: + # IPv6 address support + self._server_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + else: + self._server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._server_socket.bind((self._host, self._port)) + self._server_socket.listen(socket.SOMAXCONN) - threading.Thread.__init__(self) log.info("Telnet server initialized, waiting for clients on {}:{}".format(self._host, self._port)) def run(self): diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 2b318498..801d9bb4 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -711,7 +711,10 @@ class VirtualBoxVM(BaseVM): self._serial_pipe = open(pipe_name, "a+b") except OSError as e: raise VirtualBoxError("Could not open the pipe {}: {}".format(pipe_name, e)) - self._telnet_server_thread = TelnetServer(self._vmname, msvcrt.get_osfhandle(self._serial_pipe.fileno()), self._manager.port_manager.console_host, self._console) + try: + self._telnet_server_thread = TelnetServer(self._vmname, msvcrt.get_osfhandle(self._serial_pipe.fileno()), self._manager.port_manager.console_host, self._console) + except OSError as e: + raise VirtualBoxError("Unable to create Telnet server: {}".format(e)) self._telnet_server_thread.start() else: try: @@ -719,7 +722,10 @@ class VirtualBoxVM(BaseVM): self._serial_pipe.connect(pipe_name) except OSError as e: raise VirtualBoxError("Could not connect to the pipe {}: {}".format(pipe_name, e)) - self._telnet_server_thread = TelnetServer(self._vmname, self._serial_pipe, self._manager.port_manager.console_host, self._console) + try: + self._telnet_server_thread = TelnetServer(self._vmname, self._serial_pipe, self._manager.port_manager.console_host, self._console) + except OSError as e: + raise VirtualBoxError("Unable to create Telnet server: {}".format(e)) self._telnet_server_thread.start() def _stop_remote_console(self): @@ -728,14 +734,15 @@ class VirtualBoxVM(BaseVM): """ if self._telnet_server_thread: - self._telnet_server_thread.stop() - self._telnet_server_thread.join(timeout=3) - if self._telnet_server_thread.isAlive(): + if self._telnet_server_thread.is_alive(): + self._telnet_server_thread.stop() + self._telnet_server_thread.join(timeout=3) + if self._telnet_server_thread.is_alive(): log.warn("Serial pipe thread is still alive!") self._telnet_server_thread = None if self._serial_pipe: - if sys.platform.startswith('win'): + if sys.platform.startswith("win"): win32file.CloseHandle(msvcrt.get_osfhandle(self._serial_pipe.fileno())) else: self._serial_pipe.close() From d657f94c18e739c0796f9dae5b0b7a7a57fd358e Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 6 Mar 2015 14:46:31 +0100 Subject: [PATCH 395/485] Fix crash when you start capture on a non running IOU --- gns3server/handlers/api/iou_handler.py | 17 ++++++-- tests/handlers/api/test_iou.py | 54 +++++++++++++++++++------- 2 files changed, 54 insertions(+), 17 deletions(-) diff --git a/gns3server/handlers/api/iou_handler.py b/gns3server/handlers/api/iou_handler.py index cd90fb2a..5769a992 100644 --- a/gns3server/handlers/api/iou_handler.py +++ b/gns3server/handlers/api/iou_handler.py @@ -16,7 +16,7 @@ # along with this program. If not, see . import os - +from aiohttp.web import HTTPConflict from ...web.route import Route from ...modules.port_manager import PortManager @@ -255,7 +255,8 @@ class IOUHandler: status_codes={ 200: "Capture started", 400: "Invalid request", - 404: "Instance doesn't exist" + 404: "Instance doesn't exist", + 409: "VM not started" }, description="Start a packet capture on a IOU VM instance", input=IOU_CAPTURE_SCHEMA) @@ -266,8 +267,11 @@ class IOUHandler: adapter_number = int(request.match_info["adapter_number"]) port_number = int(request.match_info["port_number"]) pcap_file_path = os.path.join(vm.project.capture_working_directory(), request.json["capture_file_name"]) + + if not vm.is_running(): + raise HTTPConflict(text="You can't capture the traffic on a non started VM") yield from vm.start_capture(adapter_number, port_number, pcap_file_path, request.json["data_link_type"]) - response.json({"pcap_file_path": pcap_file_path}) + response.json({"pcap_file_path": str(pcap_file_path)}) @Route.post( r"/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/stop_capture", @@ -280,13 +284,18 @@ class IOUHandler: status_codes={ 204: "Capture stopped", 400: "Invalid request", - 404: "Instance doesn't exist" + 404: "Instance doesn't exist", + 409: "VM not started" }, description="Stop a packet capture on a IOU VM instance") def stop_capture(request, response): iou_manager = IOU.instance() vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + + if not vm.is_running(): + raise HTTPConflict(text="You can't capture the traffic on a non started VM") + adapter_number = int(request.match_info["adapter_number"]) port_number = int(request.match_info["port_number"]) yield from vm.stop_capture(adapter_number, port_number) diff --git a/tests/handlers/api/test_iou.py b/tests/handlers/api/test_iou.py index 5221f0eb..8aa059f5 100644 --- a/tests/handlers/api/test_iou.py +++ b/tests/handlers/api/test_iou.py @@ -18,8 +18,9 @@ import pytest import os import stat + from tests.utils import asyncio_patch -from unittest.mock import patch +from unittest.mock import patch, MagicMock, PropertyMock @pytest.fixture @@ -207,26 +208,53 @@ def test_iou_delete_nio(server, vm): assert response.route == "/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" -def test_iou_start_capture(server, vm, tmpdir): +def test_iou_start_capture(server, vm, tmpdir, project): - with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM.start_capture", return_value=True) as mock: + with patch("gns3server.modules.iou.iou_vm.IOUVM.is_running", return_value=True) as mock: + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM.start_capture") as start_capture: - params = {"capture_file_name": "test.pcap", "data_link_type": "DLT_EN10MB"} - response = server.post("/projects/{project_id}/iou/vms/{vm_id}/adapters/0/ports/0/start_capture".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), body=params, example=True) + params = {"capture_file_name": "test.pcap", "data_link_type": "DLT_EN10MB"} + response = server.post("/projects/{project_id}/iou/vms/{vm_id}/adapters/0/ports/0/start_capture".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), body=params, example=True) - assert mock.called - assert response.status == 200 - assert "test.pcap" in response.json["pcap_file_path"] + assert response.status == 200 + assert start_capture.called + assert "test.pcap" in response.json["pcap_file_path"] -def test_iou_stop_capture(server, vm): - with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM.stop_capture", return_value=True) as mock: +def test_iou_start_capture_not_started(server, vm, tmpdir): - response = server.post("/projects/{project_id}/iou/vms/{vm_id}/adapters/0/ports/0/stop_capture".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) + with patch("gns3server.modules.iou.iou_vm.IOUVM.is_running", return_value=False) as mock: + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM.start_capture") as start_capture: - assert mock.called - assert response.status == 204 + params = {"capture_file_name": "test.pcap", "data_link_type": "DLT_EN10MB"} + response = server.post("/projects/{project_id}/iou/vms/{vm_id}/adapters/0/ports/0/start_capture".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), body=params) + + assert not start_capture.called + assert response.status == 409 + + +def test_iou_stop_capture(server, vm, tmpdir, project): + + with patch("gns3server.modules.iou.iou_vm.IOUVM.is_running", return_value=True) as mock: + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM.stop_capture") as stop_capture: + + response = server.post("/projects/{project_id}/iou/vms/{vm_id}/adapters/0/ports/0/stop_capture".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) + + assert response.status == 204 + + assert stop_capture.called + + +def test_iou_stop_capture_not_started(server, vm, tmpdir): + + with patch("gns3server.modules.iou.iou_vm.IOUVM.is_running", return_value=False) as mock: + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM.stop_capture") as stop_capture: + + response = server.post("/projects/{project_id}/iou/vms/{vm_id}/adapters/0/ports/0/stop_capture".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + + assert not stop_capture.called + assert response.status == 409 def test_get_initial_config_without_config_file(server, vm): From e37392c482f635c275d3e35e7d9a1d369f1209e3 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 6 Mar 2015 15:48:16 +0100 Subject: [PATCH 396/485] Correctly recover id when closing VMS Fixes #91 --- gns3server/modules/iou/__init__.py | 5 +++-- gns3server/modules/project.py | 5 +---- gns3server/modules/vpcs/__init__.py | 5 +++-- tests/modules/test_project.py | 12 +++++------- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/gns3server/modules/iou/__init__.py b/gns3server/modules/iou/__init__.py index 1a7211e5..3cdddfe7 100644 --- a/gns3server/modules/iou/__init__.py +++ b/gns3server/modules/iou/__init__.py @@ -46,13 +46,14 @@ class IOU(BaseManager): return vm @asyncio.coroutine - def delete_vm(self, vm_id, *args, **kwargs): + def close_vm(self, vm_id, *args, **kwargs): vm = self.get_vm(vm_id) i = self._used_application_ids[vm_id] self._free_application_ids.insert(0, i) del self._used_application_ids[vm_id] - yield from super().delete_vm(vm_id, *args, **kwargs) + yield from super().close_vm(vm_id, *args, **kwargs) + return vm def get_application_id(self, vm_id): """ diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index 0c622063..a6a48a66 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -274,10 +274,7 @@ class Project: tasks = [] for vm in self._vms: - if asyncio.iscoroutinefunction(vm.close): - tasks.append(asyncio.async(vm.close())) - else: - vm.close() + tasks.append(asyncio.async(vm.manager.close_vm(vm.id))) if tasks: done, _ = yield from asyncio.wait(tasks) diff --git a/gns3server/modules/vpcs/__init__.py b/gns3server/modules/vpcs/__init__.py index 03250c55..bae14fe1 100644 --- a/gns3server/modules/vpcs/__init__.py +++ b/gns3server/modules/vpcs/__init__.py @@ -47,13 +47,14 @@ class VPCS(BaseManager): return vm @asyncio.coroutine - def delete_vm(self, vm_id, *args, **kwargs): + def close_vm(self, vm_id, *args, **kwargs): vm = self.get_vm(vm_id) i = self._used_mac_ids[vm_id] self._free_mac_ids[vm.project.id].insert(0, i) del self._used_mac_ids[vm_id] - yield from super().delete_vm(vm_id, *args, **kwargs) + yield from super().close_vm(vm_id, *args, **kwargs) + return vm def get_mac_id(self, vm_id): """ diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py index a647508c..eb52dcef 100644 --- a/tests/modules/test_project.py +++ b/tests/modules/test_project.py @@ -37,8 +37,9 @@ def manager(port_manager): @pytest.fixture(scope="function") -def vm(project, manager): - return VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) +def vm(project, manager, loop): + vm = manager.create_vm("test", project.id, "00010203-0405-0607-0809-0a0b0c0d0e0f") + return loop.run_until_complete(asyncio.async(vm)) def test_affect_uuid(): @@ -180,11 +181,8 @@ def test_project_add_vm(manager): assert len(project.vms) == 1 -def test_project_close(loop, manager): - project = Project() - vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) - project.add_vm(vm) - vm.manager._vms = {vm.id: vm} +def test_project_close(loop, vm, project): + with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM.close") as mock: loop.run_until_complete(asyncio.async(project.close())) assert mock.called From b58f9e10f9dd29c78ac3e94b698904ade72bddcc Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 6 Mar 2015 10:34:02 -0700 Subject: [PATCH 397/485] Bump version to 1.3.0beta1.dev1 --- gns3server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/version.py b/gns3server/version.py index 27c59e37..b7a55632 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,5 +23,5 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "1.3.0alpha1" +__version__ = "1.3.0beta1.dev1" __version_info__ = (1, 3, 0, -99) From 053fd9cc0c70ee51b799cb1663dc30f67f971bd8 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 6 Mar 2015 11:20:28 -0700 Subject: [PATCH 398/485] Adds warnings if the cacert.pem file cannot be found. --- gns3server/crash_report.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py index ac930723..cf27b8d1 100644 --- a/gns3server/crash_report.py +++ b/gns3server/crash_report.py @@ -37,7 +37,10 @@ class CrashReport: DSN = "sync+https://50af75d8641d4ea7a4ea6b38c7df6cf9:41d54936f8f14e558066262e2ec8bbeb@app.getsentry.com/38482" if hasattr(sys, "frozen"): cacert = os.path.join(os.getcwd(), "cacert.pem") - DSN += "?ca_certs={}".format(cacert) + if os.path.isfile(cacert): + DSN += "?ca_certs={}".format(cacert) + else: + log.warning("The SSL certificate bundle file could not be found".format(cacert)) _instance = None def __init__(self): From f188bc43e14047f4ef33989c0291be5156fafa40 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 6 Mar 2015 11:25:25 -0700 Subject: [PATCH 399/485] Includes SSL cacert file path in the warnings. --- gns3server/crash_report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py index cf27b8d1..b8260568 100644 --- a/gns3server/crash_report.py +++ b/gns3server/crash_report.py @@ -40,7 +40,7 @@ class CrashReport: if os.path.isfile(cacert): DSN += "?ca_certs={}".format(cacert) else: - log.warning("The SSL certificate bundle file could not be found".format(cacert)) + log.warning("The SSL certificate bundle file '{}' could not be found".format(cacert)) _instance = None def __init__(self): From d87ebb3ed2f2a8ccd70951f841e3eddf730b812d Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 6 Mar 2015 15:16:19 -0700 Subject: [PATCH 400/485] Fixes suspend and resume for Qemu. --- gns3server/modules/qemu/qemu_vm.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index 85a9537f..1732417a 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -537,7 +537,7 @@ class QemuVM(BaseVM): if self.is_running(): # resume the VM if it is paused - self.resume() + yield from self.resume() return else: @@ -598,7 +598,7 @@ class QemuVM(BaseVM): if self.is_running() and self._monitor: log.debug("Execute QEMU monitor command: {}".format(command)) try: - reader, writer = yield from asyncio.open_connection("127.0.0.1", self._monitor) + reader, writer = yield from asyncio.open_connection(self._monitor_host, self._monitor) except OSError as e: log.warn("Could not connect to QEMU monitor: {}".format(e)) return result @@ -616,7 +616,7 @@ class QemuVM(BaseVM): break for expect in expected: if expect in line: - result = line + result = line.decode().strip() break except EOFError as e: log.warn("Could not read from QEMU monitor: {}".format(e)) @@ -644,7 +644,7 @@ class QemuVM(BaseVM): """ result = yield from self._control_vm("info status", [b"running", b"paused"]) - return result + return result.rsplit(' ', 1)[1] @asyncio.coroutine def suspend(self): From ee578d3c12f9b0facd7fa103d6efaa5a4b397f0e Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 6 Mar 2015 20:08:00 -0700 Subject: [PATCH 401/485] Fixes Qemu networking. --- gns3server/handlers/api/qemu_handler.py | 8 +++--- gns3server/handlers/api/virtualbox_handler.py | 2 +- gns3server/modules/qemu/qemu_vm.py | 28 +++++++++++-------- tests/modules/qemu/test_qemu_vm.py | 14 +++++----- 4 files changed, 28 insertions(+), 24 deletions(-) diff --git a/gns3server/handlers/api/qemu_handler.py b/gns3server/handlers/api/qemu_handler.py index 85425568..20f72e33 100644 --- a/gns3server/handlers/api/qemu_handler.py +++ b/gns3server/handlers/api/qemu_handler.py @@ -236,7 +236,7 @@ class QEMUHandler: "project_id": "UUID for the project", "vm_id": "UUID for the instance", "adapter_number": "Network adapter where the nio is located", - "port_number": "Port where the nio should be added" + "port_number": "Port on the adapter (always 0)" }, status_codes={ 201: "NIO created", @@ -251,7 +251,7 @@ class QEMUHandler: qemu_manager = Qemu.instance() vm = qemu_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) nio = qemu_manager.create_nio(vm.qemu_path, request.json) - yield from vm.adapter_add_nio_binding(int(request.match_info["adapter_number"]), int(request.match_info["port_number"]), nio) + yield from vm.adapter_add_nio_binding(int(request.match_info["adapter_number"]), nio) response.set_status(201) response.json(nio) @@ -262,7 +262,7 @@ class QEMUHandler: "project_id": "UUID for the project", "vm_id": "UUID for the instance", "adapter_number": "Network adapter where the nio is located", - "port_number": "Port from where the nio should be removed" + "port_number": "Port on the adapter (always 0)" }, status_codes={ 204: "NIO deleted", @@ -274,7 +274,7 @@ class QEMUHandler: qemu_manager = Qemu.instance() vm = qemu_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) - yield from vm.adapter_remove_nio_binding(int(request.match_info["adapter_number"]), int(request.match_info["port_number"])) + yield from vm.adapter_remove_nio_binding(int(request.match_info["adapter_number"])) response.set_status(204) @classmethod diff --git a/gns3server/handlers/api/virtualbox_handler.py b/gns3server/handlers/api/virtualbox_handler.py index 441ab6df..e37d9453 100644 --- a/gns3server/handlers/api/virtualbox_handler.py +++ b/gns3server/handlers/api/virtualbox_handler.py @@ -290,7 +290,7 @@ class VirtualBoxHandler: "project_id": "UUID for the project", "vm_id": "UUID for the instance", "adapter_number": "Adapter from where the nio should be removed", - "port_number": "Port on the adapter (always)" + "port_number": "Port on the adapter (always 0)" }, status_codes={ 204: "NIO deleted", diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index 1732417a..dc29ced6 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -598,6 +598,7 @@ class QemuVM(BaseVM): if self.is_running() and self._monitor: log.debug("Execute QEMU monitor command: {}".format(command)) try: + log.info("Connecting to Qemu monitor on {}:{}".format(self._monitor_host, self._monitor)) reader, writer = yield from asyncio.open_connection(self._monitor_host, self._monitor) except OSError as e: log.warn("Could not connect to QEMU monitor: {}".format(e)) @@ -682,13 +683,12 @@ class QemuVM(BaseVM): log.info("QEMU VM is not paused to be resumed, current status is {}".format(vm_status)) @asyncio.coroutine - def adapter_add_nio_binding(self, adapter_id, port_id, nio): + def adapter_add_nio_binding(self, adapter_id, nio): """ Adds a port NIO binding. :param adapter_id: adapter ID - :param port_id: port ID - :param nio: NIO instance to add to the slot/port + :param nio: NIO instance to add to the adapter """ try: @@ -708,13 +708,16 @@ class QemuVM(BaseVM): nio.rport, nio.rhost)) else: - yield from self._control_vm("host_net_remove {} gns3-{}".format(adapter_id, adapter_id)) - yield from self._control_vm("host_net_add socket vlan={},name=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_id, - adapter_id, - nio.rhost, - nio.rport, - self._host, - nio.lport)) + # FIXME: does it work? very undocumented feature... + # Apparently there is a bug in Qemu... + # netdev_add [user|tap|socket|hubport|netmap],id=str[,prop=value][,...] -- add host network device + # netdev_del id -- remove host network device + yield from self._control_vm("netdev_del gns3-{}".format(adapter_id)) + yield from self._control_vm("netdev_add socket,id=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_id, + nio.rhost, + nio.rport, + self._host, + nio.lport)) adapter.add_nio(0, nio) log.info("QEMU VM {name} [id={id}]: {nio} added to adapter {adapter_id}".format(name=self._name, @@ -723,12 +726,11 @@ class QemuVM(BaseVM): adapter_id=adapter_id)) @asyncio.coroutine - def adapter_remove_nio_binding(self, adapter_id, port_id): + def adapter_remove_nio_binding(self, adapter_id): """ Removes a port NIO binding. :param adapter_id: adapter ID - :param port_id: port ID :returns: NIO instance """ @@ -745,6 +747,8 @@ class QemuVM(BaseVM): yield from self._control_vm("host_net_add user vlan={},name=gns3-{}".format(adapter_id, adapter_id)) nio = adapter.get_nio(0) + if isinstance(nio, NIOUDP): + self.manager.port_manager.release_udp_port(nio.lport) adapter.remove_nio(0) log.info("QEMU VM {name} [id={id}]: {nio} removed from adapter {adapter_id}".format(name=self._name, id=self._id, diff --git a/tests/modules/qemu/test_qemu_vm.py b/tests/modules/qemu/test_qemu_vm.py index c0b6d1ee..e13bdc27 100644 --- a/tests/modules/qemu/test_qemu_vm.py +++ b/tests/modules/qemu/test_qemu_vm.py @@ -102,7 +102,7 @@ def test_stop(loop, vm, running_subprocess_mock): with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): nio = Qemu.instance().create_nio(vm.qemu_path, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) - vm.adapter_add_nio_binding(0, 0, nio) + vm.adapter_add_nio_binding(0, nio) loop.run_until_complete(asyncio.async(vm.start())) assert vm.is_running() loop.run_until_complete(asyncio.async(vm.stop())) @@ -128,21 +128,21 @@ def test_suspend(loop, vm): def test_add_nio_binding_udp(vm, loop): nio = Qemu.instance().create_nio(vm.qemu_path, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) - loop.run_until_complete(asyncio.async(vm.adapter_add_nio_binding(0, 0, nio))) + loop.run_until_complete(asyncio.async(vm.adapter_add_nio_binding(0, nio))) assert nio.lport == 4242 def test_add_nio_binding_ethernet(vm, loop): with patch("gns3server.modules.base_manager.BaseManager._has_privileged_access", return_value=True): nio = Qemu.instance().create_nio(vm.qemu_path, {"type": "nio_generic_ethernet", "ethernet_device": "eth0"}) - loop.run_until_complete(asyncio.async(vm.adapter_add_nio_binding(0, 0, nio))) + loop.run_until_complete(asyncio.async(vm.adapter_add_nio_binding(0, nio))) assert nio.ethernet_device == "eth0" def test_port_remove_nio_binding(vm, loop): nio = Qemu.instance().create_nio(vm.qemu_path, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) - loop.run_until_complete(asyncio.async(vm.adapter_add_nio_binding(0, 0, nio))) - loop.run_until_complete(asyncio.async(vm.adapter_remove_nio_binding(0, 0))) + loop.run_until_complete(asyncio.async(vm.adapter_add_nio_binding(0, nio))) + loop.run_until_complete(asyncio.async(vm.adapter_remove_nio_binding(0))) assert vm._ethernet_adapters[0].ports[0] is None @@ -244,10 +244,10 @@ def test_control_vm_expect_text(vm, loop, running_subprocess_mock): with asyncio_patch("asyncio.open_connection", return_value=(reader, writer)) as open_connect: future = asyncio.Future() - future.set_result("epic product") + future.set_result(b"epic product") reader.readline.return_value = future - res = loop.run_until_complete(asyncio.async(vm._control_vm("test", ["epic"]))) + res = loop.run_until_complete(asyncio.async(vm._control_vm("test", [b"epic"]))) assert writer.write.called_with("test") assert res == "epic product" From 1b68a54234d4f9fd75188f394e9833aedb41cff0 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Sat, 7 Mar 2015 11:56:07 +0100 Subject: [PATCH 402/485] Look for qemu images in ~/GNS3/images --- gns3server/modules/qemu/qemu_vm.py | 9 +++++++++ tests/handlers/api/test_qemu.py | 8 ++++---- tests/modules/qemu/test_qemu_vm.py | 18 ++++++++++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index dc29ced6..89ac9f45 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -32,6 +32,7 @@ from ..adapters.ethernet_adapter import EthernetAdapter from ..nios.nio_udp import NIOUDP from ..base_vm import BaseVM from ...schemas.qemu import QEMU_OBJECT_SCHEMA +from ...config import Config import logging log = logging.getLogger(__name__) @@ -182,6 +183,10 @@ class QemuVM(BaseVM): :param hda_disk_image: QEMU hda disk image path """ + if hda_disk_image[0] != "/": + server_config = Config.instance().get_section_config("Server") + hda_disk_image = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), hda_disk_image) + log.info("QEMU VM {name} [id={id}] has set the QEMU hda disk image path to {disk_image}".format(name=self._name, id=self._id, disk_image=hda_disk_image)) @@ -205,6 +210,10 @@ class QemuVM(BaseVM): :param hdb_disk_image: QEMU hdb disk image path """ + if hdb_disk_image[0] != "/": + server_config = Config.instance().get_section_config("Server") + hdb_disk_image = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), hdb_disk_image) + log.info("QEMU VM {name} [id={id}] has set the QEMU hdb disk image path to {disk_image}".format(name=self._name, id=self._id, disk_image=hdb_disk_image)) diff --git a/tests/handlers/api/test_qemu.py b/tests/handlers/api/test_qemu.py index 12a72e87..b06189f5 100644 --- a/tests/handlers/api/test_qemu.py +++ b/tests/handlers/api/test_qemu.py @@ -56,7 +56,7 @@ def test_qemu_create(server, project, base_params): def test_qemu_create_with_params(server, project, base_params): params = base_params params["ram"] = 1024 - params["hda_disk_image"] = "hda" + params["hda_disk_image"] = "/tmp/hda" response = server.post("/projects/{project_id}/qemu/vms".format(project_id=project.id), params, example=True) assert response.status == 201 @@ -64,7 +64,7 @@ def test_qemu_create_with_params(server, project, base_params): assert response.json["name"] == "PC TEST 1" assert response.json["project_id"] == project.id assert response.json["ram"] == 1024 - assert response.json["hda_disk_image"] == "hda" + assert response.json["hda_disk_image"] == "/tmp/hda" def test_qemu_get(server, project, vm): @@ -122,13 +122,13 @@ def test_qemu_update(server, vm, tmpdir, free_console_port, project): "name": "test", "console": free_console_port, "ram": 1024, - "hdb_disk_image": "hdb" + "hdb_disk_image": "/tmp/hdb" } response = server.put("/projects/{project_id}/qemu/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), params, example=True) assert response.status == 200 assert response.json["name"] == "test" assert response.json["console"] == free_console_port - assert response.json["hdb_disk_image"] == "hdb" + assert response.json["hdb_disk_image"] == "/tmp/hdb" assert response.json["ram"] == 1024 diff --git a/tests/modules/qemu/test_qemu_vm.py b/tests/modules/qemu/test_qemu_vm.py index e13bdc27..71c5d70b 100644 --- a/tests/modules/qemu/test_qemu_vm.py +++ b/tests/modules/qemu/test_qemu_vm.py @@ -284,3 +284,21 @@ def test_build_command_without_display(vm, loop, fake_qemu_binary): with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()) as process: cmd = loop.run_until_complete(asyncio.async(vm._build_command())) assert "-nographic" in cmd + + +def test_hda_disk_image(vm, tmpdir): + + with patch("gns3server.config.Config.get_section_config", return_value={"images_path": str(tmpdir)}): + vm.hda_disk_image = "/tmp/test" + assert vm.hda_disk_image == "/tmp/test" + vm.hda_disk_image = "test" + assert vm.hda_disk_image == str(tmpdir / "test") + + +def test_hdb_disk_image(vm, tmpdir): + + with patch("gns3server.config.Config.get_section_config", return_value={"images_path": str(tmpdir)}): + vm.hdb_disk_image = "/tmp/test" + assert vm.hdb_disk_image == "/tmp/test" + vm.hdb_disk_image = "test" + assert vm.hdb_disk_image == str(tmpdir / "test") From d126db1fe94e7826ebad970619ece383e7f7e9f7 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Sat, 7 Mar 2015 13:52:40 +0100 Subject: [PATCH 403/485] The upload interfaces allow user to choose an image type --- gns3server/handlers/upload_handler.py | 8 ++++++-- gns3server/templates/upload.html | 7 ++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/gns3server/handlers/upload_handler.py b/gns3server/handlers/upload_handler.py index 0df82392..2ca5eb1e 100644 --- a/gns3server/handlers/upload_handler.py +++ b/gns3server/handlers/upload_handler.py @@ -55,9 +55,13 @@ class UploadHandler: response.redirect("/upload") return - destination_path = os.path.join(UploadHandler.image_directory(), data["file"].filename) + if data["type"] not in ["IOU", "QEMU", "IOS"]: + raise HTTPForbidden("You are not authorized to upload this kind of image {}".format(data["type"])) + + destination_dir = os.path.join(UploadHandler.image_directory(), data["type"]) + destination_path = os.path.join(destination_dir, data["file"].filename) try: - os.makedirs(UploadHandler.image_directory(), exist_ok=True) + os.makedirs(destination_dir, exist_ok=True) with open(destination_path, "wb+") as f: chunk = data["file"].file.read() f.write(chunk) diff --git a/gns3server/templates/upload.html b/gns3server/templates/upload.html index 0cd9fbcc..1b230474 100644 --- a/gns3server/templates/upload.html +++ b/gns3server/templates/upload.html @@ -2,7 +2,12 @@ {% block body %}

Select & Upload an image for GNS3

- File: + File:
+ Image type:

From ed2e4e43f223bc5449f5cde03edf3c8aeb565202 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Sat, 7 Mar 2015 14:27:09 +0100 Subject: [PATCH 404/485] Support the options use_default_iou_values Fix #92 --- gns3server/modules/iou/iou_vm.py | 3 ++- gns3server/schemas/iou.py | 14 +++++++++++++- tests/handlers/api/test_iou.py | 7 ++++++- tests/handlers/test_upload.py | 3 ++- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index e80c99a3..f93ccc7d 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -205,7 +205,8 @@ class IOUVM(BaseVM): "ram": self._ram, "nvram": self._nvram, "l1_keepalives": self._l1_keepalives, - "initial_config": self.relative_initial_config_file + "initial_config": self.relative_initial_config_file, + "use_default_iou_values": self._use_default_iou_values } @property diff --git a/gns3server/schemas/iou.py b/gns3server/schemas/iou.py index 9a19c229..df04039a 100644 --- a/gns3server/schemas/iou.py +++ b/gns3server/schemas/iou.py @@ -66,6 +66,10 @@ IOU_CREATE_SCHEMA = { "description": "Always up ethernet interface", "type": ["boolean", "null"] }, + "use_default_iou_values": { + "description": "Use default IOU values", + "type": ["boolean", "null"] + }, "initial_config_content": { "description": "Initial configuration of the IOU", "type": ["string", "null"] @@ -118,6 +122,10 @@ IOU_UPDATE_SCHEMA = { "initial_config_content": { "description": "Initial configuration of the IOU", "type": ["string", "null"] + }, + "use_default_iou_values": { + "description": "Use default IOU values", + "type": ["boolean", "null"] } }, "additionalProperties": False, @@ -180,10 +188,14 @@ IOU_OBJECT_SCHEMA = { "initial_config": { "description": "Path of the initial config content relative to project directory", "type": ["string", "null"] + }, + "use_default_iou_values": { + "description": "Use default IOU values", + "type": ["boolean", "null"] } }, "additionalProperties": False, - "required": ["name", "vm_id", "console", "project_id", "path", "serial_adapters", "ethernet_adapters", "ram", "nvram", "l1_keepalives", "initial_config"] + "required": ["name", "vm_id", "console", "project_id", "path", "serial_adapters", "ethernet_adapters", "ram", "nvram", "l1_keepalives", "initial_config", "use_default_iou_values"] } IOU_NIO_SCHEMA = { diff --git a/tests/handlers/api/test_iou.py b/tests/handlers/api/test_iou.py index 8aa059f5..ecf07f78 100644 --- a/tests/handlers/api/test_iou.py +++ b/tests/handlers/api/test_iou.py @@ -74,6 +74,7 @@ def test_iou_create_with_params(server, project, base_params): params["ethernet_adapters"] = 0 params["l1_keepalives"] = True params["initial_config_content"] = "hostname test" + params["use_default_iou_values"] = True response = server.post("/projects/{project_id}/iou/vms".format(project_id=project.id), params, example=True) assert response.status == 201 @@ -85,6 +86,8 @@ def test_iou_create_with_params(server, project, base_params): assert response.json["ram"] == 1024 assert response.json["nvram"] == 512 assert response.json["l1_keepalives"] is True + assert response.json["use_default_iou_values"] is True + assert "initial-config.cfg" in response.json["initial_config"] with open(initial_config_file(project, response.json)) as f: assert f.read() == params["initial_config_content"] @@ -140,7 +143,8 @@ def test_iou_update(server, vm, tmpdir, free_console_port, project): "ethernet_adapters": 4, "serial_adapters": 0, "l1_keepalives": True, - "initial_config_content": "hostname test" + "initial_config_content": "hostname test", + "use_default_iou_values": True } response = server.put("/projects/{project_id}/iou/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), params, example=True) assert response.status == 200 @@ -151,6 +155,7 @@ def test_iou_update(server, vm, tmpdir, free_console_port, project): assert response.json["ram"] == 512 assert response.json["nvram"] == 2048 assert response.json["l1_keepalives"] is True + assert response.json["use_default_iou_values"] is True assert "initial-config.cfg" in response.json["initial_config"] with open(initial_config_file(project, response.json)) as f: assert f.read() == "hostname test" diff --git a/tests/handlers/test_upload.py b/tests/handlers/test_upload.py index cec27948..4e3cebc5 100644 --- a/tests/handlers/test_upload.py +++ b/tests/handlers/test_upload.py @@ -36,12 +36,13 @@ def test_upload(server, tmpdir): with open(str(tmpdir / "test"), "w+") as f: f.write("TEST") body = aiohttp.FormData() + body.add_field("type", "QEMU") body.add_field("file", open(str(tmpdir / "test"), "rb"), content_type="application/iou", filename="test2") with patch("gns3server.config.Config.get_section_config", return_value={"images_path": str(tmpdir)}): response = server.post('/upload', api_version=None, body=body, raw=True) - with open(str(tmpdir / "test2")) as f: + with open(str(tmpdir / "QEMU" / "test2")) as f: assert f.read() == "TEST" assert "test2" in response.body.decode("utf-8") From be1e0fa1f20e40eea5f0844145781d285ba70e14 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Sat, 7 Mar 2015 14:38:38 +0100 Subject: [PATCH 405/485] Upload IOURC file via the web interface --- gns3server/handlers/upload_handler.py | 10 +++++++--- gns3server/templates/upload.html | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/gns3server/handlers/upload_handler.py b/gns3server/handlers/upload_handler.py index 2ca5eb1e..a4a9396e 100644 --- a/gns3server/handlers/upload_handler.py +++ b/gns3server/handlers/upload_handler.py @@ -55,11 +55,15 @@ class UploadHandler: response.redirect("/upload") return - if data["type"] not in ["IOU", "QEMU", "IOS"]: + if data["type"] not in ["IOU", "IOURC", "QEMU", "IOS"]: raise HTTPForbidden("You are not authorized to upload this kind of image {}".format(data["type"])) - destination_dir = os.path.join(UploadHandler.image_directory(), data["type"]) - destination_path = os.path.join(destination_dir, data["file"].filename) + if data["type"] == "IOURC": + destination_dir = os.path.expanduser("~/") + destination_path = os.path.join(destination_dir, ".iourc") + else: + destination_dir = os.path.join(UploadHandler.image_directory(), data["type"]) + destination_path = os.path.join(destination_dir, data["file"].filename) try: os.makedirs(destination_dir, exist_ok=True) with open(destination_path, "wb+") as f: diff --git a/gns3server/templates/upload.html b/gns3server/templates/upload.html index 1b230474..eeeeae55 100644 --- a/gns3server/templates/upload.html +++ b/gns3server/templates/upload.html @@ -5,6 +5,7 @@ File:
Image type: From 0d379f428e5c483e72a7a660920bee569550bd8c Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 7 Mar 2015 18:16:46 -0700 Subject: [PATCH 406/485] Makes absolute path checks work on Windows. --- gns3server/modules/dynamips/nodes/router.py | 3 +-- gns3server/modules/iou/iou_vm.py | 2 +- gns3server/modules/qemu/qemu_vm.py | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index aff7683b..706fc319 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -421,8 +421,7 @@ class Router(BaseVM): :param image: path to IOS image file """ - if not os.path.dirname(image): - # this must be a relative path + if not os.path.isabs(image): server_config = Config.instance().get_section_config("Server") image = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), image) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index f93ccc7d..ea90f446 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -131,7 +131,7 @@ class IOUVM(BaseVM): :params path: Path to the binary """ - if path[0] != "/": + if not os.path.isabs(path): server_config = Config.instance().get_section_config("Server") path = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), path) diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index 89ac9f45..a270e0f3 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -183,7 +183,7 @@ class QemuVM(BaseVM): :param hda_disk_image: QEMU hda disk image path """ - if hda_disk_image[0] != "/": + if os.path.isabs(hda_disk_image): server_config = Config.instance().get_section_config("Server") hda_disk_image = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), hda_disk_image) @@ -210,7 +210,7 @@ class QemuVM(BaseVM): :param hdb_disk_image: QEMU hdb disk image path """ - if hdb_disk_image[0] != "/": + if not os.path.isabs(hdb_disk_image): server_config = Config.instance().get_section_config("Server") hdb_disk_image = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), hdb_disk_image) From 2b34e35027dc30b8adc3967b6b1416b1cfaafd65 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 7 Mar 2015 20:19:52 -0700 Subject: [PATCH 407/485] Pypi doesn't like Python 3.5... yet Upload failed (400): Invalid classifier "Programming Language :: Python :: 3.5" --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 37fd5902..a7fd1da1 100644 --- a/setup.py +++ b/setup.py @@ -74,7 +74,6 @@ setup( "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: Implementation :: CPython", ], ) From c3014632a4e600b25d48eae4fde122e618b9f855 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 8 Mar 2015 12:32:36 -0600 Subject: [PATCH 408/485] Fixes rename bug for linked clones in VirtualBox. --- gns3server/handlers/api/config_handler.py | 2 -- gns3server/handlers/api/virtualbox_handler.py | 7 +++++-- .../modules/virtualbox/virtualbox_vm.py | 20 +++++++------------ 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/gns3server/handlers/api/config_handler.py b/gns3server/handlers/api/config_handler.py index 0025731e..fc1a61d0 100644 --- a/gns3server/handlers/api/config_handler.py +++ b/gns3server/handlers/api/config_handler.py @@ -19,8 +19,6 @@ from ...web.route import Route from ...config import Config from aiohttp.web import HTTPForbidden -import asyncio - class ConfigHandler: diff --git a/gns3server/handlers/api/virtualbox_handler.py b/gns3server/handlers/api/virtualbox_handler.py index e37d9453..f8a71845 100644 --- a/gns3server/handlers/api/virtualbox_handler.py +++ b/gns3server/handlers/api/virtualbox_handler.py @@ -121,6 +121,11 @@ class VirtualBoxHandler: vbox_manager = VirtualBox.instance() vm = vbox_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + if "vmname" in request.json: + vmname = request.json.pop("vmname") + if vmname != vm.vmname: + yield from vm.set_vmname(vmname) + if "enable_remote_console" in request.json: yield from vm.set_enable_remote_console(request.json.pop("enable_remote_console")) @@ -132,8 +137,6 @@ class VirtualBoxHandler: for name, value in request.json.items(): if hasattr(vm, name) and getattr(vm, name) != value: setattr(vm, name, value) - if name == "vmname": - yield from vm.rename_in_virtualbox() response.json(vm) diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 801d9bb4..7eea8d12 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -410,33 +410,27 @@ class VirtualBoxVM(BaseVM): @property def vmname(self): """ - Returns the VM name associated with this VirtualBox VM. + Returns the VirtualBox VM name. :returns: VirtualBox VM name """ return self._vmname - @vmname.setter - def vmname(self, vmname): + @asyncio.coroutine + def set_vmname(self, vmname): """ - Sets the VM name associated with this VirtualBox VM. + Renames the VirtualBox VM. :param vmname: VirtualBox VM name """ + if self._linked_clone: + yield from self._modify_vm('--name "{}"'.format(vmname)) + log.info("VirtualBox VM '{name}' [{id}] has set the VM name to '{vmname}'".format(name=self.name, id=self.id, vmname=vmname)) self._vmname = vmname - @asyncio.coroutine - def rename_in_virtualbox(self): - """ - Renames the VirtualBox VM. - """ - - if self._linked_clone: - yield from self._modify_vm('--name "{}"'.format(self._vmname)) - @property def adapters(self): """ From b1eccc0ace928776a139dbe687e98ebd0bf755d8 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 8 Mar 2015 14:13:19 -0600 Subject: [PATCH 409/485] Properly restore configs for Dynamips routers. --- gns3server/modules/dynamips/__init__.py | 25 ++++++++++++--------- gns3server/modules/dynamips/nodes/router.py | 22 +----------------- 2 files changed, 16 insertions(+), 31 deletions(-) diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index 21db9cbd..ed538f23 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -458,10 +458,8 @@ class Dynamips(BaseManager): if hasattr(vm, name) and getattr(vm, name) != value: if hasattr(vm, "set_{}".format(name)): setter = getattr(vm, "set_{}".format(name)) - if asyncio.iscoroutinefunction(vm.close): - yield from setter(value) - else: - setter(value) + yield from setter(value) + elif name.startswith("slot") and value in ADAPTER_MATRIX: slot_id = int(name[-1]) adapter_name = value @@ -496,29 +494,36 @@ class Dynamips(BaseManager): yield from vm.set_sparsemem(False) # update the configs if needed - yield from self.create_vm_configs(vm, settings.get("startup_config_content"), settings.get("private_config_content")) + yield from self.create_vm_configs(vm, settings) @asyncio.coroutine - def create_vm_configs(self, vm, startup_config_content, private_config_content): + def create_vm_configs(self, vm, settings): """ Creates VM configs from pushed content. :param vm: VM instance - :param startup_config_content: content of the startup-config - :param private_config_content: content of the private-config + :param settings: VM settings """ module_workdir = vm.project.module_working_directory(self.module_name.lower()) default_startup_config_path = os.path.join(module_workdir, "configs", "i{}_startup-config.cfg".format(vm.dynamips_id)) default_private_config_path = os.path.join(module_workdir, "configs", "i{}_private-config.cfg".format(vm.dynamips_id)) + startup_config_content = settings.get("startup_config_content") if startup_config_content: startup_config_path = self._create_config(vm, startup_config_content, default_startup_config_path) - yield from vm.set_config(startup_config_path) + yield from vm.set_configs(startup_config_path) + else: + startup_config_path = settings.get("startup_config", "") + yield from vm.set_configs(startup_config_path) + private_config_content = settings.get("private_config_content") if private_config_content: private_config_path = self._create_config(vm, private_config_content, default_private_config_path) - yield from vm.set_config(vm.startup_config, private_config_path) + yield from vm.set_configs(vm.startup_config, private_config_path) + else: + private_config_path = settings.get("private_config", "") + yield from vm.set_configs(vm.startup_config, private_config_path) def _create_config(self, vm, content, path): """ diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index 706fc319..92079aad 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -1396,16 +1396,6 @@ class Router(BaseVM): return self._startup_config - @startup_config.setter - def startup_config(self, startup_config): - """ - Sets the startup-config for this router. - - :param startup_config: path to startup-config file - """ - - self._startup_config = startup_config - @property def private_config(self): """ @@ -1416,16 +1406,6 @@ class Router(BaseVM): return self._private_config - @private_config.setter - def private_config(self, private_config): - """ - Sets the private-config for this router. - - :param private_config: path to private-config file - """ - - self._private_config = private_config - @asyncio.coroutine def set_name(self, new_name): """ @@ -1466,7 +1446,7 @@ class Router(BaseVM): self._name = new_name @asyncio.coroutine - def set_config(self, startup_config, private_config=''): + def set_configs(self, startup_config, private_config=''): """ Sets the config files that are pushed to startup-config and private-config in NVRAM when the instance is started. From 2934232afb1f165d9c31bdbb60efd954c539909a Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 8 Mar 2015 17:45:29 -0600 Subject: [PATCH 410/485] Convert legacy IOU directories on remote servers. --- gns3server/modules/base_manager.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index f6b43175..49e9ace7 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -175,6 +175,17 @@ class BaseManager: raise aiohttp.web.HTTPInternalServerError(text="Could not move project files directory: {} to {} {}".format(legacy_project_files_path, new_project_files_path, e)) + legacy_iou_dir = os.path.join(project.path, "iou") + new_iou_dir = os.path.join(project.path, "project-files", "iou") + if os.path.exists(legacy_iou_dir) and not os.path.exists(new_iou_dir): + # move the iou dir on remote server + try: + log.info('Moving "{}" to "{}"'.format(legacy_iou_dir, new_iou_dir)) + yield from wait_run_in_executor(shutil.move, legacy_iou_dir, new_iou_dir) + except OSError as e: + raise aiohttp.web.HTTPInternalServerError(text="Could not move IOU directory: {} to {} {}".format(legacy_iou_dir, + new_iou_dir, e)) + if hasattr(self, "get_legacy_vm_workdir"): # rename old project VM working dir log.info("Converting old VM working directory...") From 95766fa30da94214eb8310eef6e5303fea766d7d Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 8 Mar 2015 19:13:01 -0600 Subject: [PATCH 411/485] Let the server know about the project name and convert old IOU projects on remote servers. --- gns3server/handlers/api/project_handler.py | 2 ++ gns3server/modules/base_manager.py | 25 +++++++++++----------- gns3server/modules/project.py | 22 ++++++++++++++++--- gns3server/modules/project_manager.py | 4 ++-- gns3server/schemas/project.py | 15 +++++++++++++ 5 files changed, 51 insertions(+), 17 deletions(-) diff --git a/gns3server/handlers/api/project_handler.py b/gns3server/handlers/api/project_handler.py index 14e40d52..e31d32bc 100644 --- a/gns3server/handlers/api/project_handler.py +++ b/gns3server/handlers/api/project_handler.py @@ -37,6 +37,7 @@ class ProjectHandler: pm = ProjectManager.instance() p = pm.create_project( + name=request.json.get("name"), path=request.json.get("path"), project_id=request.json.get("project_id"), temporary=request.json.get("temporary", False) @@ -81,6 +82,7 @@ class ProjectHandler: pm = ProjectManager.instance() project = pm.get_project(request.match_info["project_id"]) project.temporary = request.json.get("temporary", project.temporary) + project.name = request.json.get("name", project.name) project_path = request.json.get("path", project.path) if project_path != project.path: project.path = project_path diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 49e9ace7..9c642c7d 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -162,8 +162,7 @@ class BaseManager: """ new_id = str(uuid4()) - project_name = os.path.basename(project.path) - legacy_project_files_path = os.path.join(project.path, "{}-files".format(project_name)) + legacy_project_files_path = os.path.join(project.path, "{}-files".format(project.name)) new_project_files_path = os.path.join(project.path, "project-files") if os.path.exists(legacy_project_files_path) and not os.path.exists(new_project_files_path): # move the project files @@ -175,16 +174,18 @@ class BaseManager: raise aiohttp.web.HTTPInternalServerError(text="Could not move project files directory: {} to {} {}".format(legacy_project_files_path, new_project_files_path, e)) - legacy_iou_dir = os.path.join(project.path, "iou") - new_iou_dir = os.path.join(project.path, "project-files", "iou") - if os.path.exists(legacy_iou_dir) and not os.path.exists(new_iou_dir): - # move the iou dir on remote server - try: - log.info('Moving "{}" to "{}"'.format(legacy_iou_dir, new_iou_dir)) - yield from wait_run_in_executor(shutil.move, legacy_iou_dir, new_iou_dir) - except OSError as e: - raise aiohttp.web.HTTPInternalServerError(text="Could not move IOU directory: {} to {} {}".format(legacy_iou_dir, - new_iou_dir, e)) + if project.is_local() is False: + legacy_remote_iou_project = os.path.join(project.location, project.name, "iou") + new_iou_project_path = os.path.join(project.path, "project-files", "iou") + if os.path.exists(legacy_remote_iou_project) and not os.path.exists(new_iou_project_path): + # move the legacy remote IOU project (remote servers only) + log.info("Converting old remote IOU project...") + try: + log.info('Moving "{}" to "{}"'.format(legacy_remote_iou_project, new_iou_project_path)) + yield from wait_run_in_executor(shutil.move, legacy_remote_iou_project, new_iou_project_path) + except OSError as e: + raise aiohttp.web.HTTPInternalServerError(text="Could not move IOU directory: {} to {} {}".format(legacy_remote_iou_project, + new_iou_project_path, e)) if hasattr(self, "get_legacy_vm_workdir"): # rename old project VM working dir diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index a6a48a66..23c545bf 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -41,8 +41,9 @@ class Project: :param temporary: Boolean the project is a temporary project (destroy when closed) """ - def __init__(self, project_id=None, path=None, location=None, temporary=False): + def __init__(self, name=None, project_id=None, path=None, location=None, temporary=False): + self._name = name if project_id is None: self._id = str(uuid4()) else: @@ -75,6 +76,7 @@ class Project: def __json__(self): return { + "name": self._name, "project_id": self._id, "location": self._location, "temporary": self._temporary, @@ -85,6 +87,10 @@ class Project: return Config.instance().get_section_config("Server") + def is_local(self): + + return self._config().get("local", False) + @classmethod def _get_default_project_directory(cls): """ @@ -113,7 +119,7 @@ class Project: @location.setter def location(self, location): - if location != self._location and self._config().get("local", False) is False: + if location != self._location and self.is_local() is False: raise aiohttp.web.HTTPForbidden(text="You are not allowed to modify the project directory location") self._location = location @@ -127,12 +133,22 @@ class Project: def path(self, path): if hasattr(self, "_path"): - if path != self._path and self._config().get("local", False) is False: + if path != self._path and self.is_local() is False: raise aiohttp.web.HTTPForbidden(text="You are not allowed to modify the project directory location") self._path = path self._update_temporary_file() + @property + def name(self): + + return self._name + + @name.setter + def name(self, name): + + self._name = name + @property def vms(self): diff --git a/gns3server/modules/project_manager.py b/gns3server/modules/project_manager.py index fa34217d..454b3114 100644 --- a/gns3server/modules/project_manager.py +++ b/gns3server/modules/project_manager.py @@ -60,7 +60,7 @@ class ProjectManager: raise aiohttp.web.HTTPNotFound(text="Project ID {} doesn't exist".format(project_id)) return self._projects[project_id] - def create_project(self, project_id=None, path=None, temporary=False): + def create_project(self, name=None, project_id=None, path=None, temporary=False): """ Create a project and keep a references to it in project manager. @@ -71,7 +71,7 @@ class ProjectManager: return self._projects[project_id] # FIXME: should we have an error? #raise aiohttp.web.HTTPConflict(text="Project ID {} is already in use on this server".format(project_id)) - project = Project(project_id=project_id, path=path, temporary=temporary) + project = Project(name=name, project_id=project_id, path=path, temporary=temporary) self._projects[project.id] = project return project diff --git a/gns3server/schemas/project.py b/gns3server/schemas/project.py index 7bd8b723..38bea4c0 100644 --- a/gns3server/schemas/project.py +++ b/gns3server/schemas/project.py @@ -21,6 +21,11 @@ PROJECT_CREATE_SCHEMA = { "description": "Request validation to create a new Project instance", "type": "object", "properties": { + "name": { + "description": "Project name", + "type": ["string", "null"], + "minLength": 1 + }, "path": { "description": "Project directory", "type": ["string", "null"], @@ -46,6 +51,11 @@ PROJECT_UPDATE_SCHEMA = { "description": "Request validation to update a Project instance", "type": "object", "properties": { + "name": { + "description": "Project name", + "type": "string", + "minLength": 1 + }, "temporary": { "description": "If project is a temporary project", "type": "boolean" @@ -63,6 +73,11 @@ PROJECT_OBJECT_SCHEMA = { "description": "Project instance", "type": "object", "properties": { + "name": { + "description": "Project name", + "type": "string", + "minLength": 1 + }, "location": { "description": "Base directory where the project should be created on remote server", "type": "string", From 4f1674f50c3d8f5ce007b9a6452f18e37b87adf3 Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 9 Mar 2015 11:38:02 -0600 Subject: [PATCH 412/485] Bump version to 1.3.0beta1.dev2 --- gns3server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/version.py b/gns3server/version.py index b7a55632..a81c0b03 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,5 +23,5 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "1.3.0beta1.dev1" +__version__ = "1.3.0beta1.dev2" __version_info__ = (1, 3, 0, -99) From 5ca65093e4e859973469bbeeec2d0eea9ea55ede Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 9 Mar 2015 12:45:02 -0600 Subject: [PATCH 413/485] Fixes bugs when checking if this is a local project. --- gns3server/modules/project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index 23c545bf..8d1d449d 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -89,7 +89,7 @@ class Project: def is_local(self): - return self._config().get("local", False) + return self._config().getboolean("local", False) @classmethod def _get_default_project_directory(cls): From ad5548f70bc12bc99344d28ba2ee95df6340f1c9 Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 9 Mar 2015 21:46:23 -0600 Subject: [PATCH 414/485] Convert more that IOU pre 1.3 projects but also other modules on remote servers. --- gns3server/handlers/upload_handler.py | 3 ++- gns3server/modules/base_manager.py | 18 +++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/gns3server/handlers/upload_handler.py b/gns3server/handlers/upload_handler.py index a4a9396e..0e0a7291 100644 --- a/gns3server/handlers/upload_handler.py +++ b/gns3server/handlers/upload_handler.py @@ -16,6 +16,7 @@ # along with this program. If not, see . import os +import aiohttp import stat from ..config import Config @@ -56,7 +57,7 @@ class UploadHandler: return if data["type"] not in ["IOU", "IOURC", "QEMU", "IOS"]: - raise HTTPForbidden("You are not authorized to upload this kind of image {}".format(data["type"])) + raise aiohttp.web.HTTPForbidden("You are not authorized to upload this kind of image {}".format(data["type"])) if data["type"] == "IOURC": destination_dir = os.path.expanduser("~/") diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 9c642c7d..23a4e34d 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -175,17 +175,17 @@ class BaseManager: new_project_files_path, e)) if project.is_local() is False: - legacy_remote_iou_project = os.path.join(project.location, project.name, "iou") - new_iou_project_path = os.path.join(project.path, "project-files", "iou") - if os.path.exists(legacy_remote_iou_project) and not os.path.exists(new_iou_project_path): - # move the legacy remote IOU project (remote servers only) - log.info("Converting old remote IOU project...") + legacy_remote_project_path = os.path.join(project.location, project.name, self.module_name.lower()) + new_remote_project_path = os.path.join(project.path, "project-files", self.module_name.lower()) + if os.path.exists(legacy_remote_project_path) and not os.path.exists(new_remote_project_path): + # move the legacy remote project (remote servers only) + log.info("Converting old remote project...") try: - log.info('Moving "{}" to "{}"'.format(legacy_remote_iou_project, new_iou_project_path)) - yield from wait_run_in_executor(shutil.move, legacy_remote_iou_project, new_iou_project_path) + log.info('Moving "{}" to "{}"'.format(legacy_remote_project_path, new_remote_project_path)) + yield from wait_run_in_executor(shutil.move, legacy_remote_project_path, new_remote_project_path) except OSError as e: - raise aiohttp.web.HTTPInternalServerError(text="Could not move IOU directory: {} to {} {}".format(legacy_remote_iou_project, - new_iou_project_path, e)) + raise aiohttp.web.HTTPInternalServerError(text="Could not move directory: {} to {} {}".format(legacy_remote_project_path, + new_remote_project_path, e)) if hasattr(self, "get_legacy_vm_workdir"): # rename old project VM working dir From 5910b4b0be942d2e2adc36196580ee773a796d89 Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 9 Mar 2015 21:57:21 -0600 Subject: [PATCH 415/485] Have the server look in the right place for relative image paths. --- gns3server/modules/dynamips/nodes/router.py | 2 +- gns3server/modules/iou/iou_vm.py | 2 +- gns3server/modules/qemu/qemu_vm.py | 12 ++++++++++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index 92079aad..98d9384f 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -423,7 +423,7 @@ class Router(BaseVM): if not os.path.isabs(image): server_config = Config.instance().get_section_config("Server") - image = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), image) + image = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), "IOS", image) if not os.path.isfile(image): raise DynamipsError("IOS image '{}' is not accessible".format(image)) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index ea90f446..059db000 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -133,7 +133,7 @@ class IOUVM(BaseVM): if not os.path.isabs(path): server_config = Config.instance().get_section_config("Server") - path = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), path) + path = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), "IOU", path) self._path = path if not os.path.isfile(self._path) or not os.path.exists(self._path): diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index a270e0f3..09f87d51 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -185,7 +185,7 @@ class QemuVM(BaseVM): if os.path.isabs(hda_disk_image): server_config = Config.instance().get_section_config("Server") - hda_disk_image = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), hda_disk_image) + hda_disk_image = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), "QEMU", hda_disk_image) log.info("QEMU VM {name} [id={id}] has set the QEMU hda disk image path to {disk_image}".format(name=self._name, id=self._id, @@ -212,7 +212,7 @@ class QemuVM(BaseVM): if not os.path.isabs(hdb_disk_image): server_config = Config.instance().get_section_config("Server") - hdb_disk_image = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), hdb_disk_image) + hdb_disk_image = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), "QEMU", hdb_disk_image) log.info("QEMU VM {name} [id={id}] has set the QEMU hdb disk image path to {disk_image}".format(name=self._name, id=self._id, @@ -406,6 +406,10 @@ class QemuVM(BaseVM): :param initrd: QEMU initrd path """ + if os.path.isabs(initrd): + server_config = Config.instance().get_section_config("Server") + initrd = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), "QEMU", initrd) + log.info("QEMU VM {name} [id={id}] has set the QEMU initrd path to {initrd}".format(name=self._name, id=self._id, initrd=initrd)) @@ -429,6 +433,10 @@ class QemuVM(BaseVM): :param kernel_image: QEMU kernel image path """ + if os.path.isabs(kernel_image): + server_config = Config.instance().get_section_config("Server") + kernel_image = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), "QEMU", kernel_image) + log.info("QEMU VM {name} [id={id}] has set the QEMU kernel image path to {kernel_image}".format(name=self._name, id=self._id, kernel_image=kernel_image)) From 03dfd177f9a0d61756a1427a54e59791dc8cacda Mon Sep 17 00:00:00 2001 From: grossmj Date: Tue, 10 Mar 2015 00:34:57 -0600 Subject: [PATCH 416/485] Use TCP instead of Telnet to communicate with Qemu monitor. --- gns3server/modules/qemu/qemu_vm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index 09f87d51..eedf4a83 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -831,7 +831,7 @@ class QemuVM(BaseVM): def _monitor_options(self): if self._monitor: - return ["-monitor", "telnet:{}:{},server,nowait".format(self._monitor_host, self._monitor)] + return ["-monitor", "tcp:{}:{},server,nowait".format(self._monitor_host, self._monitor)] else: return [] From 4c68fd0d52c7691e45c5c1a3498e66b9797cbcfa Mon Sep 17 00:00:00 2001 From: grossmj Date: Tue, 10 Mar 2015 11:00:32 -0600 Subject: [PATCH 417/485] Renames server.conf and server.ini to gns3_server.conf and gns3_server.ini respectively. --- gns3server/config.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/gns3server/config.py b/gns3server/config.py index 4b8f4241..657ac67b 100644 --- a/gns3server/config.py +++ b/gns3server/config.py @@ -53,15 +53,15 @@ class Config(object): appname = "GNS3" # On windows, the configuration file location can be one of the following: - # 1: %APPDATA%/GNS3/server.ini + # 1: %APPDATA%/GNS3/gns3_server.ini # 2: %APPDATA%/GNS3.ini - # 3: %COMMON_APPDATA%/GNS3/server.ini + # 3: %COMMON_APPDATA%/GNS3/gns3_server.ini # 4: %COMMON_APPDATA%/GNS3.ini # 5: server.ini in the current working directory appdata = os.path.expandvars("%APPDATA%") common_appdata = os.path.expandvars("%COMMON_APPDATA%") - filename = "server.ini" + filename = "gns3_server.ini" if self._files is None: self._files = [os.path.join(appdata, appname, filename), os.path.join(appdata, appname + ".ini"), @@ -71,9 +71,9 @@ class Config(object): else: # On UNIX-like platforms, the configuration file location can be one of the following: - # 1: $HOME/.config/GNS3/server.conf + # 1: $HOME/.config/GNS3/gns3_server.conf # 2: $HOME/.config/GNS3.conf - # 3: /etc/xdg/GNS3/server.conf + # 3: /etc/xdg/GNS3/gns3_server.conf # 4: /etc/xdg/GNS3.conf # 5: server.conf in the current working directory @@ -82,7 +82,7 @@ class Config(object): else: appname = "GNS3" home = os.path.expanduser("~") - filename = "server.conf" + filename = "gns3_server.conf" if self._files is None: self._files = [os.path.join(home, ".config", appname, filename), os.path.join(home, ".config", appname + ".conf"), From 062e5a5986591e85995584d3a3b1fe862fc46aff Mon Sep 17 00:00:00 2001 From: grossmj Date: Tue, 10 Mar 2015 11:05:52 -0600 Subject: [PATCH 418/485] Fixes bug when starting a packet capture in VirtualBox with the project path containing spaces. --- gns3server/modules/virtualbox/virtualbox_vm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 7eea8d12..c6151f2a 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -650,7 +650,7 @@ class VirtualBoxVM(BaseVM): if nio.capturing: yield from self._modify_vm("--nictrace{} on".format(adapter_number + 1)) - yield from self._modify_vm("--nictracefile{} {}".format(adapter_number + 1, nio.pcap_output_file)) + yield from self._modify_vm('--nictracefile{} "{}"'.format(adapter_number + 1, nio.pcap_output_file)) for adapter_number in range(len(self._ethernet_adapters), self._maximum_adapters): log.debug("disabling remaining adapter {}".format(adapter_number)) From 1610067eeed445508f5791e42f18ab52c5e3349e Mon Sep 17 00:00:00 2001 From: grossmj Date: Tue, 10 Mar 2015 11:50:30 -0600 Subject: [PATCH 419/485] Support for HDC and HDD disk images in Qemu. --- gns3server/modules/qemu/qemu_vm.py | 101 +++++++++++++++++++++++++++-- gns3server/schemas/qemu.py | 28 +++++++- 2 files changed, 122 insertions(+), 7 deletions(-) diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index eedf4a83..c4c17851 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -82,6 +82,8 @@ class QemuVM(BaseVM): self._qemu_path = qemu_path self._hda_disk_image = "" self._hdb_disk_image = "" + self._hdc_disk_image = "" + self._hdd_disk_image = "" self._options = "" self._ram = 256 self._monitor = monitor @@ -219,6 +221,59 @@ class QemuVM(BaseVM): disk_image=hdb_disk_image)) self._hdb_disk_image = hdb_disk_image + @property + def hdc_disk_image(self): + """ + Returns the hdc disk image path for this QEMU VM. + + :returns: QEMU hdc disk image path + """ + + return self._hdc_disk_image + + @hdc_disk_image.setter + def hdc_disk_image(self, hdc_disk_image): + """ + Sets the hdc disk image for this QEMU VM. + + :param hdc_disk_image: QEMU hdc disk image path + """ + + if os.path.isabs(hdc_disk_image): + server_config = Config.instance().get_section_config("Server") + hdc_disk_image = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), "QEMU", hdc_disk_image) + + log.info("QEMU VM {name} [id={id}] has set the QEMU hdc disk image path to {disk_image}".format(name=self._name, + id=self._id, + disk_image=hdc_disk_image)) + self._hdc_disk_image = hdc_disk_image + + @property + def hdd_disk_image(self): + """ + Returns the hdd disk image path for this QEMU VM. + + :returns: QEMU hdd disk image path + """ + + return self._hdd_disk_image + + @hdd_disk_image.setter + def hdd_disk_image(self, hdd_disk_image): + """ + Sets the hdd disk image for this QEMU VM. + + :param hdd_disk_image: QEMU hdd disk image path + """ + + if os.path.isabs(hdd_disk_image): + server_config = Config.instance().get_section_config("Server") + hdd_disk_image = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), "QEMU", hdd_disk_image) + + log.info("QEMU VM {name} [id={id}] has set the QEMU hdd disk image path to {disk_image}".format(name=self._name, + id=self._id, + disk_image=hdd_disk_image)) + @property def adapters(self): """ @@ -869,14 +924,14 @@ class QemuVM(BaseVM): # create a "FLASH" with 256MB if no disk image has been specified hda_disk = os.path.join(self.working_dir, "flash.qcow2") if not os.path.exists(hda_disk): - process = yield from asyncio.create_subprocess_exec(qemu_img_path, "create", "-f", "qcow2", hda_disk, "128M") + process = yield from asyncio.create_subprocess_exec(qemu_img_path, "create", "-f", "qcow2", hda_disk, "256M") retcode = yield from process.wait() log.info("{} returned with {}".format(qemu_img_path, retcode)) except (OSError, subprocess.SubprocessError) as e: - raise QemuError("Could not create disk image {}".format(e)) - + raise QemuError("Could not create hda disk image {}".format(e)) options.extend(["-hda", hda_disk]) + if self._hdb_disk_image: if not os.path.isfile(self._hdb_disk_image) or not os.path.exists(self._hdb_disk_image): if os.path.islink(self._hdb_disk_image): @@ -892,9 +947,45 @@ class QemuVM(BaseVM): retcode = yield from process.wait() log.info("{} returned with {}".format(qemu_img_path, retcode)) except (OSError, subprocess.SubprocessError) as e: - raise QemuError("Could not create disk image {}".format(e)) + raise QemuError("Could not create hdb disk image {}".format(e)) options.extend(["-hdb", hdb_disk]) + if self._hdc_disk_image: + if not os.path.isfile(self._hdc_disk_image) or not os.path.exists(self._hdc_disk_image): + if os.path.islink(self._hdc_disk_image): + raise QemuError("hdc disk image '{}' linked to '{}' is not accessible".format(self._hdc_disk_image, os.path.realpath(self._hdc_disk_image))) + else: + raise QemuError("hdc disk image '{}' is not accessible".format(self._hdc_disk_image)) + hdc_disk = os.path.join(self.working_dir, "hdc_disk.qcow2") + if not os.path.exists(hdc_disk): + try: + process = yield from asyncio.create_subprocess_exec(qemu_img_path, "create", "-o", + "backing_file={}".format(self._hdc_disk_image), + "-f", "qcow2", hdc_disk) + retcode = yield from process.wait() + log.info("{} returned with {}".format(qemu_img_path, retcode)) + except (OSError, subprocess.SubprocessError) as e: + raise QemuError("Could not create hdc disk image {}".format(e)) + options.extend(["-hdc", hdc_disk]) + + if self._hdd_disk_image: + if not os.path.isfile(self._hdd_disk_image) or not os.path.exists(self._hdd_disk_image): + if os.path.islink(self._hdd_disk_image): + raise QemuError("hdd disk image '{}' linked to '{}' is not accessible".format(self._hdd_disk_image, os.path.realpath(self._hdd_disk_image))) + else: + raise QemuError("hdd disk image '{}' is not accessible".format(self._hdd_disk_image)) + hdd_disk = os.path.join(self.working_dir, "hdd_disk.qcow2") + if not os.path.exists(hdd_disk): + try: + process = yield from asyncio.create_subprocess_exec(qemu_img_path, "create", "-o", + "backing_file={}".format(self._hdd_disk_image), + "-f", "qcow2", hdd_disk) + retcode = yield from process.wait() + log.info("{} returned with {}".format(qemu_img_path, retcode)) + except (OSError, subprocess.SubprocessError) as e: + raise QemuError("Could not create hdd disk image {}".format(e)) + options.extend(["-hdd", hdd_disk]) + return options def _linux_boot_options(self): @@ -993,7 +1084,7 @@ class QemuVM(BaseVM): "project_id": self.project.id, "vm_id": self.id } - # Qemu has a long list of options. The JSON schema is the single source of informations + # Qemu has a long list of options. The JSON schema is the single source of information for field in QEMU_OBJECT_SCHEMA["required"]: if field not in answer: answer[field] = getattr(self, field) diff --git a/gns3server/schemas/qemu.py b/gns3server/schemas/qemu.py index 30b62601..7632525a 100644 --- a/gns3server/schemas/qemu.py +++ b/gns3server/schemas/qemu.py @@ -61,6 +61,14 @@ QEMU_CREATE_SCHEMA = { "description": "QEMU hdb disk image path", "type": ["string", "null"], }, + "hdc_disk_image": { + "description": "QEMU hdc disk image path", + "type": ["string", "null"], + }, + "hdd_disk_image": { + "description": "QEMU hdd disk image path", + "type": ["string", "null"], + }, "ram": { "description": "amount of RAM in MB", "type": ["integer", "null"] @@ -152,6 +160,14 @@ QEMU_UPDATE_SCHEMA = { "description": "QEMU hdb disk image path", "type": ["string", "null"], }, + "hdc_disk_image": { + "description": "QEMU hdc disk image path", + "type": ["string", "null"], + }, + "hdd_disk_image": { + "description": "QEMU hdd disk image path", + "type": ["string", "null"], + }, "ram": { "description": "amount of RAM in MB", "type": ["integer", "null"] @@ -296,6 +312,14 @@ QEMU_OBJECT_SCHEMA = { "description": "QEMU hdb disk image path", "type": "string", }, + "hdc_disk_image": { + "description": "QEMU hdc disk image path", + "type": "string", + }, + "hdd_disk_image": { + "description": "QEMU hdd disk image path", + "type": "string", + }, "ram": { "description": "amount of RAM in MB", "type": "integer" @@ -360,8 +384,8 @@ QEMU_OBJECT_SCHEMA = { }, }, "additionalProperties": False, - "required": ["vm_id", "project_id", "name", "qemu_path", "hda_disk_image", - "hdb_disk_image", "ram", "adapters", "adapter_type", "console", + "required": ["vm_id", "project_id", "name", "qemu_path", "hda_disk_image", "hdb_disk_image", + "hdc_disk_image", "hdd_disk_image", "ram", "adapters", "adapter_type", "console", "monitor", "initrd", "kernel_image", "kernel_command_line", "legacy_networking", "cpu_throttling", "process_priority", "options" ] From 223f3ee705f3d35d8911a46f10a4ce3bde6068ca Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 11 Mar 2015 10:53:09 -0600 Subject: [PATCH 420/485] Should fix ProcessLookupError exceptions. --- gns3server/modules/dynamips/__init__.py | 16 +++++++++------- gns3server/modules/dynamips/hypervisor.py | 4 ++-- gns3server/modules/dynamips/nodes/router.py | 2 ++ gns3server/modules/iou/iou_vm.py | 11 ++++++----- gns3server/modules/vpcs/vpcs_vm.py | 4 ++-- 5 files changed, 21 insertions(+), 16 deletions(-) diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index ed538f23..16bc67c6 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -494,12 +494,12 @@ class Dynamips(BaseManager): yield from vm.set_sparsemem(False) # update the configs if needed - yield from self.create_vm_configs(vm, settings) + yield from self.set_vm_configs(vm, settings) @asyncio.coroutine - def create_vm_configs(self, vm, settings): + def set_vm_configs(self, vm, settings): """ - Creates VM configs from pushed content. + Set VM configs from pushed content or existing config files. :param vm: VM instance :param settings: VM settings @@ -514,16 +514,18 @@ class Dynamips(BaseManager): startup_config_path = self._create_config(vm, startup_config_content, default_startup_config_path) yield from vm.set_configs(startup_config_path) else: - startup_config_path = settings.get("startup_config", "") - yield from vm.set_configs(startup_config_path) + startup_config_path = settings.get("startup_config") + if startup_config_path: + yield from vm.set_configs(startup_config_path) private_config_content = settings.get("private_config_content") if private_config_content: private_config_path = self._create_config(vm, private_config_content, default_private_config_path) yield from vm.set_configs(vm.startup_config, private_config_path) else: - private_config_path = settings.get("private_config", "") - yield from vm.set_configs(vm.startup_config, private_config_path) + private_config_path = settings.get("private_config") + if private_config_path: + yield from vm.set_configs(vm.startup_config, private_config_path) def _create_config(self, vm, content, path): """ diff --git a/gns3server/modules/dynamips/hypervisor.py b/gns3server/modules/dynamips/hypervisor.py index 5a620357..ce1b4ade 100644 --- a/gns3server/modules/dynamips/hypervisor.py +++ b/gns3server/modules/dynamips/hypervisor.py @@ -138,9 +138,9 @@ class Hypervisor(DynamipsHypervisor): try: yield from asyncio.wait_for(self._process.wait(), timeout=3) except asyncio.TimeoutError: - self._process.kill() if self._process.returncode is None: - log.warn("Dynamips process {} is still running".format(self._process.pid)) + log.warn("Dynamips process {} is still running... killing it".format(self._process.pid)) + self._process.kill() if self._stdout_file and os.access(self._stdout_file, os.W_OK): try: diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index 98d9384f..03a8f60c 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -1249,6 +1249,8 @@ class Router(BaseVM): port_number=port_number)) nio = adapter.get_nio(port_number) + if nio is None: + return if isinstance(nio, NIOUDP): self.manager.port_manager.release_udp_port(nio.lport) adapter.remove_nio(port_number) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 059db000..299a9258 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -488,9 +488,10 @@ class IOUVM(BaseVM): try: yield from gns3server.utils.asyncio.wait_for_process_termination(self._iou_process, timeout=3) except asyncio.TimeoutError: - self._iou_process.kill() if self._iou_process.returncode is None: - log.warn("IOU process {} is still running".format(self._iou_process.pid)) + log.warn("IOU process {} is still running... killing it".format(self._iou_process.pid)) + self._iou_process.kill() + self._iou_process = None if self._iouyap_process is not None: @@ -498,11 +499,11 @@ class IOUVM(BaseVM): try: yield from gns3server.utils.asyncio.wait_for_process_termination(self._iouyap_process, timeout=3) except asyncio.TimeoutError: - self._iouyap_process.kill() if self._iouyap_process.returncode is None: - log.warn("IOUYAP process {} is still running".format(self._iouyap_process.pid)) - self._iouyap_process = None + log.warn("IOUYAP process {} is still running... killing it".format(self._iouyap_process.pid)) + self._iouyap_process.kill() + self._iouyap_process = None self._started = False def _terminate_process_iouyap(self): diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index 56c89bd1..dad5e87c 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -245,9 +245,9 @@ class VPCSVM(BaseVM): try: yield from asyncio.wait_for(self._process.wait(), timeout=3) except asyncio.TimeoutError: - self._process.kill() if self._process.returncode is None: - log.warn("VPCS process {} is still running".format(self._process.pid)) + log.warn("VPCS process {} is still running... killing it".format(self._process.pid)) + self._process.kill() self._process = None self._started = False From c41bec0516afe83afd6cadedbef3c5be67bd835b Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 11 Mar 2015 12:05:22 -0600 Subject: [PATCH 421/485] Do not give attachment warning for generic attachments in VirtualBox. --- gns3server/modules/virtualbox/virtualbox_vm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index c6151f2a..7375eb1d 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -599,7 +599,7 @@ class VirtualBoxVM(BaseVM): entry = "nic{}".format(adapter_number + 1) if entry in vm_info: value = vm_info[entry] - nics.append(value) + nics.append(value.lower()) else: nics.append(None) return nics @@ -618,7 +618,7 @@ class VirtualBoxVM(BaseVM): self._modify_vm("--cableconnected{} off".format(adapter_number + 1)) nio = self._ethernet_adapters[adapter_number].get_nio(0) if nio: - if not self._use_any_adapter and attachment not in ("none", "null"): + if not self._use_any_adapter and attachment not in ("none", "null", "generic"): raise VirtualBoxError("Attachment ({}) already configured on adapter {}. " "Please set it to 'Not attached' to allow GNS3 to use it.".format(attachment, adapter_number + 1)) From aebcd9f08bcb145aa669a363da34cfc0207e99b3 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 11 Mar 2015 15:04:11 -0600 Subject: [PATCH 422/485] Relative path support of IOU, IOS and Qemu images. --- gns3server/modules/dynamips/__init__.py | 3 +- gns3server/modules/dynamips/nodes/router.py | 9 +++- gns3server/modules/iou/iou_vm.py | 36 +++++++++------- gns3server/modules/qemu/qemu_vm.py | 48 +++++++++++++++------ 4 files changed, 65 insertions(+), 31 deletions(-) diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index 16bc67c6..6a5f4c70 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -44,7 +44,6 @@ from .hypervisor import Hypervisor from .nodes.router import Router from .dynamips_vm import DynamipsVM from .dynamips_device import DynamipsDevice -from gns3server.config import Config # NIOs from .nios.nio_udp import NIOUDP @@ -312,7 +311,7 @@ class Dynamips(BaseManager): # FIXME: hypervisor should always listen to 127.0.0.1 # See https://github.com/GNS3/dynamips/issues/62 - server_config = Config.instance().get_section_config("Server") + server_config = self.config.get_section_config("Server") server_host = server_config.get("host") try: diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index 03a8f60c..8cfa23a8 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -34,7 +34,6 @@ from ...base_vm import BaseVM from ..dynamips_error import DynamipsError from ..nios.nio_udp import NIOUDP -from gns3server.config import Config from gns3server.utils.asyncio import wait_run_in_executor @@ -152,6 +151,12 @@ class Router(BaseVM): "mac_addr": self._mac_addr, "system_id": self._system_id} + # return the relative path if the IOS image is in the images_path directory + server_config = self.manager.config.get_section_config("Server") + relative_image = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), "IOS", self._image) + if os.path.exists(relative_image): + router_info["image"] = os.path.basename(self._image) + # add the slots slot_number = 0 for slot in self._slots: @@ -422,7 +427,7 @@ class Router(BaseVM): """ if not os.path.isabs(image): - server_config = Config.instance().get_section_config("Server") + server_config = self.manager.config.get_section_config("Server") image = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), "IOS", image) if not os.path.isfile(image): diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 299a9258..bdcb8bb8 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -39,7 +39,6 @@ from ..nios.nio_tap import NIOTAP from ..nios.nio_generic_ethernet import NIOGenericEthernet from ..base_vm import BaseVM from .ioucon import start_ioucon -from ...config import Config import gns3server.utils.asyncio @@ -132,7 +131,7 @@ class IOUVM(BaseVM): """ if not os.path.isabs(path): - server_config = Config.instance().get_section_config("Server") + server_config = self.manager.config.get_section_config("Server") path = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), "IOU", path) self._path = path @@ -195,19 +194,26 @@ class IOUVM(BaseVM): def __json__(self): - return {"name": self.name, - "vm_id": self.id, - "console": self._console, - "project_id": self.project.id, - "path": self.path, - "ethernet_adapters": len(self._ethernet_adapters), - "serial_adapters": len(self._serial_adapters), - "ram": self._ram, - "nvram": self._nvram, - "l1_keepalives": self._l1_keepalives, - "initial_config": self.relative_initial_config_file, - "use_default_iou_values": self._use_default_iou_values - } + iou_vm_info = {"name": self.name, + "vm_id": self.id, + "console": self._console, + "project_id": self.project.id, + "path": self.path, + "ethernet_adapters": len(self._ethernet_adapters), + "serial_adapters": len(self._serial_adapters), + "ram": self._ram, + "nvram": self._nvram, + "l1_keepalives": self._l1_keepalives, + "initial_config": self.relative_initial_config_file, + "use_default_iou_values": self._use_default_iou_values} + + # return the relative path if the IOU image is in the images_path directory + server_config = self.manager.config.get_section_config("Server") + relative_image = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), "IOU", self.path) + if os.path.exists(relative_image): + iou_vm_info["path"] = os.path.basename(self.path) + + return iou_vm_info @property def iouyap_path(self): diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index c4c17851..8e8237c6 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -32,7 +32,6 @@ from ..adapters.ethernet_adapter import EthernetAdapter from ..nios.nio_udp import NIOUDP from ..base_vm import BaseVM from ...schemas.qemu import QEMU_OBJECT_SCHEMA -from ...config import Config import logging log = logging.getLogger(__name__) @@ -185,8 +184,8 @@ class QemuVM(BaseVM): :param hda_disk_image: QEMU hda disk image path """ - if os.path.isabs(hda_disk_image): - server_config = Config.instance().get_section_config("Server") + if not os.path.isabs(hda_disk_image): + server_config = self.manager.config.get_section_config("Server") hda_disk_image = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), "QEMU", hda_disk_image) log.info("QEMU VM {name} [id={id}] has set the QEMU hda disk image path to {disk_image}".format(name=self._name, @@ -213,7 +212,7 @@ class QemuVM(BaseVM): """ if not os.path.isabs(hdb_disk_image): - server_config = Config.instance().get_section_config("Server") + server_config = self.manager.config.get_section_config("Server") hdb_disk_image = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), "QEMU", hdb_disk_image) log.info("QEMU VM {name} [id={id}] has set the QEMU hdb disk image path to {disk_image}".format(name=self._name, @@ -239,8 +238,8 @@ class QemuVM(BaseVM): :param hdc_disk_image: QEMU hdc disk image path """ - if os.path.isabs(hdc_disk_image): - server_config = Config.instance().get_section_config("Server") + if not os.path.isabs(hdc_disk_image): + server_config = self.manager.config.get_section_config("Server") hdc_disk_image = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), "QEMU", hdc_disk_image) log.info("QEMU VM {name} [id={id}] has set the QEMU hdc disk image path to {disk_image}".format(name=self._name, @@ -266,8 +265,8 @@ class QemuVM(BaseVM): :param hdd_disk_image: QEMU hdd disk image path """ - if os.path.isabs(hdd_disk_image): - server_config = Config.instance().get_section_config("Server") + if not os.path.isabs(hdd_disk_image): + server_config = self.manager.config.get_section_config("Server") hdd_disk_image = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), "QEMU", hdd_disk_image) log.info("QEMU VM {name} [id={id}] has set the QEMU hdd disk image path to {disk_image}".format(name=self._name, @@ -461,8 +460,8 @@ class QemuVM(BaseVM): :param initrd: QEMU initrd path """ - if os.path.isabs(initrd): - server_config = Config.instance().get_section_config("Server") + if not os.path.isabs(initrd): + server_config = self.manager.config.get_section_config("Server") initrd = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), "QEMU", initrd) log.info("QEMU VM {name} [id={id}] has set the QEMU initrd path to {initrd}".format(name=self._name, @@ -488,8 +487,8 @@ class QemuVM(BaseVM): :param kernel_image: QEMU kernel image path """ - if os.path.isabs(kernel_image): - server_config = Config.instance().get_section_config("Server") + if not os.path.isabs(kernel_image): + server_config = self.manager.config.get_section_config("Server") kernel_image = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), "QEMU", kernel_image) log.info("QEMU VM {name} [id={id}] has set the QEMU kernel image path to {kernel_image}".format(name=self._name, @@ -1079,6 +1078,23 @@ class QemuVM(BaseVM): command.extend(self._graphic()) return command + def _get_relative_disk_image_path(self, disk_image): + """ + Returns a relative image path if the disk image is in the images directory. + + :param disk_image: path to the disk image + + :returns: relative or full path + """ + + if disk_image: + # return the relative path if disks images are in the images_path directory + server_config = self.manager.config.get_section_config("Server") + relative_image = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), "QEMU", disk_image) + if os.path.exists(relative_image): + return os.path.basename(disk_image) + return disk_image + def __json__(self): answer = { "project_id": self.project.id, @@ -1088,4 +1104,12 @@ class QemuVM(BaseVM): for field in QEMU_OBJECT_SCHEMA["required"]: if field not in answer: answer[field] = getattr(self, field) + + answer["hda_disk_image"] = self._get_relative_disk_image_path(self._hda_disk_image) + answer["hdb_disk_image"] = self._get_relative_disk_image_path(self._hdb_disk_image) + answer["hdc_disk_image"] = self._get_relative_disk_image_path(self._hdc_disk_image) + answer["hdd_disk_image"] = self._get_relative_disk_image_path(self._hdd_disk_image) + answer["initrd"] = self._get_relative_disk_image_path(self._initrd) + answer["kernel_image"] = self._get_relative_disk_image_path(self._kernel_image) + return answer From 54c2d34185cf9c15647a76d71c09d2ff7038e397 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 11 Mar 2015 18:59:57 -0600 Subject: [PATCH 423/485] Optional IOU license key check. --- gns3server/modules/iou/iou_vm.py | 69 ++++++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 4 deletions(-) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index bdcb8bb8..4d80f947 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -22,6 +22,7 @@ order to run an IOU instance. import os import signal +import socket import re import asyncio import subprocess @@ -29,6 +30,8 @@ import shutil import argparse import threading import configparser +import struct +import hashlib import glob from .iou_error import IOUError @@ -236,7 +239,17 @@ class IOUVM(BaseVM): :returns: path to IOURC """ - return self._manager.config.get_section_config("IOU").get("iourc_path") + iourc_path = self._manager.config.get_section_config("IOU").get("iourc_path") + if not iourc_path: + # look for the iourc file in the user home dir. + path = os.path.join(os.path.expanduser("~/"), ".iourc") + if os.path.exists(path): + return path + # look for the iourc file in the current working dir. + path = os.path.join(self.working_dir, "iourc") + if os.path.exists(path): + return path + return iourc_path @property def ram(self): @@ -326,6 +339,49 @@ class IOUVM(BaseVM): raise IOUError("The following shared library dependencies cannot be found for IOU image {}: {}".format(self._path, ", ".join(missing_libs))) + def _check_iou_licence(self): + """ + Checks for a valid IOU key in the iourc file (paranoid mode). + """ + + config = configparser.ConfigParser() + try: + with open(self.iourc_path) as f: + config.read_file(f) + except OSError as e: + raise IOUError("Could not open iourc file {}: {}".format(self.iourc_path, e)) + except configparser.Error as e: + raise IOUError("Could not parse iourc file {}: {}".format(self.iourc_path, e)) + if "license" not in config: + raise IOUError("License section not found in iourc file {}".format(self.iourc_path)) + hostname = socket.gethostname() + if hostname not in config["license"]: + raise IOUError("Hostname key not found in iourc file {}".format(self.iourc_path)) + user_ioukey = config["license"][hostname] + print(user_ioukey[-1:]) + if user_ioukey[-1:] != ';': + raise IOUError("IOU key not ending with ; in iourc file".format(self.iourc_path)) + if len(user_ioukey) != 17: + raise IOUError("IOU key length is not 16 characters in iourc file".format(self.iourc_path)) + user_ioukey = user_ioukey[:16] + try: + hostid = os.popen("hostid").read().strip() + except OSError as e: + raise IOUError("Could not read the hostid: {}".format(e)) + try: + ioukey = int(hostid, 16) + except ValueError: + raise IOUError("Invalid hostid detected: {}".format(hostid)) + for x in hostname: + ioukey += ord(x) + pad1 = b'\x4B\x58\x21\x81\x56\x7B\x0D\xF3\x21\x43\x9B\x7E\xAC\x1D\xE6\x8A' + pad2 = b'\x80' + 39 * b'\0' + ioukey = hashlib.md5(pad1 + pad2 + struct.pack('!i', ioukey) + pad1).hexdigest()[:16] + if ioukey != user_ioukey: + raise IOUError("Invalid IOU license key {} detected in iourc file {} for host {}".format(user_ioukey, + self.iourc_path, + hostname)) + @asyncio.coroutine def start(self): """ @@ -343,6 +399,10 @@ class IOUVM(BaseVM): if iourc_path and not os.path.isfile(iourc_path): raise IOUError("A valid iourc file is necessary to start IOU") + license_check = self._manager.config.get_section_config("IOU").getboolean("license_check", True) + if license_check: + self._check_iou_licence() + iouyap_path = self.iouyap_path if not iouyap_path or not os.path.isfile(iouyap_path): raise IOUError("iouyap is necessary to start IOU") @@ -351,7 +411,7 @@ class IOUVM(BaseVM): # created a environment variable pointing to the iourc file. env = os.environ.copy() - if iourc_path: + if "IOURC" not in os.environ: env["IOURC"] = iourc_path self._command = yield from self._build_command() try: @@ -515,7 +575,7 @@ class IOUVM(BaseVM): def _terminate_process_iouyap(self): """Terminate the process if running""" - if self._iou_process: + if self._iouyap_process: log.info("Stopping IOUYAP instance {} PID={}".format(self.name, self._iouyap_process.pid)) try: self._iouyap_process.terminate() @@ -818,7 +878,8 @@ class IOUVM(BaseVM): """ env = os.environ.copy() - env["IOURC"] = self.iourc_path + if "IOURC" not in os.environ: + env["IOURC"] = self.iourc_path try: output = yield from gns3server.utils.asyncio.subprocess_check_output(self._path, "-h", cwd=self.working_dir, env=env) if re.search("-l\s+Enable Layer 1 keepalive messages", output): From 34c4649d0b85165933e195e31441b70e6150edee Mon Sep 17 00:00:00 2001 From: grossmj Date: Wed, 11 Mar 2015 22:09:43 -0600 Subject: [PATCH 424/485] Bump version to 1.3beta1 --- gns3server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/version.py b/gns3server/version.py index a81c0b03..277c6f63 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,5 +23,5 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "1.3.0beta1.dev2" +__version__ = "1.3.0beta1" __version_info__ = (1, 3, 0, -99) From 38fa3e9a86d541b038aa532a5f173e2b07600268 Mon Sep 17 00:00:00 2001 From: grossmj Date: Wed, 11 Mar 2015 22:33:51 -0600 Subject: [PATCH 425/485] Update CHANGELOG. --- CHANGELOG | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 6ef0aca3..52012e13 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,35 @@ # Change Log -## 1.3.0alpha 1 03/03/2015 +## 1.3.0beta1 11/03/2015 + +* New title for VMs/Devices/routers preference pages. +* Deactivate auto idle-pc in contextual menu while we think about a better implementation. +* Optional IOU license key check. +* Relative picture paths are saved in projects. +* Relative path support of IOU, IOS and Qemu images. +* More checks when automatically starting the local server and find an alternative port if needed. +* Support for HDC and HDD disk images in Qemu. +* Fixed base IOS and IOU base configs. +* Fixed GNS3 console issues. +* Renamed server.conf and server.ini to gns3_server.conf and gns3_server.ini respectively. +* Remove remote servers list from module preferences + some other prefences re-factoring. +* Automatically convert old projects on remote servers. +* Bump the progress dialog minimum duration before display to 1000ms. +* Fixed port listing bug with Cloud and Host nodes. +* Fixed Qemu networking. +* Give a warning when a object is move the background layer. +* Option to draw a rectangle when a node is selected. +* New project icon (little yellow indicator). +* Default name for screenshot file is "screenshot". +* Alignment options (horizontal & vertical). +* Fixed import / export of the preferences file. +* Fixed pkg_ressource bug. +* Brought back Qemu preferences page. +* Include SSL cacert file with GNS3 Windows exe and Mac OS App to send crash report using HTTPS. +* Fixed adapter bug with VirtualBox. +* Fixed various errors when a project was not initialized. + +## 1.3.0alpha1 03/03/2015 * HTTP Rest API instead of WebSocket * API documentation @@ -9,6 +38,7 @@ * Use UUID instead of id ## 1.2.3 2015/01/17 + * Fixed broken -netdev + legacy virtio in Qemu support. * Ping and traceroute added to the IOU VM. @@ -29,6 +59,7 @@ * Fixed bug when importing Host node with UDP NIOs. ## 1.2.1 2014/12/04 + * Early support for IOSv and IOSv-L2 (with Qemu for now, which is slow on Windows/Mac OS X). * Support for CPU throttling and process priority for Qemu. * Fixed C7200 IO cards insert/remove issues and makes C7200-IO-FE the default. @@ -36,6 +67,7 @@ ## 1.2 2014/11/20 + * New VirtualBox support * New Telnet server for VirtualBox. * Add detection of qemu and qemu.exe binaries. @@ -45,5 +77,6 @@ ## 1.1 2014/10/23 + * Serial console for local VirtualBox. From 4f7b896a6aacb38b767850cebe6b35f4c72e0efa Mon Sep 17 00:00:00 2001 From: grossmj Date: Wed, 11 Mar 2015 23:09:01 -0600 Subject: [PATCH 426/485] Fixes tests. --- gns3server/modules/qemu/qemu_vm.py | 1 + tests/modules/iou/test_iou_vm.py | 3 ++- tests/modules/qemu/test_qemu_vm.py | 24 ++++++++++++++++++++---- tests/modules/test_project.py | 2 +- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index 8e8237c6..2c34e8a6 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -272,6 +272,7 @@ class QemuVM(BaseVM): log.info("QEMU VM {name} [id={id}] has set the QEMU hdd disk image path to {disk_image}".format(name=self._name, id=self._id, disk_image=hdd_disk_image)) + self._hdd_disk_image = hdd_disk_image @property def adapters(self): diff --git a/tests/modules/iou/test_iou_vm.py b/tests/modules/iou/test_iou_vm.py index 045b9814..ced8f49c 100644 --- a/tests/modules/iou/test_iou_vm.py +++ b/tests/modules/iou/test_iou_vm.py @@ -83,7 +83,8 @@ def test_vm_invalid_iouyap_path(project, manager, loop): def test_start(loop, vm, monkeypatch): - with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._check_requirements", return_value=True): + + with patch("gns3server.modules.iou.iou_vm.IOUVM._check_requirements", return_value=True): with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._start_ioucon", return_value=True): with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._start_iouyap", return_value=True): with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): diff --git a/tests/modules/qemu/test_qemu_vm.py b/tests/modules/qemu/test_qemu_vm.py index 71c5d70b..537b4947 100644 --- a/tests/modules/qemu/test_qemu_vm.py +++ b/tests/modules/qemu/test_qemu_vm.py @@ -202,7 +202,7 @@ def test_disk_options(vm, loop, fake_qemu_img_binary): loop.run_until_complete(asyncio.async(vm._disk_options())) assert process.called args, kwargs = process.call_args - assert args == (fake_qemu_img_binary, "create", "-f", "qcow2", os.path.join(vm.working_dir, "flash.qcow2"), "128M") + assert args == (fake_qemu_img_binary, "create", "-f", "qcow2", os.path.join(vm.working_dir, "flash.qcow2"), "256M") def test_set_process_priority(vm, loop, fake_qemu_img_binary): @@ -270,7 +270,7 @@ def test_build_command(vm, loop, fake_qemu_binary, port_manager): "-serial", "telnet:127.0.0.1:{},server,nowait".format(vm.console), "-monitor", - "telnet:127.0.0.1:{},server,nowait".format(vm.monitor), + "tcp:127.0.0.1:{},server,nowait".format(vm.monitor), "-device", "e1000,mac=00:00:ab:7e:b5:00,netdev=gns3-0", "-netdev", @@ -292,7 +292,7 @@ def test_hda_disk_image(vm, tmpdir): vm.hda_disk_image = "/tmp/test" assert vm.hda_disk_image == "/tmp/test" vm.hda_disk_image = "test" - assert vm.hda_disk_image == str(tmpdir / "test") + assert vm.hda_disk_image == str(tmpdir / "QEMU" / "test") def test_hdb_disk_image(vm, tmpdir): @@ -301,4 +301,20 @@ def test_hdb_disk_image(vm, tmpdir): vm.hdb_disk_image = "/tmp/test" assert vm.hdb_disk_image == "/tmp/test" vm.hdb_disk_image = "test" - assert vm.hdb_disk_image == str(tmpdir / "test") + assert vm.hdb_disk_image == str(tmpdir / "QEMU" / "test") + +def test_hdc_disk_image(vm, tmpdir): + + with patch("gns3server.config.Config.get_section_config", return_value={"images_path": str(tmpdir)}): + vm.hdc_disk_image = "/tmp/test" + assert vm.hdc_disk_image == "/tmp/test" + vm.hdc_disk_image = "test" + assert vm.hdc_disk_image == str(tmpdir / "QEMU" / "test") + +def test_hdd_disk_image(vm, tmpdir): + + with patch("gns3server.config.Config.get_section_config", return_value={"images_path": str(tmpdir)}): + vm.hdd_disk_image = "/tmp/test" + assert vm.hdd_disk_image == "/tmp/test" + vm.hdd_disk_image = "test" + assert vm.hdd_disk_image == str(tmpdir / "QEMU" / "test") diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py index eb52dcef..7fdaf487 100644 --- a/tests/modules/test_project.py +++ b/tests/modules/test_project.py @@ -102,7 +102,7 @@ def test_changing_location_not_allowed(tmpdir): def test_changing_path_not_allowed(tmpdir): - with patch("gns3server.config.Config.get_section_config", return_value={"local": False}): + with patch("gns3server.config.Config.getboolean", return_value=False): with pytest.raises(aiohttp.web.HTTPForbidden): p = Project() p.path = str(tmpdir) From 6d901e829582ac2fb7d263ad8079e320e094985b Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 12 Mar 2015 16:53:22 -0600 Subject: [PATCH 427/485] Fixes issue when VBoxManage returns an error. --- gns3server/modules/virtualbox/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gns3server/modules/virtualbox/__init__.py b/gns3server/modules/virtualbox/__init__.py index e0394373..b746fbf9 100644 --- a/gns3server/modules/virtualbox/__init__.py +++ b/gns3server/modules/virtualbox/__init__.py @@ -105,8 +105,7 @@ class VirtualBox(BaseManager): if process.returncode: # only the first line of the output is useful vboxmanage_error = stderr_data.decode("utf-8", errors="ignore") - log.warn("VBoxManage has returned an error: {}".format(vboxmanage_error)) - raise VirtualBoxError(vboxmanage_error.splitlines()[0]) + raise VirtualBoxError("VirtualBox has returned an error: {}".format(vboxmanage_error)) return stdout_data.decode("utf-8", errors="ignore").splitlines() From 03796ca7296d47713c2c9f2939807b256c8ce2da Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 12 Mar 2015 18:44:05 -0600 Subject: [PATCH 428/485] Server handler to shutdown a local server. --- gns3server/config.py | 2 +- gns3server/handlers/__init__.py | 1 + gns3server/handlers/api/server_handler.py | 42 +++++++++++++++++++++++ gns3server/main.py | 2 +- gns3server/server.py | 40 ++++++++++++++------- 5 files changed, 72 insertions(+), 15 deletions(-) create mode 100644 gns3server/handlers/api/server_handler.py diff --git a/gns3server/config.py b/gns3server/config.py index 657ac67b..7a91d530 100644 --- a/gns3server/config.py +++ b/gns3server/config.py @@ -157,7 +157,7 @@ class Config(object): @staticmethod def instance(files=None): """ - Singleton to return only on instance of Config. + Singleton to return only one instance of Config. :params files: Array of configuration files (optional) :returns: instance of Config diff --git a/gns3server/handlers/__init__.py b/gns3server/handlers/__init__.py index 325e8ba4..55dc3c86 100644 --- a/gns3server/handlers/__init__.py +++ b/gns3server/handlers/__init__.py @@ -26,6 +26,7 @@ from gns3server.handlers.api.qemu_handler import QEMUHandler from gns3server.handlers.api.virtualbox_handler import VirtualBoxHandler from gns3server.handlers.api.vpcs_handler import VPCSHandler from gns3server.handlers.api.config_handler import ConfigHandler +from gns3server.handlers.api.server_handler import ServerHandler from gns3server.handlers.upload_handler import UploadHandler if sys.platform.startswith("linux") or hasattr(sys, "_called_from_test"): diff --git a/gns3server/handlers/api/server_handler.py b/gns3server/handlers/api/server_handler.py new file mode 100644 index 00000000..65e8892f --- /dev/null +++ b/gns3server/handlers/api/server_handler.py @@ -0,0 +1,42 @@ +# -*- 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 ...config import Config +from aiohttp.web import HTTPForbidden +import asyncio + +class ServerHandler: + + @classmethod + @Route.post( + r"/server/shutdown", + description="Shutdown the local server", + status_codes={ + 201: "Server is shutting down", + 403: "Server shutdown refused" + }) + def shutdown(request, response): + + config = Config.instance() + if config.get_section_config("Server").getboolean("local", False) is False: + raise HTTPForbidden(text="You can only stop a local server") + + from gns3server.server import Server + server = Server.instance() + asyncio.async(server.shutdown_server()) + response.set_status(201) diff --git a/gns3server/main.py b/gns3server/main.py index 0d846445..0ef24789 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -173,7 +173,7 @@ def main(): CrashReport.instance() host = server_config["host"] port = int(server_config["port"]) - server = Server(host, port) + server = Server.instance(host, port) try: server.run() except Exception as e: diff --git a/gns3server/server.py b/gns3server/server.py index be677676..aae6ca4a 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -48,9 +48,22 @@ class Server: self._host = host self._port = port self._loop = None + self._handler = None self._start_time = time.time() self._port_manager = PortManager(host) + @staticmethod + def instance(host=None, port=None): + """ + Singleton to return only one instance of Server. + + :returns: instance of Server + """ + + if not hasattr(Server, "_instance") or Server._instance is None: + Server._instance = Server(host, port) + return Server._instance + @asyncio.coroutine def _run_application(self, handler, ssl_context=None): @@ -63,11 +76,14 @@ class Server: return server @asyncio.coroutine - def _stop_application(self): + def shutdown_server(self): """ - Cleanup the modules (shutdown running emulators etc.) + Cleanly shutdown the server. """ + if self._handler: + yield from self._handler.finish_connections() + for module in MODULES: log.debug("Unloading module {}".format(module.__name__)) m = module.instance() @@ -81,13 +97,12 @@ class Server: self._loop.stop() - def _signal_handling(self, handler): + def _signal_handling(self): @asyncio.coroutine def signal_handler(signame): log.warning("Server has got signal {}, exiting...".format(signame)) - yield from handler.finish_connections() - yield from self._stop_application() + yield from self.shutdown_server() signals = ["SIGTERM", "SIGINT"] if sys.platform.startswith("win"): @@ -103,14 +118,13 @@ class Server: else: self._loop.add_signal_handler(getattr(signal, signal_name), callback) - def _reload_hook(self, handler): + def _reload_hook(self): @asyncio.coroutine def reload(): log.info("Reloading") - yield from handler.finish_connections() - yield from self._stop_application() + yield from self.shutdown_server() os.execv(sys.executable, [sys.executable] + sys.argv) # code extracted from tornado @@ -130,7 +144,7 @@ class Server: if modified > self._start_time: log.debug("File {} has been modified".format(path)) asyncio.async(reload()) - self._loop.call_later(1, self._reload_hook, handler) + self._loop.call_later(1, self._reload_hook) def _create_ssl_context(self, server_config): @@ -196,13 +210,13 @@ class Server: m.port_manager = self._port_manager log.info("Starting server on {}:{}".format(self._host, self._port)) - handler = app.make_handler(handler=RequestHandler) - self._loop.run_until_complete(self._run_application(handler, ssl_context)) - self._signal_handling(handler) + self._handler = app.make_handler(handler=RequestHandler) + self._loop.run_until_complete(self._run_application(self._handler, ssl_context)) + self._signal_handling() if server_config.getboolean("live"): log.info("Code live reload is enabled, watching for file changes") - self._loop.call_later(1, self._reload_hook, handler) + self._loop.call_later(1, self._reload_hook) if server_config.getboolean("shell"): asyncio.async(self.start_shell()) From 36daa3627e023689ea948605f0979acb102a4183 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 12 Mar 2015 18:48:07 -0600 Subject: [PATCH 429/485] Ignore exception in asyncio loop on Windows when the local server gets a signal. --- gns3server/server.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/gns3server/server.py b/gns3server/server.py index aae6ca4a..ac84cdd7 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -221,4 +221,10 @@ class Server: if server_config.getboolean("shell"): asyncio.async(self.start_shell()) - self._loop.run_forever() + try: + self._loop.run_forever() + except TypeError as e: + # This is to ignore an asyncio.windows_events exception + # on Windows when the process get the SIGBREAK signal + # TypeError: async() takes 1 positional argument but 3 were given + log.warning("TypeError exception in the loop {}".format(e)) From 500b7112f50780a87fdfc11607e11d590417884a Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 12 Mar 2015 18:50:38 -0600 Subject: [PATCH 430/485] Assert host and port are not None when creating the Server instance. --- gns3server/server.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gns3server/server.py b/gns3server/server.py index ac84cdd7..d1f51471 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -61,6 +61,8 @@ class Server: """ if not hasattr(Server, "_instance") or Server._instance is None: + assert host is not None + assert port is not None Server._instance = Server(host, port) return Server._instance @@ -225,6 +227,6 @@ class Server: self._loop.run_forever() except TypeError as e: # This is to ignore an asyncio.windows_events exception - # on Windows when the process get the SIGBREAK signal + # on Windows when the process gets the SIGBREAK signal # TypeError: async() takes 1 positional argument but 3 were given log.warning("TypeError exception in the loop {}".format(e)) From 5637b7be860e6feb029b0edca0bc5e485d5015c4 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 12 Mar 2015 18:51:22 -0600 Subject: [PATCH 431/485] Bump version to 1.3.0beta2.dev1 --- gns3server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/version.py b/gns3server/version.py index 277c6f63..166c0f14 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,5 +23,5 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "1.3.0beta1" +__version__ = "1.3.0beta2.dev1" __version_info__ = (1, 3, 0, -99) From c596147b59722b0ce7fdb3ebf646e114c944079d Mon Sep 17 00:00:00 2001 From: grossmj Date: Thu, 12 Mar 2015 20:56:10 -0600 Subject: [PATCH 432/485] List the iourc file in upload handler. --- gns3server/handlers/upload_handler.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/gns3server/handlers/upload_handler.py b/gns3server/handlers/upload_handler.py index 0e0a7291..7fef01b5 100644 --- a/gns3server/handlers/upload_handler.py +++ b/gns3server/handlers/upload_handler.py @@ -32,16 +32,19 @@ class UploadHandler: api_version=None ) def index(request, response): - image_files = [] + uploaded_files = [] try: for root, _, files in os.walk(UploadHandler.image_directory()): for filename in files: image_file = os.path.join(root, filename) if os.access(image_file, os.X_OK): - image_files.append(image_file) + uploaded_files.append(image_file) except OSError: pass - response.template("upload.html", files=image_files) + iourc_path = os.path.join(os.path.expanduser("~/"), ".iourc") + if os.path.exists(iourc_path): + uploaded_files.append(iourc_path) + response.template("upload.html", files=uploaded_files) @classmethod @Route.post( From f7d3af4a590f1e60b9b1557e5db26fce868a8a3d Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 13 Mar 2015 11:45:38 -0600 Subject: [PATCH 433/485] Fixes hostid retrieval. --- gns3server/modules/iou/iou_vm.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 4d80f947..7325a8ea 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -339,6 +339,7 @@ class IOUVM(BaseVM): raise IOUError("The following shared library dependencies cannot be found for IOU image {}: {}".format(self._path, ", ".join(missing_libs))) + @asyncio.coroutine def _check_iou_licence(self): """ Checks for a valid IOU key in the iourc file (paranoid mode). @@ -358,16 +359,17 @@ class IOUVM(BaseVM): if hostname not in config["license"]: raise IOUError("Hostname key not found in iourc file {}".format(self.iourc_path)) user_ioukey = config["license"][hostname] - print(user_ioukey[-1:]) if user_ioukey[-1:] != ';': raise IOUError("IOU key not ending with ; in iourc file".format(self.iourc_path)) if len(user_ioukey) != 17: raise IOUError("IOU key length is not 16 characters in iourc file".format(self.iourc_path)) user_ioukey = user_ioukey[:16] try: - hostid = os.popen("hostid").read().strip() - except OSError as e: - raise IOUError("Could not read the hostid: {}".format(e)) + hostid = (yield from gns3server.utils.asyncio.subprocess_check_output("hostid")).strip() + except FileNotFoundError as e: + raise IOUError("Could not find hostid: {}".format(e)) + except subprocess.SubprocessError as e: + raise IOUError("Could not execute hostid: {}".format(e)) try: ioukey = int(hostid, 16) except ValueError: @@ -401,7 +403,7 @@ class IOUVM(BaseVM): license_check = self._manager.config.get_section_config("IOU").getboolean("license_check", True) if license_check: - self._check_iou_licence() + yield from self._check_iou_licence() iouyap_path = self.iouyap_path if not iouyap_path or not os.path.isfile(iouyap_path): From 81420c60c7be125c27df1a6034a5a98e178f3fcf Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 13 Mar 2015 11:46:02 -0600 Subject: [PATCH 434/485] Changes words in upload template. --- gns3server/templates/upload.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gns3server/templates/upload.html b/gns3server/templates/upload.html index eeeeae55..a894e467 100644 --- a/gns3server/templates/upload.html +++ b/gns3server/templates/upload.html @@ -2,10 +2,10 @@ {% block body %}

Select & Upload an image for GNS3

- File:
- Image type:
+ File type: From 221befa73eddffd6b9992255c5349e7cf129faf0 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 13 Mar 2015 14:43:39 -0600 Subject: [PATCH 435/485] Option to record curl requests into a file (to replay them later). --- gns3server/main.py | 3 +++ gns3server/web/route.py | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/gns3server/main.py b/gns3server/main.py index 0ef24789..ac6c6b19 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -86,6 +86,7 @@ def parse_arguments(argv, config): "ssl": config.getboolean("ssl", False), "certfile": config.get("certfile", ""), "certkey": config.get("certkey", ""), + "record": config.get("record", ""), "local": config.getboolean("local", False), "allow": config.getboolean("allow_remote_console", False), "quiet": config.getboolean("quiet", False), @@ -101,6 +102,7 @@ def parse_arguments(argv, config): parser.add_argument("--ssl", action="store_true", help="run in SSL mode") parser.add_argument("--certfile", help="SSL cert file") parser.add_argument("--certkey", help="SSL key file") + parser.add_argument("--record", help="save curl requests into a file") parser.add_argument("-L", "--local", action="store_true", help="local mode (allows some insecure operations)") parser.add_argument("-A", "--allow", action="store_true", help="allow remote connections to local console ports") parser.add_argument("-q", "--quiet", action="store_true", help="do not show logs on stdout") @@ -122,6 +124,7 @@ def set_config(args): server_config["ssl"] = str(args.ssl) server_config["certfile"] = args.certfile server_config["certkey"] = args.certkey + server_config["record"] = args.record server_config["debug"] = str(args.debug) server_config["live"] = str(args.live) server_config["shell"] = str(args.shell) diff --git a/gns3server/web/route.py b/gns3server/web/route.py index f6f9407f..84b06da4 100644 --- a/gns3server/web/route.py +++ b/gns3server/web/route.py @@ -28,6 +28,7 @@ log = logging.getLogger(__name__) from ..modules.vm_error import VMError from .response import Response from ..crash_report import CrashReport +from ..config import Config @asyncio.coroutine @@ -125,6 +126,15 @@ class Route(object): # API call try: request = yield from parse_request(request, input_schema) + server_config = Config.instance().get_section_config("Server") + record_file = server_config.get("record") + if record_file: + try: + with open(record_file, "a") as f: + f.write("curl -X {} 'http://{}{}' -d '{}'".format(request.method, request.host, request.path_qs, json.dumps(request.json))) + f.write("\n") + except OSError as e: + log.warn("Could not write to the record file {}: {}".format(record_file, e)) response = Response(route=route, output_schema=output_schema) yield from func(request, response) except aiohttp.web.HTTPException as e: From a81d2274cd3f5c637fc944d58d86c009001acfcc Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 13 Mar 2015 15:15:27 -0600 Subject: [PATCH 436/485] Adds info either the server is started as a local server in VersionHandler response. --- gns3server/handlers/api/version_handler.py | 8 +++++--- gns3server/schemas/version.py | 4 ++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/gns3server/handlers/api/version_handler.py b/gns3server/handlers/api/version_handler.py index 930c733e..10dc138a 100644 --- a/gns3server/handlers/api/version_handler.py +++ b/gns3server/handlers/api/version_handler.py @@ -16,12 +16,11 @@ # along with this program. If not, see . from ...web.route import Route +from ...config import Config from ...schemas.version import VERSION_SCHEMA from ...version import __version__ from aiohttp.web import HTTPConflict -import asyncio - class VersionHandler: @@ -31,7 +30,10 @@ class VersionHandler: description="Retrieve the server version number", output=VERSION_SCHEMA) def version(request, response): - response.json({"version": __version__}) + + config = Config.instance() + local_server =config.get_section_config("Server").getboolean("local", False) + response.json({"version": __version__, "local": local_server}) @classmethod @Route.post( diff --git a/gns3server/schemas/version.py b/gns3server/schemas/version.py index 127084df..95e08507 100644 --- a/gns3server/schemas/version.py +++ b/gns3server/schemas/version.py @@ -24,6 +24,10 @@ VERSION_SCHEMA = { "version": { "description": "Version number human readable", "type": "string", + }, + "local": { + "description": "Either this is a local server", + "type": "boolean", } } } From 4ccca5dc99dc4070ec24b937ea39c4e893f00552 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 13 Mar 2015 17:13:36 -0600 Subject: [PATCH 437/485] Support RAM setting for VirtualBox VMs. --- gns3server/handlers/api/virtualbox_handler.py | 10 ++++++ gns3server/modules/virtualbox/__init__.py | 12 ++++++- .../modules/virtualbox/virtualbox_vm.py | 31 +++++++++++++++++++ gns3server/schemas/virtualbox.py | 18 +++++++++++ 4 files changed, 70 insertions(+), 1 deletion(-) diff --git a/gns3server/handlers/api/virtualbox_handler.py b/gns3server/handlers/api/virtualbox_handler.py index f8a71845..f0765180 100644 --- a/gns3server/handlers/api/virtualbox_handler.py +++ b/gns3server/handlers/api/virtualbox_handler.py @@ -73,6 +73,11 @@ class VirtualBoxHandler: if "enable_remote_console" in request.json: yield from vm.set_enable_remote_console(request.json.pop("enable_remote_console")) + if "ram" in request.json: + ram = request.json.pop("ram") + if ram != vm.ram: + yield from vm.set_ram(ram) + for name, value in request.json.items(): if hasattr(vm, name) and getattr(vm, name) != value: setattr(vm, name, value) @@ -134,6 +139,11 @@ class VirtualBoxHandler: if adapters != vm.adapters: yield from vm.set_adapters(adapters) + if "ram" in request.json: + ram = request.json.pop("ram") + if ram != vm.ram: + yield from vm.set_ram(ram) + for name, value in request.json.items(): if hasattr(vm, name) and getattr(vm, name) != value: setattr(vm, name, value) diff --git a/gns3server/modules/virtualbox/__init__.py b/gns3server/modules/virtualbox/__init__.py index b746fbf9..18253925 100644 --- a/gns3server/modules/virtualbox/__init__.py +++ b/gns3server/modules/virtualbox/__init__.py @@ -124,7 +124,17 @@ class VirtualBox(BaseManager): continue # ignore inaccessible VMs extra_data = yield from self.execute("getextradata", [vmname, "GNS3/Clone"]) if not extra_data[0].strip() == "Value: yes": - vms.append(vmname) + # get the amount of RAM + info_results = yield from self.execute("showvminfo", [vmname, "--machinereadable"]) + for info in info_results: + try: + name, value = info.split('=', 1) + if name.strip() == "memory": + ram = int(value.strip()) + break + except ValueError: + continue + vms.append({"vmname": vmname, "ram": ram}) return vms @staticmethod diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 7375eb1d..d8c29afe 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -67,6 +67,7 @@ class VirtualBoxVM(BaseVM): self._enable_remote_console = False self._vmname = vmname self._use_any_adapter = False + self._ram = 0 self._adapter_type = "Intel PRO/1000 MT Desktop (82540EM)" def __json__(self): @@ -80,6 +81,7 @@ class VirtualBoxVM(BaseVM): "enable_remote_console": self.enable_remote_console, "adapters": self._adapters, "adapter_type": self.adapter_type, + "ram": self.ram, "use_any_adapter": self.use_any_adapter} @asyncio.coroutine @@ -152,6 +154,9 @@ class VirtualBoxVM(BaseVM): if self._adapters: yield from self.set_adapters(self._adapters) + vm_info = yield from self._get_vm_info() + self._ram = int(vm_info["memory"]) + @asyncio.coroutine def start(self): """ @@ -407,6 +412,32 @@ class VirtualBoxVM(BaseVM): self._stop_remote_console() self._enable_remote_console = enable_remote_console + @property + def ram(self): + """ + Returns the amount of RAM allocated to this VirtualBox VM. + + :returns: amount RAM in MB (integer) + """ + + return self._ram + + @asyncio.coroutine + def set_ram(self, ram): + """ + Set the amount of RAM allocated to this VirtualBox VM. + + :param ram: amount RAM in MB (integer) + """ + + if ram == 0: + return + + yield from self._modify_vm('--memory {}'.format(ram)) + + log.info("VirtualBox VM '{name}' [{id}] has set amount of RAM to {ram}".format(name=self.name, id=self.id, ram=ram)) + self._ram = ram + @property def vmname(self): """ diff --git a/gns3server/schemas/virtualbox.py b/gns3server/schemas/virtualbox.py index b527f6f6..930cbbbe 100644 --- a/gns3server/schemas/virtualbox.py +++ b/gns3server/schemas/virtualbox.py @@ -70,6 +70,12 @@ VBOX_CREATE_SCHEMA = { "description": "enable the remote console", "type": "boolean" }, + "ram": { + "description": "Amount of RAM", + "minimum": 0, + "maximum": 65535, + "type": "integer" + }, "headless": { "description": "headless mode", "type": "boolean" @@ -119,6 +125,12 @@ VBOX_UPDATE_SCHEMA = { "description": "enable the remote console", "type": "boolean" }, + "ram": { + "description": "Amount of RAM", + "minimum": 0, + "maximum": 65535, + "type": "integer" + }, "headless": { "description": "headless mode", "type": "boolean" @@ -240,6 +252,12 @@ VBOX_OBJECT_SCHEMA = { "maximum": 65535, "type": "integer" }, + "ram": { + "description": "Amount of RAM", + "minimum": 0, + "maximum": 65535, + "type": "integer" + }, }, "additionalProperties": False, "required": ["name", "vm_id", "project_id"] From cf92bfe81ec2bb71e3f65ccdcd6d37b197e1861c Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 13 Mar 2015 18:57:27 -0600 Subject: [PATCH 438/485] Alternative local server shutdown (mostly intended for Windows). --- gns3server/handlers/api/server_handler.py | 24 +++++++++++++++++++++++ gns3server/modules/iou/iou_vm.py | 2 +- gns3server/modules/project_manager.py | 10 ++++++++++ gns3server/server.py | 4 ++-- 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/gns3server/handlers/api/server_handler.py b/gns3server/handlers/api/server_handler.py index 65e8892f..105129f2 100644 --- a/gns3server/handlers/api/server_handler.py +++ b/gns3server/handlers/api/server_handler.py @@ -17,8 +17,14 @@ from ...web.route import Route from ...config import Config +from ...modules.project_manager import ProjectManager from aiohttp.web import HTTPForbidden + import asyncio +import logging + +log = logging.getLogger(__name__) + class ServerHandler: @@ -36,6 +42,24 @@ class ServerHandler: if config.get_section_config("Server").getboolean("local", False) is False: raise HTTPForbidden(text="You can only stop a local server") + # close all the projects first + pm = ProjectManager.instance() + projects = pm.projects + + tasks = [] + for project in projects: + tasks.append(asyncio.async(project.close())) + + if tasks: + done, _ = yield from asyncio.wait(tasks) + for future in done: + try: + future.result() + except Exception as e: + log.error("Could not close project {}".format(e), exc_info=1) + continue + + # then shutdown the server itself from gns3server.server import Server server = Server.instance() asyncio.async(server.shutdown_server()) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 7325a8ea..36e2e7c0 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -401,7 +401,7 @@ class IOUVM(BaseVM): if iourc_path and not os.path.isfile(iourc_path): raise IOUError("A valid iourc file is necessary to start IOU") - license_check = self._manager.config.get_section_config("IOU").getboolean("license_check", True) + license_check = self._manager.config.get_section_config("IOU").getboolean("license_check", False) if license_check: yield from self._check_iou_licence() diff --git a/gns3server/modules/project_manager.py b/gns3server/modules/project_manager.py index 454b3114..4ea21612 100644 --- a/gns3server/modules/project_manager.py +++ b/gns3server/modules/project_manager.py @@ -42,6 +42,16 @@ class ProjectManager: cls._instance = cls() return cls._instance + @property + def projects(self): + """ + Returns all projects. + + :returns: Project instances + """ + + return self._projects.values() + def get_project(self, project_id): """ Returns a Project instance. diff --git a/gns3server/server.py b/gns3server/server.py index d1f51471..dca8ff29 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -190,8 +190,8 @@ class Server: # because asyncio.add_signal_handler() is not supported yet on that platform # otherwise the loop runs outside of signal module's ability to trap signals. def wakeup(): - loop.call_later(0.1, wakeup) - loop.call_later(0.1, wakeup) + loop.call_later(0.5, wakeup) + loop.call_later(0.5, wakeup) asyncio.set_event_loop(loop) ssl_context = None From 95f9431b59ab3144cb5b746b00e8714d75c724ad Mon Sep 17 00:00:00 2001 From: grossmj Date: Fri, 13 Mar 2015 22:00:19 -0600 Subject: [PATCH 439/485] Update CHANGELOG --- CHANGELOG | 48 ++++++++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 52012e13..a80989ce 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,33 +1,37 @@ # Change Log +## 1.3.0beta2 13/03/2015 + +* Fixed issue when VBoxManage returns an error. +* Server handler to shutdown a local server. +* List the iourc file in upload handler. +* Fixed hostid error. +* Support RAM setting for VirtualBox VMs. +* Alternative local server shutdown (intended for Windows). +* Request user permission to kill the local server if it cannot be stopped. + ## 1.3.0beta1 11/03/2015 -* New title for VMs/Devices/routers preference pages. -* Deactivate auto idle-pc in contextual menu while we think about a better implementation. * Optional IOU license key check. -* Relative picture paths are saved in projects. * Relative path support of IOU, IOS and Qemu images. -* More checks when automatically starting the local server and find an alternative port if needed. +* Do not give attachment warning for generic attachments in VirtualBox. * Support for HDC and HDD disk images in Qemu. -* Fixed base IOS and IOU base configs. -* Fixed GNS3 console issues. -* Renamed server.conf and server.ini to gns3_server.conf and gns3_server.ini respectively. -* Remove remote servers list from module preferences + some other prefences re-factoring. -* Automatically convert old projects on remote servers. -* Bump the progress dialog minimum duration before display to 1000ms. -* Fixed port listing bug with Cloud and Host nodes. +* Fixed bug when starting a packet capture in VirtualBox with the project path containing spaces. +* Renames server.conf and server.ini to gns3_server.conf and gns3_server.ini respectively. +* Use TCP instead of Telnet to communicate with Qemu monitor. +* Have the server look in the right place for relative image paths. +* Fixed bugs when checking if this is a local project. +* Concert old projects on remote servers. +* Properly restore configs for Dynamips routers. +* Fixed rename bug for linked clones in VirtualBox. +* Makes absolute path checks work on Windows. +* Upload IOURC file via the web interface +* Upload interface allows users to choose an image type. * Fixed Qemu networking. -* Give a warning when a object is move the background layer. -* Option to draw a rectangle when a node is selected. -* New project icon (little yellow indicator). -* Default name for screenshot file is "screenshot". -* Alignment options (horizontal & vertical). -* Fixed import / export of the preferences file. -* Fixed pkg_ressource bug. -* Brought back Qemu preferences page. -* Include SSL cacert file with GNS3 Windows exe and Mac OS App to send crash report using HTTPS. -* Fixed adapter bug with VirtualBox. -* Fixed various errors when a project was not initialized. +* Fixed suspend and resume for Qemu VMs. +* Fixed crash when you start capture on a non running IOU. +* Fixed Telnet server initialization issue in VirtualBox. +* Disconnect network cable if adapter is not attached in VirtualBox vNIC. ## 1.3.0alpha1 03/03/2015 From a90805135dd74c9d1a74bec85720252a1660de40 Mon Sep 17 00:00:00 2001 From: grossmj Date: Fri, 13 Mar 2015 22:02:28 -0600 Subject: [PATCH 440/485] Bump version to 1.3.0beta2 --- gns3server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/version.py b/gns3server/version.py index 166c0f14..b0e20fe9 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,5 +23,5 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "1.3.0beta2.dev1" +__version__ = "1.3.0beta2" __version_info__ = (1, 3, 0, -99) From 25b778aec031b84d10e572a06df45557c9ea6843 Mon Sep 17 00:00:00 2001 From: grossmj Date: Fri, 13 Mar 2015 22:42:25 -0600 Subject: [PATCH 441/485] Bump version to 1.3.0rc1.dev1 --- gns3server/crash_report.py | 1 + gns3server/version.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py index b8260568..d26f9343 100644 --- a/gns3server/crash_report.py +++ b/gns3server/crash_report.py @@ -74,6 +74,7 @@ class CrashReport: report = self._client.captureException() except Exception as e: log.error("Can't send crash report to Sentry: {}".format(e)) + return log.info("Crash report sent with event ID: {}".format(self._client.get_ident(report))) @classmethod diff --git a/gns3server/version.py b/gns3server/version.py index b0e20fe9..89518e96 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,5 +23,5 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "1.3.0beta2" +__version__ = "1.3.0rc1.dev1" __version_info__ = (1, 3, 0, -99) From 6d56da03e5dff4aae90a0d861fc64a0d13e6d549 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 14 Mar 2015 13:16:27 -0600 Subject: [PATCH 442/485] Fixes tests. --- gns3server/modules/iou/iou_vm.py | 9 +++-- .../modules/virtualbox/virtualbox_vm.py | 3 +- gns3server/schemas/project.py | 2 +- tests/handlers/api/test_project.py | 37 +++++++++++-------- .../modules/dynamips/test_dynamips_router.py | 5 +-- tests/modules/iou/test_iou_vm.py | 26 +++++++------ tests/modules/test_manager.py | 2 +- tests/modules/test_project.py | 15 ++++---- 8 files changed, 53 insertions(+), 46 deletions(-) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 36e2e7c0..0ccbf376 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -345,6 +345,10 @@ class IOUVM(BaseVM): Checks for a valid IOU key in the iourc file (paranoid mode). """ + license_check = self._manager.config.get_section_config("IOU").getboolean("license_check", False) + if license_check: + return + config = configparser.ConfigParser() try: with open(self.iourc_path) as f: @@ -401,10 +405,7 @@ class IOUVM(BaseVM): if iourc_path and not os.path.isfile(iourc_path): raise IOUError("A valid iourc file is necessary to start IOU") - license_check = self._manager.config.get_section_config("IOU").getboolean("license_check", False) - if license_check: - yield from self._check_iou_licence() - + yield from self._check_iou_licence() iouyap_path = self.iouyap_path if not iouyap_path or not os.path.isfile(iouyap_path): raise IOUError("iouyap is necessary to start IOU") diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index d8c29afe..83dc86d3 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -155,7 +155,8 @@ class VirtualBoxVM(BaseVM): yield from self.set_adapters(self._adapters) vm_info = yield from self._get_vm_info() - self._ram = int(vm_info["memory"]) + if "memory" in vm_info: + self._ram = int(vm_info["memory"]) @asyncio.coroutine def start(self): diff --git a/gns3server/schemas/project.py b/gns3server/schemas/project.py index 38bea4c0..3e9dfa6d 100644 --- a/gns3server/schemas/project.py +++ b/gns3server/schemas/project.py @@ -53,7 +53,7 @@ PROJECT_UPDATE_SCHEMA = { "properties": { "name": { "description": "Project name", - "type": "string", + "type": ["string", "null"], "minLength": 1 }, "temporary": { diff --git a/tests/handlers/api/test_project.py b/tests/handlers/api/test_project.py index 659c78a8..cd0bc419 100644 --- a/tests/handlers/api/test_project.py +++ b/tests/handlers/api/test_project.py @@ -25,44 +25,49 @@ from tests.utils import asyncio_patch def test_create_project_with_path(server, tmpdir): - with patch("gns3server.config.Config.get_section_config", return_value={"local": True}): - response = server.post("/projects", {"path": str(tmpdir)}) + with patch("gns3server.modules.project.Project.is_local", return_value=True): + response = server.post("/projects", {"name": "test", "path": str(tmpdir)}) assert response.status == 201 assert response.json["path"] == str(tmpdir) + assert response.json["name"] == "test" def test_create_project_without_dir(server): - query = {} + query = {"name": "test"} response = server.post("/projects", query, example=True) assert response.status == 201 assert response.json["project_id"] is not None assert response.json["temporary"] is False + assert response.json["name"] == "test" def test_create_temporary_project(server): - query = {"temporary": True} + query = {"name": "test", "temporary": True} response = server.post("/projects", query) assert response.status == 201 assert response.json["project_id"] is not None assert response.json["temporary"] is True + assert response.json["name"] == "test" def test_create_project_with_uuid(server): - query = {"project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f"} + query = {"name": "test", "project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f"} response = server.post("/projects", query) assert response.status == 201 assert response.json["project_id"] == "00010203-0405-0607-0809-0a0b0c0d0e0f" + assert response.json["name"] == "test" def test_show_project(server): - query = {"project_id": "00010203-0405-0607-0809-0a0b0c0d0e02", "temporary": False} + query = {"name": "test", "project_id": "00010203-0405-0607-0809-0a0b0c0d0e02", "temporary": False} response = server.post("/projects", query) assert response.status == 201 response = server.get("/projects/00010203-0405-0607-0809-0a0b0c0d0e02", example=True) - assert len(response.json.keys()) == 4 + assert len(response.json.keys()) == 5 assert len(response.json["location"]) > 0 assert response.json["project_id"] == "00010203-0405-0607-0809-0a0b0c0d0e02" assert response.json["temporary"] is False + assert response.json["name"] == "test" def test_show_project_invalid_uuid(server): @@ -71,10 +76,10 @@ def test_show_project_invalid_uuid(server): def test_update_temporary_project(server): - query = {"temporary": True} + query = {"name": "test", "temporary": True} response = server.post("/projects", query) assert response.status == 201 - query = {"temporary": False} + query = {"name": "test", "temporary": False} response = server.put("/projects/{project_id}".format(project_id=response.json["project_id"]), query, example=True) assert response.status == 200 assert response.json["temporary"] is False @@ -82,21 +87,23 @@ def test_update_temporary_project(server): def test_update_path_project(server, tmpdir): - with patch("gns3server.config.Config.get_section_config", return_value={"local": True}): - response = server.post("/projects", {}) + with patch("gns3server.modules.project.Project.is_local", return_value=True): + response = server.post("/projects", {"name": "first_name"}) assert response.status == 201 - query = {"path": str(tmpdir)} + assert response.json["name"] == "first_name" + query = {"name": "second_name", "path": str(tmpdir)} response = server.put("/projects/{project_id}".format(project_id=response.json["project_id"]), query, example=True) assert response.status == 200 assert response.json["path"] == str(tmpdir) + assert response.json["name"] == "second_name" def test_update_path_project_non_local(server, tmpdir): - with patch("gns3server.config.Config.get_section_config", return_value={"local": False}): - response = server.post("/projects", {}) + with patch("gns3server.modules.project.Project.is_local", return_value=False): + response = server.post("/projects", {"name": "first_name"}) assert response.status == 201 - query = {"path": str(tmpdir)} + query = {"name": "second_name", "path": str(tmpdir)} response = server.put("/projects/{project_id}".format(project_id=response.json["project_id"]), query, example=True) assert response.status == 403 diff --git a/tests/modules/dynamips/test_dynamips_router.py b/tests/modules/dynamips/test_dynamips_router.py index 48b76536..de5fb99d 100644 --- a/tests/modules/dynamips/test_dynamips_router.py +++ b/tests/modules/dynamips/test_dynamips_router.py @@ -44,10 +44,7 @@ def test_router(project, manager): def test_router_invalid_dynamips_path(project, manager, loop): - config = configparser.ConfigParser() - config.add_section("Dynamips") - config.set("Dynamips", "dynamips_path", "/bin/test_fake") - with patch("gns3server.config.Config", return_value=config): + with patch("gns3server.config.Config.get_section_config", return_value={"dynamips_path": "/bin/test_fake"}): with pytest.raises(DynamipsError): router = Router("test", "00010203-0405-0607-0809-0a0b0c0d0e0e", project, manager) loop.run_until_complete(asyncio.async(router.create())) diff --git a/tests/modules/iou/test_iou_vm.py b/tests/modules/iou/test_iou_vm.py index ced8f49c..a6ab2ab6 100644 --- a/tests/modules/iou/test_iou_vm.py +++ b/tests/modules/iou/test_iou_vm.py @@ -85,11 +85,12 @@ def test_vm_invalid_iouyap_path(project, manager, loop): def test_start(loop, vm, monkeypatch): with patch("gns3server.modules.iou.iou_vm.IOUVM._check_requirements", return_value=True): - with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._start_ioucon", return_value=True): - with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._start_iouyap", return_value=True): - with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): - loop.run_until_complete(asyncio.async(vm.start())) - assert vm.is_running() + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._check_iou_licence", return_value=True): + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._start_ioucon", return_value=True): + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._start_iouyap", return_value=True): + with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): + loop.run_until_complete(asyncio.async(vm.start())) + assert vm.is_running() def test_start_with_iourc(loop, vm, monkeypatch, tmpdir): @@ -100,13 +101,14 @@ def test_start_with_iourc(loop, vm, monkeypatch, tmpdir): with patch("gns3server.config.Config.get_section_config", return_value={"iourc_path": fake_file, "iouyap_path": vm.iouyap_path}): with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._check_requirements", return_value=True): - with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._start_ioucon", return_value=True): - with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._start_iouyap", return_value=True): - with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()) as exec_mock: - loop.run_until_complete(asyncio.async(vm.start())) - assert vm.is_running() - arsgs, kwargs = exec_mock.call_args - assert kwargs["env"]["IOURC"] == fake_file + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._check_iou_licence", return_value=True): + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._start_ioucon", return_value=True): + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._start_iouyap", return_value=True): + with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()) as exec_mock: + loop.run_until_complete(asyncio.async(vm.start())) + assert vm.is_running() + arsgs, kwargs = exec_mock.call_args + assert kwargs["env"]["IOURC"] == fake_file def test_rename_nvram_file(loop, vm, monkeypatch): diff --git a/tests/modules/test_manager.py b/tests/modules/test_manager.py index 5efa1e33..81a2ddf0 100644 --- a/tests/modules/test_manager.py +++ b/tests/modules/test_manager.py @@ -60,7 +60,7 @@ def test_create_vm_new_topology_without_uuid(loop, project, port_manager): def test_create_vm_old_topology(loop, project, tmpdir, port_manager): - with patch("gns3server.config.Config.get_section_config", return_value={"local": True}): + with patch("gns3server.modules.project.Project.is_local", return_value=True): # Create an old topology directory project_dir = str(tmpdir / "testold") vm_dir = os.path.join(project_dir, "testold-files", "vpcs", "pc-1") diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py index 7fdaf487..60ec1765 100644 --- a/tests/modules/test_project.py +++ b/tests/modules/test_project.py @@ -20,7 +20,6 @@ import os import asyncio import pytest import aiohttp -import shutil from uuid import uuid4 from unittest.mock import patch @@ -51,7 +50,7 @@ def test_affect_uuid(): def test_path(tmpdir): - with patch("gns3server.config.Config.get_section_config", return_value={"local": True}): + with patch("gns3server.modules.project.Project.is_local", return_value=True): p = Project(location=str(tmpdir)) assert p.path == os.path.join(str(tmpdir), p.id) assert os.path.exists(os.path.join(str(tmpdir), p.id)) @@ -60,14 +59,14 @@ def test_path(tmpdir): def test_init_path(tmpdir): - with patch("gns3server.config.Config.get_section_config", return_value={"local": True}): + with patch("gns3server.modules.project.Project.is_local", return_value=True): p = Project(path=str(tmpdir)) assert p.path == str(tmpdir) def test_changing_path_temporary_flag(tmpdir): - with patch("gns3server.config.Config.get_section_config", return_value={"local": True}): + with patch("gns3server.modules.project.Project.is_local", return_value=True): p = Project(temporary=True) assert os.path.exists(p.path) assert os.path.exists(os.path.join(p.path, ".gns3_temporary")) @@ -96,13 +95,13 @@ def test_remove_temporary_flag(): def test_changing_location_not_allowed(tmpdir): - with patch("gns3server.config.Config.get_section_config", return_value={"local": False}): + with patch("gns3server.modules.project.Project.is_local", return_value=False): with pytest.raises(aiohttp.web.HTTPForbidden): p = Project(location=str(tmpdir)) def test_changing_path_not_allowed(tmpdir): - with patch("gns3server.config.Config.getboolean", return_value=False): + with patch("gns3server.modules.project.Project.is_local", return_value=False): with pytest.raises(aiohttp.web.HTTPForbidden): p = Project() p.path = str(tmpdir) @@ -110,11 +109,11 @@ def test_changing_path_not_allowed(tmpdir): def test_json(tmpdir): p = Project() - assert p.__json__() == {"location": p.location, "path": p.path, "project_id": p.id, "temporary": False} + assert p.__json__() == {"name": p.name, "location": p.location, "path": p.path, "project_id": p.id, "temporary": False} def test_vm_working_directory(tmpdir, vm): - with patch("gns3server.config.Config.get_section_config", return_value={"local": True}): + with patch("gns3server.modules.project.Project.is_local", return_value=True): p = Project(location=str(tmpdir)) assert p.vm_working_directory(vm) == os.path.join(str(tmpdir), p.id, 'project-files', vm.module_name, vm.id) assert os.path.exists(p.vm_working_directory(vm)) From 30ed89847ba14b6b5f1e210df36a91e12fb70816 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 14 Mar 2015 15:40:00 -0600 Subject: [PATCH 443/485] Adds netifaces module in the setup dependencies. --- gns3server/utils/interfaces.py | 2 +- setup.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/gns3server/utils/interfaces.py b/gns3server/utils/interfaces.py index 27774eb3..79e4ef28 100644 --- a/gns3server/utils/interfaces.py +++ b/gns3server/utils/interfaces.py @@ -91,7 +91,7 @@ def interfaces(): results.append({"id": interface, "name": interface}) except ImportError: - return + raise aiohttp.web.HTTPInternalServerError(text="Could not import netifaces module") else: try: results = get_windows_interfaces() diff --git a/setup.py b/setup.py index a7fd1da1..7d26539f 100644 --- a/setup.py +++ b/setup.py @@ -39,6 +39,9 @@ dependencies = ["aiohttp==0.14.4", "Jinja2==2.7.3", "raven==5.2.0"] +if not sys.platform.startswith("win"): + dependencies.append("netifaces==0.10.4") + if sys.version_info == (3, 3): dependencies.append("asyncio==3.4.2") From 21587fda5a57a10a06c5129aea578b252221099e Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 14 Mar 2015 16:31:15 -0600 Subject: [PATCH 444/485] Removes confreg setting for IOS routers. --- gns3server/modules/dynamips/nodes/router.py | 29 --------------------- gns3server/schemas/dynamips_vm.py | 18 ------------- 2 files changed, 47 deletions(-) diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index 8cfa23a8..63acce4d 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -86,7 +86,6 @@ class Router(BaseVM): self._exec_area = 64 # 64 MB on other systems self._disk0 = 0 # Megabytes self._disk1 = 0 # Megabytes - self._confreg = "0x2102" self._aux = aux self._mac_addr = "" self._system_id = "FTX0945W0MY" # processor board ID in IOS @@ -145,7 +144,6 @@ class Router(BaseVM): "exec_area": self._exec_area, "disk0": self._disk0, "disk1": self._disk1, - "confreg": self._confreg, "console": self._console, "aux": self._aux, "mac_addr": self._mac_addr, @@ -863,33 +861,6 @@ class Router(BaseVM): new_disk1=disk1)) self._disk1 = disk1 - @property - def confreg(self): - """ - Returns the configuration register. - The default is 0x2102. - - :returns: configuration register value (string) - """ - - return self._confreg - - @asyncio.coroutine - def set_confreg(self, confreg): - """ - Sets the configuration register. - - :param confreg: configuration register value (string) - """ - - yield from self._hypervisor.send('vm set_conf_reg "{name}" {confreg}'.format(name=self._name, confreg=confreg)) - - log.info('Router "{name}" [{id}]: confreg updated from {old_confreg} to {new_confreg}'.format(name=self._name, - id=self._id, - old_confreg=self._confreg, - new_confreg=confreg)) - self._confreg = confreg - @asyncio.coroutine def set_console(self, console): """ diff --git a/gns3server/schemas/dynamips_vm.py b/gns3server/schemas/dynamips_vm.py index 32e9dad5..934646c8 100644 --- a/gns3server/schemas/dynamips_vm.py +++ b/gns3server/schemas/dynamips_vm.py @@ -120,12 +120,6 @@ VM_CREATE_SCHEMA = { "description": "disk1 size in MB", "type": "integer" }, - "confreg": { - "description": "configuration register", - "type": "string", - "minLength": 1, - "pattern": "^0x[0-9a-fA-F]{4}$" - }, "console": { "description": "console TCP port", "type": "integer", @@ -353,12 +347,6 @@ VM_UPDATE_SCHEMA = { "description": "disk1 size in MB", "type": "integer" }, - "confreg": { - "description": "configuration register", - "type": "string", - "minLength": 1, - "pattern": "^0x[0-9a-fA-F]{4}$" - }, "console": { "description": "console TCP port", "type": "integer", @@ -754,12 +742,6 @@ VM_OBJECT_SCHEMA = { "description": "disk1 size in MB", "type": "integer" }, - "confreg": { - "description": "configuration register", - "type": "string", - "minLength": 1, - "pattern": "^0x[0-9a-fA-F]{4}$" - }, "console": { "description": "console TCP port", "type": "integer", From 3a6a04b8e55745a47ac5e7634f225564e430e6e3 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 16 Mar 2015 10:18:37 +0100 Subject: [PATCH 445/485] Fix version test --- gns3server/handlers/api/version_handler.py | 2 +- tests/handlers/api/test_version.py | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/gns3server/handlers/api/version_handler.py b/gns3server/handlers/api/version_handler.py index 10dc138a..22a2131c 100644 --- a/gns3server/handlers/api/version_handler.py +++ b/gns3server/handlers/api/version_handler.py @@ -32,7 +32,7 @@ class VersionHandler: def version(request, response): config = Config.instance() - local_server =config.get_section_config("Server").getboolean("local", False) + local_server = config.get_section_config("Server").getboolean("local", False) response.json({"version": __version__, "local": local_server}) @classmethod diff --git a/tests/handlers/api/test_version.py b/tests/handlers/api/test_version.py index 0691d8f9..1af2ac89 100644 --- a/tests/handlers/api/test_version.py +++ b/tests/handlers/api/test_version.py @@ -20,13 +20,23 @@ This test suite check /version endpoint It's also used for unittest the HTTP implementation. """ +from unittest.mock import patch, MagicMock +from configparser import ConfigParser + from gns3server.version import __version__ def test_version_output(server): - response = server.get('/version', example=True) - assert response.status == 200 - assert response.json == {'version': __version__} + gns_config = MagicMock() + config = ConfigParser() + config.add_section("Server") + config.set("Server", "local", "true") + gns_config.get_section_config.return_value = config["Server"] + + with patch("gns3server.config.Config.instance", return_value=gns_config): + response = server.get('/version', example=True) + assert response.status == 200 + assert response.json == {'local': True, 'version': __version__} def test_version_input(server): From cf247a9301183a3502700c9682bfe17bdd587c8f Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 16 Mar 2015 11:52:22 +0100 Subject: [PATCH 446/485] Fix iou tests and add tests --- gns3server/modules/iou/iou_vm.py | 44 +++++++++++++----------- tests/modules/iou/test_iou_vm.py | 58 ++++++++++++++++++++++++++++++-- 2 files changed, 81 insertions(+), 21 deletions(-) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 0ccbf376..e1c7bc77 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -46,6 +46,7 @@ import gns3server.utils.asyncio import logging +import sys log = logging.getLogger(__name__) @@ -368,25 +369,30 @@ class IOUVM(BaseVM): if len(user_ioukey) != 17: raise IOUError("IOU key length is not 16 characters in iourc file".format(self.iourc_path)) user_ioukey = user_ioukey[:16] - try: - hostid = (yield from gns3server.utils.asyncio.subprocess_check_output("hostid")).strip() - except FileNotFoundError as e: - raise IOUError("Could not find hostid: {}".format(e)) - except subprocess.SubprocessError as e: - raise IOUError("Could not execute hostid: {}".format(e)) - try: - ioukey = int(hostid, 16) - except ValueError: - raise IOUError("Invalid hostid detected: {}".format(hostid)) - for x in hostname: - ioukey += ord(x) - pad1 = b'\x4B\x58\x21\x81\x56\x7B\x0D\xF3\x21\x43\x9B\x7E\xAC\x1D\xE6\x8A' - pad2 = b'\x80' + 39 * b'\0' - ioukey = hashlib.md5(pad1 + pad2 + struct.pack('!i', ioukey) + pad1).hexdigest()[:16] - if ioukey != user_ioukey: - raise IOUError("Invalid IOU license key {} detected in iourc file {} for host {}".format(user_ioukey, - self.iourc_path, - hostname)) + + # We can't test this because it's mean distributing a valid licence key + # in tests or generating one + if not sys._called_from_test: + try: + hostid = (yield from gns3server.utils.asyncio.subprocess_check_output("hostid")).strip() + except FileNotFoundError as e: + raise IOUError("Could not find hostid: {}".format(e)) + except subprocess.SubprocessError as e: + raise IOUError("Could not execute hostid: {}".format(e)) + + try: + ioukey = int(hostid, 16) + except ValueError: + raise IOUError("Invalid hostid detected: {}".format(hostid)) + for x in hostname: + ioukey += ord(x) + pad1 = b'\x4B\x58\x21\x81\x56\x7B\x0D\xF3\x21\x43\x9B\x7E\xAC\x1D\xE6\x8A' + pad2 = b'\x80' + 39 * b'\0' + ioukey = hashlib.md5(pad1 + pad2 + struct.pack('!i', ioukey) + pad1).hexdigest()[:16] + if ioukey != user_ioukey: + raise IOUError("Invalid IOU license key {} detected in iourc file {} for host {}".format(user_ioukey, + self.iourc_path, + hostname)) @asyncio.coroutine def start(self): diff --git a/tests/modules/iou/test_iou_vm.py b/tests/modules/iou/test_iou_vm.py index a6ab2ab6..42fa4c70 100644 --- a/tests/modules/iou/test_iou_vm.py +++ b/tests/modules/iou/test_iou_vm.py @@ -20,6 +20,7 @@ import aiohttp import asyncio import os import stat +import socket from tests.utils import asyncio_patch @@ -37,7 +38,7 @@ def manager(port_manager): @pytest.fixture(scope="function") -def vm(project, manager, tmpdir, fake_iou_bin): +def vm(project, manager, tmpdir, fake_iou_bin, iourc_file): fake_file = str(tmpdir / "iouyap") with open(fake_file, "w+") as f: f.write("1") @@ -45,17 +46,28 @@ def vm(project, manager, tmpdir, fake_iou_bin): vm = IOUVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) config = manager.config.get_section_config("IOU") config["iouyap_path"] = fake_file + config["iourc_path"] = iourc_file manager.config.set_section_config("IOU", config) vm.path = fake_iou_bin return vm +@pytest.fixture +def iourc_file(tmpdir): + path = str(tmpdir / "iourc") + with open(path, "w+") as f: + hostname = socket.gethostname() + f.write("[license]\n{} = aaaaaaaaaaaaaaaa;".format(hostname)) + return path + + @pytest.fixture def fake_iou_bin(tmpdir): """Create a fake IOU image on disk""" - path = str(tmpdir / "iou.bin") + os.makedirs(str(tmpdir / "IOU"), exist_ok=True) + path = str(tmpdir / "IOU" / "iou.bin") with open(path, "w+") as f: f.write('\x7fELF\x01\x01\x01') os.chmod(path, stat.S_IREAD | stat.S_IEXEC) @@ -313,3 +325,45 @@ def test_stop_capture(vm, tmpdir, manager, free_console_port, loop): def test_get_legacy_vm_workdir(): assert IOU.get_legacy_vm_workdir(42, "bla") == "iou/device-42" + + +def test_invalid_iou_file(loop, vm, iourc_file): + + hostname = socket.gethostname() + + loop.run_until_complete(asyncio.async(vm._check_iou_licence())) + + # Missing ; + with pytest.raises(IOUError): + with open(iourc_file, "w+") as f: + f.write("[license]\n{} = aaaaaaaaaaaaaaaa".format(hostname)) + loop.run_until_complete(asyncio.async(vm._check_iou_licence())) + + # Key too short + with pytest.raises(IOUError): + with open(iourc_file, "w+") as f: + f.write("[license]\n{} = aaaaaaaaaaaaaa;".format(hostname)) + loop.run_until_complete(asyncio.async(vm._check_iou_licence())) + + # Invalid hostname + with pytest.raises(IOUError): + with open(iourc_file, "w+") as f: + f.write("[license]\nbla = aaaaaaaaaaaaaa;") + loop.run_until_complete(asyncio.async(vm._check_iou_licence())) + + # Missing licence section + with pytest.raises(IOUError): + with open(iourc_file, "w+") as f: + f.write("[licensetest]\n{} = aaaaaaaaaaaaaaaa;") + loop.run_until_complete(asyncio.async(vm._check_iou_licence())) + + # Broken config file + with pytest.raises(IOUError): + with open(iourc_file, "w+") as f: + f.write("[") + loop.run_until_complete(asyncio.async(vm._check_iou_licence())) + + # Missing file + with pytest.raises(IOUError): + os.remove(iourc_file) + loop.run_until_complete(asyncio.async(vm._check_iou_licence())) From c05edfe4152df8f8712f4d7a556d4cd2c854e7a4 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 16 Mar 2015 12:08:23 +0100 Subject: [PATCH 447/485] Fix test manager --- tests/modules/test_manager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/modules/test_manager.py b/tests/modules/test_manager.py index 81a2ddf0..a9d32daf 100644 --- a/tests/modules/test_manager.py +++ b/tests/modules/test_manager.py @@ -65,6 +65,7 @@ def test_create_vm_old_topology(loop, project, tmpdir, port_manager): project_dir = str(tmpdir / "testold") vm_dir = os.path.join(project_dir, "testold-files", "vpcs", "pc-1") project.path = project_dir + project.name = "testold" os.makedirs(vm_dir, exist_ok=True) with open(os.path.join(vm_dir, "startup.vpc"), "w+") as f: f.write("1") From e54649accd00cd06537a60e177bf289b6d7699b5 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 16 Mar 2015 14:42:00 +0100 Subject: [PATCH 448/485] Fix dynamips tests --- tests/conftest.py | 1 + .../modules/dynamips/test_dynamips_router.py | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index b4599ce2..977fd13a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -133,6 +133,7 @@ def run_around_tests(monkeypatch): tmppath = tempfile.mkdtemp() + Config.reset() config = Config.instance() server_section = config.get_section_config("Server") server_section["project_directory"] = tmppath diff --git a/tests/modules/dynamips/test_dynamips_router.py b/tests/modules/dynamips/test_dynamips_router.py index de5fb99d..add442c0 100644 --- a/tests/modules/dynamips/test_dynamips_router.py +++ b/tests/modules/dynamips/test_dynamips_router.py @@ -23,6 +23,7 @@ from unittest.mock import patch from gns3server.modules.dynamips.nodes.router import Router from gns3server.modules.dynamips.dynamips_error import DynamipsError from gns3server.modules.dynamips import Dynamips +from gns3server.config import Config @pytest.fixture(scope="module") @@ -44,9 +45,15 @@ def test_router(project, manager): def test_router_invalid_dynamips_path(project, manager, loop): - with patch("gns3server.config.Config.get_section_config", return_value={"dynamips_path": "/bin/test_fake"}): - with pytest.raises(DynamipsError): - router = Router("test", "00010203-0405-0607-0809-0a0b0c0d0e0e", project, manager) - loop.run_until_complete(asyncio.async(router.create())) - assert router.name == "test" - assert router.id == "00010203-0405-0607-0809-0a0b0c0d0e0e" + + config = Config.instance() + dynamips_section = config.get_section_config("Dynamips") + dynamips_section["dynamips_path"] = "/bin/test_fake" + dynamips_section["allocate_aux_console_ports"] = "false" + config.set_section_config("Dynamips", dynamips_section) + + with pytest.raises(DynamipsError): + router = Router("test", "00010203-0405-0607-0809-0a0b0c0d0e0e", project, manager) + loop.run_until_complete(asyncio.async(router.create())) + assert router.name == "test" + assert router.id == "00010203-0405-0607-0809-0a0b0c0d0e0e" From bcb1ce02abb64c8f7f2bf368a76ec6d553e644b1 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 16 Mar 2015 15:03:41 +0100 Subject: [PATCH 449/485] Refactor config management in tests --- gns3server/config.py | 12 ++++++++++++ tests/conftest.py | 13 +++---------- tests/handlers/api/test_config.py | 17 ++++++----------- tests/handlers/api/test_version.py | 15 +++++---------- tests/modules/dynamips/test_dynamips_router.py | 6 ++---- tests/test_config.py | 15 ++++++++++++++- 6 files changed, 42 insertions(+), 36 deletions(-) diff --git a/gns3server/config.py b/gns3server/config.py index 7a91d530..8f3c86a1 100644 --- a/gns3server/config.py +++ b/gns3server/config.py @@ -151,9 +151,21 @@ class Config(object): if not self._config.has_section(section): self._config.add_section(section) for key in content: + if isinstance(content[key], bool): + content[key] = str(content[key]).lower() self._config.set(section, key, content[key]) self._override_config[section] = content + def set(self, section, key, value): + """ + Set a config value. + It's not dumped on the disk. + + If the section doesn't exists the section is created + """ + + self.set_section_config(section, {key: value}) + @staticmethod def instance(files=None): """ diff --git a/tests/conftest.py b/tests/conftest.py index 977fd13a..bb856412 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -135,18 +135,11 @@ def run_around_tests(monkeypatch): Config.reset() config = Config.instance() - server_section = config.get_section_config("Server") - server_section["project_directory"] = tmppath - config.set_section_config("Server", server_section) + config.set("Server", "project_directory", tmppath) # Prevent exectuions of the VM if we forgot to mock something - vbox_section = config.get_section_config("VirtualBox") - vbox_section["vboxmanage_path"] = tmppath - config.set_section_config("VirtualBox", vbox_section) - - vbox_section = config.get_section_config("VPCS") - vbox_section["vpcs_path"] = tmppath - config.set_section_config("VPCS", vbox_section) + config.set("VirtualBox", "vboxmanage_path", tmppath) + config.set("VPCS", "vpcs_path", tmppath) monkeypatch.setattr("gns3server.modules.project.Project._get_default_project_directory", lambda *args: tmppath) diff --git a/tests/handlers/api/test_config.py b/tests/handlers/api/test_config.py index b081ca13..542a1b72 100644 --- a/tests/handlers/api/test_config.py +++ b/tests/handlers/api/test_config.py @@ -15,17 +15,16 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from configparser import ConfigParser -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch +from gns3server.config import Config def test_reload_accepted(server): gns_config = MagicMock() - config = ConfigParser() - config.add_section("Server") + config = Config.instance() config.set("Server", "local", "true") - gns_config.get_section_config.return_value = config["Server"] + gns_config.get_section_config.return_value = config.get_section_config("Server") with patch("gns3server.config.Config.instance", return_value=gns_config): response = server.post('/config/reload', example=True) @@ -36,13 +35,9 @@ def test_reload_accepted(server): def test_reload_forbidden(server): - gns_config = MagicMock() - config = ConfigParser() - config.add_section("Server") + config = Config.instance() config.set("Server", "local", "false") - gns_config.get_section_config.return_value = config["Server"] - with patch("gns3server.config.Config.instance", return_value=gns_config): - response = server.post('/config/reload') + response = server.post('/config/reload') assert response.status == 403 diff --git a/tests/handlers/api/test_version.py b/tests/handlers/api/test_version.py index 1af2ac89..29cdebae 100644 --- a/tests/handlers/api/test_version.py +++ b/tests/handlers/api/test_version.py @@ -20,23 +20,18 @@ This test suite check /version endpoint It's also used for unittest the HTTP implementation. """ -from unittest.mock import patch, MagicMock -from configparser import ConfigParser +from gns3server.config import Config from gns3server.version import __version__ def test_version_output(server): - gns_config = MagicMock() - config = ConfigParser() - config.add_section("Server") + config = Config.instance() config.set("Server", "local", "true") - gns_config.get_section_config.return_value = config["Server"] - with patch("gns3server.config.Config.instance", return_value=gns_config): - response = server.get('/version', example=True) - assert response.status == 200 - assert response.json == {'local': True, 'version': __version__} + response = server.get('/version', example=True) + assert response.status == 200 + assert response.json == {'local': True, 'version': __version__} def test_version_input(server): diff --git a/tests/modules/dynamips/test_dynamips_router.py b/tests/modules/dynamips/test_dynamips_router.py index add442c0..f32b84ba 100644 --- a/tests/modules/dynamips/test_dynamips_router.py +++ b/tests/modules/dynamips/test_dynamips_router.py @@ -47,10 +47,8 @@ def test_router(project, manager): def test_router_invalid_dynamips_path(project, manager, loop): config = Config.instance() - dynamips_section = config.get_section_config("Dynamips") - dynamips_section["dynamips_path"] = "/bin/test_fake" - dynamips_section["allocate_aux_console_ports"] = "false" - config.set_section_config("Dynamips", dynamips_section) + config.set("Dynamips", "dynamips_path", "/bin/test_fake") + config.set("Dynamips", "allocate_aux_console_ports", False) with pytest.raises(DynamipsError): router = Router("test", "00010203-0405-0607-0809-0a0b0c0d0e0e", project, manager) diff --git a/tests/test_config.py b/tests/test_config.py index 3f119619..a1a0c33e 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -67,13 +67,26 @@ def test_get_section_config(tmpdir): def test_set_section_config(tmpdir): + config = load_config(tmpdir, { + "Server": { + "host": "127.0.0.1", + "local": "false" + } + }) + assert dict(config.get_section_config("Server")) == {"host": "127.0.0.1", "local": "false"} + config.set_section_config("Server", {"host": "192.168.1.1", "local": True}) + assert dict(config.get_section_config("Server")) == {"host": "192.168.1.1", "local": "true"} + + +def test_set(tmpdir): + config = load_config(tmpdir, { "Server": { "host": "127.0.0.1" } }) assert dict(config.get_section_config("Server")) == {"host": "127.0.0.1"} - config.set_section_config("Server", {"host": "192.168.1.1"}) + config.set("Server", "host", "192.168.1.1") assert dict(config.get_section_config("Server")) == {"host": "192.168.1.1"} From cc9b575b775bd6006a7adfcb41c2e83147df50a3 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 16 Mar 2015 12:45:21 -0600 Subject: [PATCH 450/485] Bind UDP tunnels to the correct source address. Fixes #96. --- gns3server/handlers/api/qemu_handler.py | 7 ++----- gns3server/modules/qemu/qemu_vm.py | 11 ++++------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/gns3server/handlers/api/qemu_handler.py b/gns3server/handlers/api/qemu_handler.py index 20f72e33..659d1b03 100644 --- a/gns3server/handlers/api/qemu_handler.py +++ b/gns3server/handlers/api/qemu_handler.py @@ -15,9 +15,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import os - - from ...web.route import Route from ...schemas.qemu import QEMU_CREATE_SCHEMA from ...schemas.qemu import QEMU_UPDATE_SCHEMA @@ -55,8 +52,8 @@ class QEMUHandler: request.json.get("vm_id"), qemu_path=request.json.get("qemu_path"), console=request.json.get("console"), - monitor=request.json.get("monitor"), - ) + monitor=request.json.get("monitor")) + # Clear already used keys map(request.json.__delitem__, ["name", "project_id", "vm_id", "qemu_path", "console", "monitor"]) diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index 2c34e8a6..e595110f 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -49,11 +49,9 @@ class QemuVM(BaseVM): :param manager: parent VM Manager :param console: TCP console port :param qemu_path: path to the QEMU binary - :param host: host/address to bind for console and UDP connections :param qemu_id: QEMU VM instance ID :param console: TCP console port :param monitor: TCP monitor port - :param monitor_host: IP address to bind for monitor connections """ def __init__(self, @@ -62,20 +60,19 @@ class QemuVM(BaseVM): project, manager, qemu_path=None, - host="127.0.0.1", console=None, - monitor=None, - monitor_host="127.0.0.1"): + monitor=None): super().__init__(name, vm_id, project, manager, console=console) - self._host = host + server_config = manager.config.get_section_config("Server") + self._host = server_config.get("host", "127.0.0.1") + self._monitor_host = server_config.get("monitor_host", "127.0.0.1") self._command = [] self._started = False self._process = None self._cpulimit_process = None self._stdout_file = "" - self._monitor_host = monitor_host # QEMU settings self._qemu_path = qemu_path From 46fe973a9609e795aea50faabcf50cdcf2312052 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 16 Mar 2015 16:33:37 -0600 Subject: [PATCH 451/485] Save IOS router configs when the user saves a project. --- gns3server/modules/base_manager.py | 10 ++++++++++ gns3server/modules/dynamips/__init__.py | 13 +++++++++++++ gns3server/modules/project.py | 2 ++ 3 files changed, 25 insertions(+) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 23a4e34d..c41463a7 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -282,6 +282,16 @@ class BaseManager: pass + @asyncio.coroutine + def project_committed(self, project): + """ + Called when a project is committed. + + :param project: Project instance + """ + + pass + @asyncio.coroutine def delete_vm(self, vm_id): """ diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index 6a5f4c70..b8053c3e 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -199,6 +199,19 @@ class Dynamips(BaseManager): if device.project.id == project.id: yield from device.hypervisor.set_working_dir(project.module_working_directory(self.module_name.lower())) + @asyncio.coroutine + def project_committed(self, project): + """ + Called when a project has been committed. + + :param project: Project instance + """ + + # save the configs when the project is committed + for vm in self._vms.values(): + if vm.project.id == project.id: + yield from vm.save_configs() + @property def dynamips_path(self): """ diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index 8d1d449d..4fc14b9b 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -324,6 +324,8 @@ class Project: vm = self._vms_to_destroy.pop() yield from vm.delete() self.remove_vm(vm) + for module in self.modules(): + yield from module.instance().project_committed(self) @asyncio.coroutine def delete(self): From 87d12452f9f662d621ca238088d102793c38421e Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 16 Mar 2015 16:35:02 -0600 Subject: [PATCH 452/485] Disable the netifaces dependency which creates issues. --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 7d26539f..23aed54e 100644 --- a/setup.py +++ b/setup.py @@ -39,8 +39,8 @@ dependencies = ["aiohttp==0.14.4", "Jinja2==2.7.3", "raven==5.2.0"] -if not sys.platform.startswith("win"): - dependencies.append("netifaces==0.10.4") +#if not sys.platform.startswith("win"): +# dependencies.append("netifaces==0.10.4") if sys.version_info == (3, 3): dependencies.append("asyncio==3.4.2") From 93a5f4be796f30e13d32638f5e3c5dddfbbd138c Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 16 Mar 2015 17:36:23 -0600 Subject: [PATCH 453/485] Temporarily deactivate IOS router saveconfigs. --- gns3server/modules/dynamips/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index b8053c3e..181347df 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -207,10 +207,11 @@ class Dynamips(BaseManager): :param project: Project instance """ + pass # save the configs when the project is committed - for vm in self._vms.values(): - if vm.project.id == project.id: - yield from vm.save_configs() + #for vm in self._vms.values(): + # if vm.project.id == project.id: + # yield from vm.save_configs() @property def dynamips_path(self): From 2de817214ff88de365964869b9d5efa8db762790 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 16 Mar 2015 19:16:15 -0600 Subject: [PATCH 454/485] Do not hide non-executable file in the UploadHandler. --- gns3server/handlers/upload_handler.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gns3server/handlers/upload_handler.py b/gns3server/handlers/upload_handler.py index 7fef01b5..a3995f5a 100644 --- a/gns3server/handlers/upload_handler.py +++ b/gns3server/handlers/upload_handler.py @@ -37,8 +37,7 @@ class UploadHandler: for root, _, files in os.walk(UploadHandler.image_directory()): for filename in files: image_file = os.path.join(root, filename) - if os.access(image_file, os.X_OK): - uploaded_files.append(image_file) + uploaded_files.append(image_file) except OSError: pass iourc_path = os.path.join(os.path.expanduser("~/"), ".iourc") From 54bccb0628f1e925ec5519334aec443902f8c470 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 17 Mar 2015 10:21:52 +0100 Subject: [PATCH 455/485] Restore configuration live reload Closes #94 --- gns3server/config.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/gns3server/config.py b/gns3server/config.py index 8f3c86a1..46f5a3ae 100644 --- a/gns3server/config.py +++ b/gns3server/config.py @@ -92,6 +92,27 @@ class Config(object): self._config = configparser.ConfigParser() self.read_config() + self._watch_config_file() + + def _watch_config_file(self): + asyncio.get_event_loop().call_later(1, self._check_config_file_change) + + def _check_config_file_change(self): + """ + Check if configuration file has changed on the disk + """ + changed = False + for file in self._watched_files: + try: + if os.stat(file).st_mtime != self._watched_files[file]: + changed = True + except OSError: + continue + if changed: + self.read_config() + for section in self._override_config: + self.set_section_config(section, self._override_config[section]) + self._watch_config_file() def reload(self): """ From bb7eda63afea9f15451f4ef529f45467a9edc895 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 17 Mar 2015 11:02:14 +0100 Subject: [PATCH 456/485] Support more all QEMU status Fix #98 --- gns3server/modules/qemu/qemu_vm.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index e595110f..92616d8d 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -710,10 +710,18 @@ class QemuVM(BaseVM): """ Returns this VM suspend status (running|paused) + Status are extracted from: + https://github.com/qemu/qemu/blob/master/qapi-schema.json#L152 + :returns: status (string) """ - result = yield from self._control_vm("info status", [b"running", b"paused"]) + result = yield from self._control_vm("info status", [ + b"debug", b"inmigrate", b"internal-error", b"io-error", + b"paused", b"postmigrate", b"prelaunch", b"finish-migrate", + b"restore-vm", b"running", b"save-vm", b"shutdown", b"suspended", + b"watchdog", b"guest-panicked" + ]) return result.rsplit(' ', 1)[1] @asyncio.coroutine From dc1c12b7d012dffae3cf25e2c370174e77de2750 Mon Sep 17 00:00:00 2001 From: Akash Agrawall Date: Tue, 17 Mar 2015 20:04:20 +0530 Subject: [PATCH 457/485] Modified README --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 7cf681e0..48bc18da 100644 --- a/README.rst +++ b/README.rst @@ -8,6 +8,8 @@ Clients like the GNS3 GUI controls the server using a JSON-RPC API over Websocke You will need the GNS3 GUI (gns3-gui repository) to control the server. +**NOTE** Checkout the asyncio branch of the repository. It will soon be released in the next release. + Linux (Debian based) -------------------- From 964ea0f577cd449ea3091d140ba1eb720cf1b6d6 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 17 Mar 2015 15:40:58 +0100 Subject: [PATCH 458/485] Fix random behavior in tests --- gns3server/config.py | 19 ++++++++++++++----- tests/conftest.py | 2 +- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/gns3server/config.py b/gns3server/config.py index 46f5a3ae..9bcc9038 100644 --- a/gns3server/config.py +++ b/gns3server/config.py @@ -45,9 +45,6 @@ class Config(object): # Monitor configuration files for changes self._watched_files = {} - # Override config from command line even if we modify the config file and live reload it. - self._override_config = {} - if sys.platform.startswith("win"): appname = "GNS3" @@ -90,9 +87,16 @@ class Config(object): os.path.join("/etc/xdg", appname + ".conf"), filename] + self.clear() + self._watch_config_file() + + def clear(self): + """Restart with a clean config""" self._config = configparser.ConfigParser() + # Override config from command line even if we modify the config file and live reload it. + self._override_config = {} + self.read_config() - self._watch_config_file() def _watch_config_file(self): asyncio.get_event_loop().call_later(1, self._check_config_file_change) @@ -185,7 +189,12 @@ class Config(object): If the section doesn't exists the section is created """ - self.set_section_config(section, {key: value}) + conf = self.get_section_config(section) + if isinstance(value, bool): + conf[key] = str(value) + else: + conf[key] = value + self.set_section_config(section, conf) @staticmethod def instance(files=None): diff --git a/tests/conftest.py b/tests/conftest.py index bb856412..378773b5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -133,8 +133,8 @@ def run_around_tests(monkeypatch): tmppath = tempfile.mkdtemp() - Config.reset() config = Config.instance() + config.clear() config.set("Server", "project_directory", tmppath) # Prevent exectuions of the VM if we forgot to mock something From 66cdf39ea27620a6cf5062cb2257c3b150df811a Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 17 Mar 2015 16:31:45 +0100 Subject: [PATCH 459/485] Support uploading iourc --- gns3server/handlers/api/iou_handler.py | 3 +- gns3server/modules/base_vm.py | 11 +++++++ gns3server/modules/iou/iou_vm.py | 23 +++++++++++-- gns3server/schemas/iou.py | 8 +++++ tests/handlers/api/test_iou.py | 3 ++ tests/modules/iou/test_iou_vm.py | 8 +++++ tests/modules/test_base_vm.py | 45 ++++++++++++++++++++++++++ 7 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 tests/modules/test_base_vm.py diff --git a/gns3server/handlers/api/iou_handler.py b/gns3server/handlers/api/iou_handler.py index 5769a992..5e076c4e 100644 --- a/gns3server/handlers/api/iou_handler.py +++ b/gns3server/handlers/api/iou_handler.py @@ -61,7 +61,8 @@ class IOUHandler: ram=request.json.get("ram"), nvram=request.json.get("nvram"), l1_keepalives=request.json.get("l1_keepalives"), - initial_config=request.json.get("initial_config_content") + initial_config=request.json.get("initial_config_content"), + iourc_content=request.json.get("iourc_content") ) vm.path = request.json.get("path", vm.path) response.set_status(201) diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index 67db308a..cfbcc2a2 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -20,6 +20,7 @@ import logging import aiohttp import shutil import asyncio +import tempfile from ..utils.asyncio import wait_run_in_executor @@ -45,6 +46,7 @@ class BaseVM: self._project = project self._manager = manager self._console = console + self._temporary_directory = None if self._console is not None: self._console = self._manager.port_manager.reserve_tcp_port(self._console) @@ -61,6 +63,9 @@ class BaseVM: def __del__(self): self.close() + if self._temporary_directory is not None: + if os.path.exists(self._temporary_directory): + shutil.rmtree(self._temporary_directory) @property def project(self): @@ -124,6 +129,12 @@ class BaseVM: return self._project.vm_working_directory(self) + @property + def temporary_directory(self): + if self._temporary_directory is None: + self._temporary_directory = tempfile.mkdtemp() + return self._temporary_directory + def create(self): """ Creates the VM. diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index e1c7bc77..cb5f917d 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -67,6 +67,7 @@ class IOUVM(BaseVM): :params nvram: Nvram KB :params l1_keepalives: Always up ethernet interface: :params initial_config: Content of the initial configuration file + :params iourc_content: Content of the iourc file if no licence is installed on server """ def __init__(self, name, vm_id, project, manager, @@ -76,7 +77,8 @@ class IOUVM(BaseVM): ethernet_adapters=None, serial_adapters=None, l1_keepalives=None, - initial_config=None): + initial_config=None, + iourc_content=None): super().__init__(name, vm_id, project, manager, console=console) @@ -99,6 +101,7 @@ class IOUVM(BaseVM): self._ram = 256 if ram is None else ram # Megabytes self._l1_keepalives = False if l1_keepalives is None else l1_keepalives # used to overcome the always-up Ethernet interfaces (not supported by all IOSes). + self.iourc_content = iourc_content if initial_config is not None: self.initial_config = initial_config @@ -209,7 +212,8 @@ class IOUVM(BaseVM): "nvram": self._nvram, "l1_keepalives": self._l1_keepalives, "initial_config": self.relative_initial_config_file, - "use_default_iou_values": self._use_default_iou_values} + "use_default_iou_values": self._use_default_iou_values, + "iourc_path": self.iourc_path} # return the relative path if the IOU image is in the images_path directory server_config = self.manager.config.get_section_config("Server") @@ -250,6 +254,10 @@ class IOUVM(BaseVM): path = os.path.join(self.working_dir, "iourc") if os.path.exists(path): return path + # look for the iourc file in the temporary dir. + path = os.path.join(self.temporary_directory, "iourc") + if os.path.exists(path): + return path return iourc_path @property @@ -322,6 +330,17 @@ class IOUVM(BaseVM): def application_id(self): return self._manager.get_application_id(self.id) + @property + def iourc_content(self): + with open(os.path.join(self.temporary_directory, "iourc")) as f: + return f.read() + + @iourc_content.setter + def iourc_content(self, value): + if value is not None: + with open(os.path.join(self.temporary_directory, "iourc"), "w+") as f: + f.write(value) + @asyncio.coroutine def _library_check(self): """ diff --git a/gns3server/schemas/iou.py b/gns3server/schemas/iou.py index df04039a..83f8fd7f 100644 --- a/gns3server/schemas/iou.py +++ b/gns3server/schemas/iou.py @@ -73,6 +73,10 @@ IOU_CREATE_SCHEMA = { "initial_config_content": { "description": "Initial configuration of the IOU", "type": ["string", "null"] + }, + "iourc_content": { + "description": "Content of the iourc file, if a file exist on servers this variable is ignored. It's mostly for compatibility with < 1.3 releases", + "type": ["string", "null"] } }, "additionalProperties": False, @@ -192,6 +196,10 @@ IOU_OBJECT_SCHEMA = { "use_default_iou_values": { "description": "Use default IOU values", "type": ["boolean", "null"] + }, + "iourc_path": { + "description": "Path of the iourc file used by remote servers", + "type": ["string", "null"] } }, "additionalProperties": False, diff --git a/tests/handlers/api/test_iou.py b/tests/handlers/api/test_iou.py index ecf07f78..bae3076d 100644 --- a/tests/handlers/api/test_iou.py +++ b/tests/handlers/api/test_iou.py @@ -75,6 +75,7 @@ def test_iou_create_with_params(server, project, base_params): params["l1_keepalives"] = True params["initial_config_content"] = "hostname test" params["use_default_iou_values"] = True + params["iourc_content"] = "test" response = server.post("/projects/{project_id}/iou/vms".format(project_id=project.id), params, example=True) assert response.status == 201 @@ -92,6 +93,8 @@ def test_iou_create_with_params(server, project, base_params): with open(initial_config_file(project, response.json)) as f: assert f.read() == params["initial_config_content"] + assert "iourc" in response.json["iourc_path"] + def test_iou_get(server, project, vm): response = server.get("/projects/{project_id}/iou/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) diff --git a/tests/modules/iou/test_iou_vm.py b/tests/modules/iou/test_iou_vm.py index 42fa4c70..7bbaf769 100644 --- a/tests/modules/iou/test_iou_vm.py +++ b/tests/modules/iou/test_iou_vm.py @@ -367,3 +367,11 @@ def test_invalid_iou_file(loop, vm, iourc_file): with pytest.raises(IOUError): os.remove(iourc_file) loop.run_until_complete(asyncio.async(vm._check_iou_licence())) + + +def test_iourc_content(vm): + + vm.iourc_content = "test" + + with open(os.path.join(vm.temporary_directory, "iourc")) as f: + assert f.read() == "test" diff --git a/tests/modules/test_base_vm.py b/tests/modules/test_base_vm.py new file mode 100644 index 00000000..f12844f9 --- /dev/null +++ b/tests/modules/test_base_vm.py @@ -0,0 +1,45 @@ +# -*- 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 pytest +import aiohttp +import asyncio +import os +from tests.utils import asyncio_patch + + +from unittest.mock import patch, MagicMock +from gns3server.modules.vpcs.vpcs_vm import VPCSVM +from gns3server.modules.vpcs.vpcs_error import VPCSError +from gns3server.modules.vpcs import VPCS + + +@pytest.fixture(scope="module") +def manager(port_manager): + m = VPCS.instance() + m.port_manager = port_manager + return m + + +@pytest.fixture(scope="function") +def vm(project, manager): + return VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + + +def test_temporary_directory(project, manager): + vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + assert isinstance(vm.temporary_directory, str) From 42f51ddc002ff65ee83772979c60eac24db087ef Mon Sep 17 00:00:00 2001 From: Vivek Jain Date: Tue, 17 Mar 2015 21:10:28 +0530 Subject: [PATCH 460/485] Modify README.rst to specify how to run tests. --- README.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.rst b/README.rst index 48bc18da..25968688 100644 --- a/README.rst +++ b/README.rst @@ -41,6 +41,12 @@ Finally these commands will install the server as well as the rest of the depend sudo python3 setup.py install gns3server +To run tests use: + +.. code:: bash + + py.test -v + Windows ------- From 8ca9c2121a1b14665ef2b9c787ecf36957575ba1 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 17 Mar 2015 18:34:23 +0100 Subject: [PATCH 461/485] Do not crash if iourc file is missing --- gns3server/modules/iou/iou_vm.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index cb5f917d..7323b9d7 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -370,6 +370,8 @@ class IOUVM(BaseVM): return config = configparser.ConfigParser() + if self.iourc_path is None: + raise IOUError("Could not found iourc file") try: with open(self.iourc_path) as f: config.read_file(f) From 4a9f5787840bf14353b085b83c3267cf61c56336 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 17 Mar 2015 19:00:14 +0100 Subject: [PATCH 462/485] Support IOURC update --- gns3server/handlers/api/iou_handler.py | 1 + gns3server/modules/iou/iou_vm.py | 2 +- gns3server/schemas/iou.py | 4 ++++ tests/handlers/api/test_iou.py | 5 ++++- 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/gns3server/handlers/api/iou_handler.py b/gns3server/handlers/api/iou_handler.py index 5e076c4e..edebae1b 100644 --- a/gns3server/handlers/api/iou_handler.py +++ b/gns3server/handlers/api/iou_handler.py @@ -117,6 +117,7 @@ class IOUHandler: vm.nvram = request.json.get("nvram", vm.nvram) vm.l1_keepalives = request.json.get("l1_keepalives", vm.l1_keepalives) vm.initial_config = request.json.get("initial_config_content", vm.initial_config) + vm.iourc_content = request.json.get("iourc_content", None) response.json(vm) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 7323b9d7..68a880f6 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -393,7 +393,7 @@ class IOUVM(BaseVM): # We can't test this because it's mean distributing a valid licence key # in tests or generating one - if not sys._called_from_test: + if not hasattr(sys, "_called_from_test"): try: hostid = (yield from gns3server.utils.asyncio.subprocess_check_output("hostid")).strip() except FileNotFoundError as e: diff --git a/gns3server/schemas/iou.py b/gns3server/schemas/iou.py index 83f8fd7f..5793488a 100644 --- a/gns3server/schemas/iou.py +++ b/gns3server/schemas/iou.py @@ -130,6 +130,10 @@ IOU_UPDATE_SCHEMA = { "use_default_iou_values": { "description": "Use default IOU values", "type": ["boolean", "null"] + }, + "iourc_content": { + "description": "Content of the iourc file, if a file exist on servers this variable is ignored. It's mostly for compatibility with < 1.3 releases", + "type": ["string", "null"] } }, "additionalProperties": False, diff --git a/tests/handlers/api/test_iou.py b/tests/handlers/api/test_iou.py index bae3076d..bc0f8da9 100644 --- a/tests/handlers/api/test_iou.py +++ b/tests/handlers/api/test_iou.py @@ -147,7 +147,8 @@ def test_iou_update(server, vm, tmpdir, free_console_port, project): "serial_adapters": 0, "l1_keepalives": True, "initial_config_content": "hostname test", - "use_default_iou_values": True + "use_default_iou_values": True, + "iourc_content": "test" } response = server.put("/projects/{project_id}/iou/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), params, example=True) assert response.status == 200 @@ -163,6 +164,8 @@ def test_iou_update(server, vm, tmpdir, free_console_port, project): with open(initial_config_file(project, response.json)) as f: assert f.read() == "hostname test" + assert "iourc" in response.json["iourc_path"] + def test_iou_nio_create_udp(server, vm): response = server.post("/projects/{project_id}/iou/vms/{vm_id}/adapters/1/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", From 386b311755871dd876a136966f9ca7d0693d695f Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 17 Mar 2015 20:15:01 +0100 Subject: [PATCH 463/485] Fix iou key verification for large hostid --- gns3server/modules/iou/iou_vm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 68a880f6..f73aa2a2 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -409,7 +409,7 @@ class IOUVM(BaseVM): ioukey += ord(x) pad1 = b'\x4B\x58\x21\x81\x56\x7B\x0D\xF3\x21\x43\x9B\x7E\xAC\x1D\xE6\x8A' pad2 = b'\x80' + 39 * b'\0' - ioukey = hashlib.md5(pad1 + pad2 + struct.pack('!i', ioukey) + pad1).hexdigest()[:16] + ioukey = hashlib.md5(pad1 + pad2 + struct.pack('!I', ioukey) + pad1).hexdigest()[:16] if ioukey != user_ioukey: raise IOUError("Invalid IOU license key {} detected in iourc file {} for host {}".format(user_ioukey, self.iourc_path, From 6330e99ff18449ff9f82798e1cf0a41c1e06884b Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 17 Mar 2015 22:18:55 +0100 Subject: [PATCH 464/485] More robust IOUVM support --- gns3server/modules/base_vm.py | 6 +++++- gns3server/modules/iou/iou_vm.py | 15 +++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index cfbcc2a2..4d315167 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -23,6 +23,7 @@ import asyncio import tempfile from ..utils.asyncio import wait_run_in_executor +from .vm_error import VMError log = logging.getLogger(__name__) @@ -132,7 +133,10 @@ class BaseVM: @property def temporary_directory(self): if self._temporary_directory is None: - self._temporary_directory = tempfile.mkdtemp() + try: + self._temporary_directory = tempfile.mkdtemp() + except OSError as e: + raise VMError("Can't create temporary directory: {}".format(e)) return self._temporary_directory def create(self): diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index f73aa2a2..f195c2ed 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -332,14 +332,21 @@ class IOUVM(BaseVM): @property def iourc_content(self): - with open(os.path.join(self.temporary_directory, "iourc")) as f: - return f.read() + try: + with open(os.path.join(self.temporary_directory, "iourc")) as f: + return f.read() + except OSError: + return None @iourc_content.setter def iourc_content(self, value): if value is not None: - with open(os.path.join(self.temporary_directory, "iourc"), "w+") as f: - f.write(value) + path = os.path.join(self.temporary_directory, "iourc") + try: + with open(path, "w+") as f: + f.write(value) + except OSError as e: + raise IOUError("Could not write iourc file {}: {}".format(path, e)) @asyncio.coroutine def _library_check(self): From 163d1e375d8db3babed4b7d2f6e4101abf2448e2 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 17 Mar 2015 18:53:24 -0600 Subject: [PATCH 465/485] Save IOS configs when a project is committed. --- gns3server/modules/dynamips/__init__.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index 181347df..b36169e1 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -115,6 +115,7 @@ class Dynamips(BaseManager): self._devices = {} self._ghost_files = set() self._dynamips_path = None + self._project_lock = asyncio.Lock() @asyncio.coroutine def unload(self): @@ -191,13 +192,14 @@ class Dynamips(BaseManager): :param project: Project instance """ - for vm in self._vms.values(): - if vm.project.id == project.id: - yield from vm.hypervisor.set_working_dir(project.module_working_directory(self.module_name.lower())) + with (yield from self._project_lock): + for vm in self._vms.values(): + if vm.project.id == project.id: + yield from vm.hypervisor.set_working_dir(project.module_working_directory(self.module_name.lower())) - for device in self._devices.values(): - if device.project.id == project.id: - yield from device.hypervisor.set_working_dir(project.module_working_directory(self.module_name.lower())) + for device in self._devices.values(): + if device.project.id == project.id: + yield from device.hypervisor.set_working_dir(project.module_working_directory(self.module_name.lower())) @asyncio.coroutine def project_committed(self, project): @@ -207,11 +209,11 @@ class Dynamips(BaseManager): :param project: Project instance """ - pass # save the configs when the project is committed - #for vm in self._vms.values(): - # if vm.project.id == project.id: - # yield from vm.save_configs() + with (yield from self._project_lock): + for vm in self._vms.values(): + if vm.project.id == project.id: + yield from vm.save_configs() @property def dynamips_path(self): From 44c8396997a7dff80b3be73f630e098002692d45 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 17 Mar 2015 19:08:18 -0600 Subject: [PATCH 466/485] Bump version to 1.3.0rc1.dev2 --- gns3server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/version.py b/gns3server/version.py index 89518e96..308e8842 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,5 +23,5 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "1.3.0rc1.dev1" +__version__ = "1.3.0rc1.dev2" __version_info__ = (1, 3, 0, -99) From f6b122cdfac31efa1dfd2c8fd508597552bd0ff7 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 17 Mar 2015 19:28:43 -0600 Subject: [PATCH 467/485] Look in legacy IOU images dir when looking for relative IOU image path. --- gns3server/modules/iou/iou_vm.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index f195c2ed..37d955ad 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -139,7 +139,9 @@ class IOUVM(BaseVM): if not os.path.isabs(path): server_config = self.manager.config.get_section_config("Server") - path = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), "IOU", path) + path = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), path) + if not os.path.exists(path): + path = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), "IOU", path) self._path = path if not os.path.isfile(self._path) or not os.path.exists(self._path): From 8415117d2dd219f3523e77a4fdb639cbc53f8093 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 18 Mar 2015 15:34:31 -0600 Subject: [PATCH 468/485] Save IOS router configs when saving the project (done right this time). --- .../handlers/api/dynamips_vm_handler.py | 21 ++++++++++++-- gns3server/modules/base_manager.py | 10 ------- gns3server/modules/dynamips/__init__.py | 28 ++++--------------- gns3server/modules/project.py | 2 -- 4 files changed, 24 insertions(+), 37 deletions(-) diff --git a/gns3server/handlers/api/dynamips_vm_handler.py b/gns3server/handlers/api/dynamips_vm_handler.py index dd5d5720..1aea6a7b 100644 --- a/gns3server/handlers/api/dynamips_vm_handler.py +++ b/gns3server/handlers/api/dynamips_vm_handler.py @@ -15,10 +15,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - import os import base64 -import asyncio from ...web.route import Route from ...schemas.dynamips_vm import VM_CREATE_SCHEMA @@ -341,7 +339,7 @@ class DynamipsVMHandler: }, output=VM_CONFIGS_SCHEMA, description="Retrieve the startup and private configs content") - def show_initial_config(request, response): + def get_configs(request, response): dynamips_manager = Dynamips.instance() vm = dynamips_manager.get_vm(request.match_info["vm_id"], @@ -355,6 +353,23 @@ class DynamipsVMHandler: response.json({"startup_config_content": startup_config_content, "private_config_content": private_config_content}) + @Route.post( + r"/projects/{project_id}/dynamips/vms/{vm_id}/configs/save", + status_codes={ + 200: "Configs saved", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Save the startup and private configs content") + def save_configs(request, response): + + dynamips_manager = Dynamips.instance() + vm = dynamips_manager.get_vm(request.match_info["vm_id"], + project_id=request.match_info["project_id"]) + + yield from vm.save_configs() + response.set_status(200) + @Route.get( r"/projects/{project_id}/dynamips/vms/{vm_id}/idlepc_proposals", status_codes={ diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index c41463a7..23a4e34d 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -282,16 +282,6 @@ class BaseManager: pass - @asyncio.coroutine - def project_committed(self, project): - """ - Called when a project is committed. - - :param project: Project instance - """ - - pass - @asyncio.coroutine def delete_vm(self, vm_id): """ diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index b36169e1..6a5f4c70 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -115,7 +115,6 @@ class Dynamips(BaseManager): self._devices = {} self._ghost_files = set() self._dynamips_path = None - self._project_lock = asyncio.Lock() @asyncio.coroutine def unload(self): @@ -192,28 +191,13 @@ class Dynamips(BaseManager): :param project: Project instance """ - with (yield from self._project_lock): - for vm in self._vms.values(): - if vm.project.id == project.id: - yield from vm.hypervisor.set_working_dir(project.module_working_directory(self.module_name.lower())) + for vm in self._vms.values(): + if vm.project.id == project.id: + yield from vm.hypervisor.set_working_dir(project.module_working_directory(self.module_name.lower())) - for device in self._devices.values(): - if device.project.id == project.id: - yield from device.hypervisor.set_working_dir(project.module_working_directory(self.module_name.lower())) - - @asyncio.coroutine - def project_committed(self, project): - """ - Called when a project has been committed. - - :param project: Project instance - """ - - # save the configs when the project is committed - with (yield from self._project_lock): - for vm in self._vms.values(): - if vm.project.id == project.id: - yield from vm.save_configs() + for device in self._devices.values(): + if device.project.id == project.id: + yield from device.hypervisor.set_working_dir(project.module_working_directory(self.module_name.lower())) @property def dynamips_path(self): diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index 4fc14b9b..8d1d449d 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -324,8 +324,6 @@ class Project: vm = self._vms_to_destroy.pop() yield from vm.delete() self.remove_vm(vm) - for module in self.modules(): - yield from module.instance().project_committed(self) @asyncio.coroutine def delete(self): From f31071d51020a26a44a82e97ea034cfd7de8a3f7 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 18 Mar 2015 15:40:02 -0600 Subject: [PATCH 469/485] Bump version to 1.3.0rc1.dev3 --- gns3server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/version.py b/gns3server/version.py index 308e8842..1f5fb897 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,5 +23,5 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "1.3.0rc1.dev2" +__version__ = "1.3.0rc1.dev3" __version_info__ = (1, 3, 0, -99) From ddb8a9f06e84e59f0b60f927cf3d88543b93b1eb Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 19 Mar 2015 15:36:06 +0100 Subject: [PATCH 470/485] Fix an issue in IOU relative path looking --- gns3server/modules/iou/iou_vm.py | 7 ++++--- tests/modules/iou/test_iou_vm.py | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 37d955ad..ef08ec49 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -139,9 +139,10 @@ class IOUVM(BaseVM): if not os.path.isabs(path): server_config = self.manager.config.get_section_config("Server") - path = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), path) - if not os.path.exists(path): - path = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), "IOU", path) + relative_path = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), path) + if not os.path.exists(relative_path): + relative_path = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), "IOU", path) + path = relative_path self._path = path if not os.path.isfile(self._path) or not os.path.exists(self._path): diff --git a/tests/modules/iou/test_iou_vm.py b/tests/modules/iou/test_iou_vm.py index 7bbaf769..68a183ac 100644 --- a/tests/modules/iou/test_iou_vm.py +++ b/tests/modules/iou/test_iou_vm.py @@ -28,7 +28,7 @@ from unittest.mock import patch, MagicMock from gns3server.modules.iou.iou_vm import IOUVM from gns3server.modules.iou.iou_error import IOUError from gns3server.modules.iou import IOU - +from gns3server.config import Config @pytest.fixture(scope="module") def manager(port_manager): @@ -196,8 +196,9 @@ def test_path(vm, fake_iou_bin): def test_path_relative(vm, fake_iou_bin, tmpdir): - with patch("gns3server.config.Config.get_section_config", return_value={"images_path": str(tmpdir)}): - vm.path = "iou.bin" + config = Config.instance() + config.set("Server", "images_path", str(tmpdir)) + vm.path = "iou.bin" assert vm.path == fake_iou_bin From 253ab4e2b5761574f269bb56d524c5eb50c30c70 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 19 Mar 2015 17:42:43 +0100 Subject: [PATCH 471/485] PEP8 --- tests/modules/iou/test_iou_vm.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/modules/iou/test_iou_vm.py b/tests/modules/iou/test_iou_vm.py index 68a183ac..272541cb 100644 --- a/tests/modules/iou/test_iou_vm.py +++ b/tests/modules/iou/test_iou_vm.py @@ -30,6 +30,7 @@ from gns3server.modules.iou.iou_error import IOUError from gns3server.modules.iou import IOU from gns3server.config import Config + @pytest.fixture(scope="module") def manager(port_manager): m = IOU.instance() From 533baf0445a3c7557b134cb6f207e12a24a08c68 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 19 Mar 2015 17:46:03 +0100 Subject: [PATCH 472/485] 1.3.0rc1 --- CHANGELOG | 9 +++++++++ gns3server/version.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index a80989ce..345a8693 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,14 @@ # Change Log +## 1.3.0rc1 19/03/2015 + +* Save IOS router config when saving the project +* Look in legacy IOU images directory +* Support IOURC upload +* Configuration on UNIX +* Support all QEMU status +* Bind tunnel UDP to the correct source index + ## 1.3.0beta2 13/03/2015 * Fixed issue when VBoxManage returns an error. diff --git a/gns3server/version.py b/gns3server/version.py index 1f5fb897..40c39b3f 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,5 +23,5 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "1.3.0rc1.dev3" +__version__ = "1.3.0rc1" __version_info__ = (1, 3, 0, -99) From 7473dec5ad9d528d681ee02ca5ebddceb0e1090f Mon Sep 17 00:00:00 2001 From: grossmj Date: Thu, 19 Mar 2015 19:56:31 -0600 Subject: [PATCH 473/485] Bump version to 1.3.0.dev1 --- README.rst | 2 -- gns3server/version.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/README.rst b/README.rst index b451f207..e1c04e6b 100644 --- a/README.rst +++ b/README.rst @@ -14,8 +14,6 @@ Clients like the GNS3 GUI controls the server using a HTTP REST API. You will need the GNS3 GUI (gns3-gui repository) to control the server. -**NOTE** Checkout the asyncio branch of the repository. It will soon be released in the next release. - Linux (Debian based) -------------------- diff --git a/gns3server/version.py b/gns3server/version.py index 40c39b3f..a9b6e119 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,5 +23,5 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "1.3.0rc1" +__version__ = "1.3.0.dev1" __version_info__ = (1, 3, 0, -99) From 01bcbe2fd9ef7707061008e18424e93c6518a3cc Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 20 Mar 2015 10:21:27 +0100 Subject: [PATCH 474/485] Lock the dynamips reader an writer Fix #103 --- .../modules/dynamips/dynamips_hypervisor.py | 130 +++++++++--------- 1 file changed, 68 insertions(+), 62 deletions(-) diff --git a/gns3server/modules/dynamips/dynamips_hypervisor.py b/gns3server/modules/dynamips/dynamips_hypervisor.py index 7c22f011..8a0d296f 100644 --- a/gns3server/modules/dynamips/dynamips_hypervisor.py +++ b/gns3server/modules/dynamips/dynamips_hypervisor.py @@ -59,6 +59,8 @@ class DynamipsHypervisor: self._reader = None self._writer = None + self._io_lock = asyncio.Lock() + @asyncio.coroutine def connect(self, timeout=10): """ @@ -80,7 +82,8 @@ class DynamipsHypervisor: while time.time() - begin < timeout: yield from asyncio.sleep(0.01) try: - self._reader, self._writer = yield from asyncio.open_connection(host, self._port) + with (yield from self._io_lock): + self._reader, self._writer = yield from asyncio.open_connection(host, self._port) except OSError as e: last_exception = e continue @@ -120,8 +123,9 @@ class DynamipsHypervisor: """ yield from self.send("hypervisor close") - self._writer.close() - self._reader, self._writer = None + with (yield from self._io_lock): + self._writer.close() + self._reader, self._writer = None @asyncio.coroutine def stop(self): @@ -129,17 +133,18 @@ class DynamipsHypervisor: Stops this hypervisor (will no longer run). """ - try: - # try to properly stop the hypervisor - yield from self.send("hypervisor stop") - except DynamipsError: - pass - try: - yield from self._writer.drain() - self._writer.close() - except OSError as e: - log.debug("Stopping hypervisor {}:{} {}".format(self._host, self._port, e)) - self._reader = self._writer = None + with (yield from self._io_lock): + try: + # try to properly stop the hypervisor + yield from self.send("hypervisor stop") + except DynamipsError: + pass + try: + yield from self._writer.drain() + self._writer.close() + except OSError as e: + log.debug("Stopping hypervisor {}:{} {}".format(self._host, self._port, e)) + self._reader = self._writer = None @asyncio.coroutine def reset(self): @@ -255,59 +260,60 @@ class DynamipsHypervisor: # but still have more data. The only thing we know for sure is the last line # will begin with '100-' or a '2xx-' and end with '\r\n' - if self._writer is None or self._reader is None: - raise DynamipsError("Not connected") + with (yield from self._io_lock): + if self._writer is None or self._reader is None: + raise DynamipsError("Not connected") - try: - command = command.strip() + '\n' - log.debug("sending {}".format(command)) - self._writer.write(command.encode()) - except OSError as e: - raise DynamipsError("Lost communication with {host}:{port} :{error}, Dynamips process running: {run}" - .format(host=self._host, port=self._port, error=e, run=self.is_running())) - - # Now retrieve the result - data = [] - buf = '' - while True: try: - chunk = yield from self._reader.read(1024) # match to Dynamips' buffer size - if not chunk: - raise DynamipsError("No data returned from {host}:{port}, Dynamips process running: {run}" - .format(host=self._host, port=self._port, run=self.is_running())) - buf += chunk.decode() + command = command.strip() + '\n' + log.debug("sending {}".format(command)) + self._writer.write(command.encode()) except OSError as e: - raise DynamipsError("Communication timed out with {host}:{port} :{error}, Dynamips process running: {run}" + raise DynamipsError("Lost communication with {host}:{port} :{error}, Dynamips process running: {run}" .format(host=self._host, port=self._port, error=e, run=self.is_running())) - # If the buffer doesn't end in '\n' then we can't be done - try: - if buf[-1] != '\n': - continue - except IndexError: - raise DynamipsError("Could not communicate with {host}:{port}, Dynamips process running: {run}" - .format(host=self._host, port=self._port, run=self.is_running())) - - data += buf.split('\r\n') - if data[-1] == '': - data.pop() + # Now retrieve the result + data = [] buf = '' + while True: + try: + chunk = yield from self._reader.read(1024) # match to Dynamips' buffer size + if not chunk: + raise DynamipsError("No data returned from {host}:{port}, Dynamips process running: {run}" + .format(host=self._host, port=self._port, run=self.is_running())) + buf += chunk.decode() + except OSError as e: + raise DynamipsError("Communication timed out with {host}:{port} :{error}, Dynamips process running: {run}" + .format(host=self._host, port=self._port, error=e, run=self.is_running())) + + # If the buffer doesn't end in '\n' then we can't be done + try: + if buf[-1] != '\n': + continue + except IndexError: + raise DynamipsError("Could not communicate with {host}:{port}, Dynamips process running: {run}" + .format(host=self._host, port=self._port, run=self.is_running())) - # Does it contain an error code? - if self.error_re.search(data[-1]): - raise DynamipsError(data[-1][4:]) - - # Or does the last line begin with '100-'? Then we are done! - if data[-1][:4] == '100-': - data[-1] = data[-1][4:] - if data[-1] == 'OK': + data += buf.split('\r\n') + if data[-1] == '': data.pop() - break - - # Remove success responses codes - for index in range(len(data)): - if self.success_re.search(data[index]): - data[index] = data[index][4:] - - log.debug("returned result {}".format(data)) - return data + buf = '' + + # Does it contain an error code? + if self.error_re.search(data[-1]): + raise DynamipsError(data[-1][4:]) + + # Or does the last line begin with '100-'? Then we are done! + if data[-1][:4] == '100-': + data[-1] = data[-1][4:] + if data[-1] == 'OK': + data.pop() + break + + # Remove success responses codes + for index in range(len(data)): + if self.success_re.search(data[index]): + data[index] = data[index][4:] + + log.debug("returned result {}".format(data)) + return data From 17d5b3a7bc68594e5c5a8f8be5d68b140605ff8d Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 20 Mar 2015 19:19:49 -0600 Subject: [PATCH 475/485] Remove unnecessary locks. --- .../modules/dynamips/dynamips_hypervisor.py | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/gns3server/modules/dynamips/dynamips_hypervisor.py b/gns3server/modules/dynamips/dynamips_hypervisor.py index 8a0d296f..2b0401d7 100644 --- a/gns3server/modules/dynamips/dynamips_hypervisor.py +++ b/gns3server/modules/dynamips/dynamips_hypervisor.py @@ -58,7 +58,6 @@ class DynamipsHypervisor: self._uuid = None self._reader = None self._writer = None - self._io_lock = asyncio.Lock() @asyncio.coroutine @@ -82,8 +81,7 @@ class DynamipsHypervisor: while time.time() - begin < timeout: yield from asyncio.sleep(0.01) try: - with (yield from self._io_lock): - self._reader, self._writer = yield from asyncio.open_connection(host, self._port) + self._reader, self._writer = yield from asyncio.open_connection(host, self._port) except OSError as e: last_exception = e continue @@ -123,9 +121,8 @@ class DynamipsHypervisor: """ yield from self.send("hypervisor close") - with (yield from self._io_lock): - self._writer.close() - self._reader, self._writer = None + self._writer.close() + self._reader, self._writer = None @asyncio.coroutine def stop(self): @@ -133,18 +130,17 @@ class DynamipsHypervisor: Stops this hypervisor (will no longer run). """ - with (yield from self._io_lock): - try: - # try to properly stop the hypervisor - yield from self.send("hypervisor stop") - except DynamipsError: - pass - try: - yield from self._writer.drain() - self._writer.close() - except OSError as e: - log.debug("Stopping hypervisor {}:{} {}".format(self._host, self._port, e)) - self._reader = self._writer = None + try: + # try to properly stop the hypervisor + yield from self.send("hypervisor stop") + except DynamipsError: + pass + try: + yield from self._writer.drain() + self._writer.close() + except OSError as e: + log.debug("Stopping hypervisor {}:{} {}".format(self._host, self._port, e)) + self._reader = self._writer = None @asyncio.coroutine def reset(self): @@ -283,7 +279,7 @@ class DynamipsHypervisor: .format(host=self._host, port=self._port, run=self.is_running())) buf += chunk.decode() except OSError as e: - raise DynamipsError("Communication timed out with {host}:{port} :{error}, Dynamips process running: {run}" + raise DynamipsError("Lost communication with {host}:{port} :{error}, Dynamips process running: {run}" .format(host=self._host, port=self._port, error=e, run=self.is_running())) # If the buffer doesn't end in '\n' then we can't be done From 628dfef0d36a2082e627beeea4d7bef711ee194d Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 21 Mar 2015 13:58:52 -0600 Subject: [PATCH 476/485] Initialize chassis when creating an IOS router. Fixes #107. --- gns3server/handlers/api/dynamips_vm_handler.py | 3 ++- gns3server/modules/dynamips/nodes/c2691.py | 6 +++++- gns3server/modules/dynamips/nodes/c3725.py | 6 +++++- gns3server/modules/dynamips/nodes/c3745.py | 6 +++++- gns3server/modules/dynamips/nodes/c7200.py | 7 +++++-- 5 files changed, 22 insertions(+), 6 deletions(-) diff --git a/gns3server/handlers/api/dynamips_vm_handler.py b/gns3server/handlers/api/dynamips_vm_handler.py index 1aea6a7b..a52159e6 100644 --- a/gns3server/handlers/api/dynamips_vm_handler.py +++ b/gns3server/handlers/api/dynamips_vm_handler.py @@ -58,7 +58,8 @@ class DynamipsVMHandler: request.json.get("dynamips_id"), request.json.pop("platform"), console=request.json.get("console"), - aux=request.json.get("aux")) + aux=request.json.get("aux"), + chassis=request.json.pop("chassis", None)) yield from dynamips_manager.update_vm_settings(vm, request.json) yield from dynamips_manager.ghost_ios_support(vm) diff --git a/gns3server/modules/dynamips/nodes/c2691.py b/gns3server/modules/dynamips/nodes/c2691.py index d161c90e..bafca2e0 100644 --- a/gns3server/modules/dynamips/nodes/c2691.py +++ b/gns3server/modules/dynamips/nodes/c2691.py @@ -23,6 +23,7 @@ http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L387 import asyncio from .router import Router from ..adapters.gt96100_fe import GT96100_FE +from ..dynamips_error import DynamipsError import logging log = logging.getLogger(__name__) @@ -42,7 +43,7 @@ class C2691(Router): :param aux: auxiliary console port """ - def __init__(self, name, vm_id, project, manager, dynamips_id, console=None, aux=None): + def __init__(self, name, vm_id, project, manager, dynamips_id, console=None, aux=None, chassis=None): Router.__init__(self, name, vm_id, project, manager, dynamips_id, console, aux, platform="c2691") # Set default values for this platform (must be the same as Dynamips) @@ -56,6 +57,9 @@ class C2691(Router): self._create_slots(2) self._slots[0] = GT96100_FE() + if chassis is not None: + raise DynamipsError("c2691 routers do not have chassis") + def __json__(self): c2691_router_info = {"iomem": self._iomem} diff --git a/gns3server/modules/dynamips/nodes/c3725.py b/gns3server/modules/dynamips/nodes/c3725.py index 41c3b19c..69bab887 100644 --- a/gns3server/modules/dynamips/nodes/c3725.py +++ b/gns3server/modules/dynamips/nodes/c3725.py @@ -23,6 +23,7 @@ http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L346 import asyncio from .router import Router from ..adapters.gt96100_fe import GT96100_FE +from ..dynamips_error import DynamipsError import logging log = logging.getLogger(__name__) @@ -42,7 +43,7 @@ class C3725(Router): :param aux: auxiliary console port """ - def __init__(self, name, vm_id, project, manager, dynamips_id, console=None, aux=None): + def __init__(self, name, vm_id, project, manager, dynamips_id, console=None, aux=None, chassis=None): Router.__init__(self, name, vm_id, project, manager, dynamips_id, console, aux, platform="c3725") # Set default values for this platform (must be the same as Dynamips) @@ -56,6 +57,9 @@ class C3725(Router): self._create_slots(3) self._slots[0] = GT96100_FE() + if chassis is not None: + raise DynamipsError("c3725 routers do not have chassis") + def __json__(self): c3725_router_info = {"iomem": self._iomem} diff --git a/gns3server/modules/dynamips/nodes/c3745.py b/gns3server/modules/dynamips/nodes/c3745.py index 62a4f267..53e4b1e3 100644 --- a/gns3server/modules/dynamips/nodes/c3745.py +++ b/gns3server/modules/dynamips/nodes/c3745.py @@ -23,6 +23,7 @@ http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L326 import asyncio from .router import Router from ..adapters.gt96100_fe import GT96100_FE +from ..dynamips_error import DynamipsError import logging log = logging.getLogger(__name__) @@ -42,7 +43,7 @@ class C3745(Router): :param aux: auxiliary console port """ - def __init__(self, name, vm_id, project, manager, dynamips_id, console=None, aux=None): + def __init__(self, name, vm_id, project, manager, dynamips_id, console=None, aux=None, chassis=None): Router.__init__(self, name, vm_id, project, manager, dynamips_id, console, aux, platform="c3745") # Set default values for this platform (must be the same as Dynamips) @@ -56,6 +57,9 @@ class C3745(Router): self._create_slots(5) self._slots[0] = GT96100_FE() + if chassis is not None: + raise DynamipsError("c3745 routers do not have chassis") + def __json__(self): c3745_router_info = {"iomem": self._iomem} diff --git a/gns3server/modules/dynamips/nodes/c7200.py b/gns3server/modules/dynamips/nodes/c7200.py index 784bb241..07e2ff4e 100644 --- a/gns3server/modules/dynamips/nodes/c7200.py +++ b/gns3server/modules/dynamips/nodes/c7200.py @@ -22,10 +22,10 @@ http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L294 import asyncio -from ..dynamips_error import DynamipsError from .router import Router from ..adapters.c7200_io_fe import C7200_IO_FE from ..adapters.c7200_io_ge_e import C7200_IO_GE_E +from ..dynamips_error import DynamipsError import logging log = logging.getLogger(__name__) @@ -46,7 +46,7 @@ class C7200(Router): :param npe: Default NPE """ - def __init__(self, name, vm_id, project, manager, dynamips_id, console=None, aux=None, npe="npe-400"): + def __init__(self, name, vm_id, project, manager, dynamips_id, console=None, aux=None, npe="npe-400", chassis=None): Router.__init__(self, name, vm_id, project, manager, dynamips_id, console, aux, platform="c7200") # Set default values for this platform (must be the same as Dynamips) @@ -71,6 +71,9 @@ class C7200(Router): self._create_slots(7) + if chassis is not None: + raise DynamipsError("c7200 routers do not have chassis") + def __json__(self): c7200_router_info = {"npe": self._npe, From 2d6d153262ccb5d0cbaedfa88980c4566a5e4785 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 21 Mar 2015 14:52:17 -0600 Subject: [PATCH 477/485] Save configs when project is committed. --- gns3server/modules/base_manager.py | 10 ++++++++++ gns3server/modules/dynamips/__init__.py | 13 +++++++++++++ gns3server/modules/project.py | 2 ++ 3 files changed, 25 insertions(+) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 23a4e34d..c41463a7 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -282,6 +282,16 @@ class BaseManager: pass + @asyncio.coroutine + def project_committed(self, project): + """ + Called when a project is committed. + + :param project: Project instance + """ + + pass + @asyncio.coroutine def delete_vm(self, vm_id): """ diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index 6a5f4c70..b8053c3e 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -199,6 +199,19 @@ class Dynamips(BaseManager): if device.project.id == project.id: yield from device.hypervisor.set_working_dir(project.module_working_directory(self.module_name.lower())) + @asyncio.coroutine + def project_committed(self, project): + """ + Called when a project has been committed. + + :param project: Project instance + """ + + # save the configs when the project is committed + for vm in self._vms.values(): + if vm.project.id == project.id: + yield from vm.save_configs() + @property def dynamips_path(self): """ diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index 8d1d449d..4fc14b9b 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -324,6 +324,8 @@ class Project: vm = self._vms_to_destroy.pop() yield from vm.delete() self.remove_vm(vm) + for module in self.modules(): + yield from module.instance().project_committed(self) @asyncio.coroutine def delete(self): From 153914bf9712d267d13769c4a6bee2dd86a1cdce Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 21 Mar 2015 17:19:12 -0600 Subject: [PATCH 478/485] Make sure used ports in a project are cleaned up when closing it. --- gns3server/modules/base_vm.py | 8 +-- .../modules/dynamips/nodes/atm_switch.py | 4 +- .../modules/dynamips/nodes/ethernet_hub.py | 4 +- .../modules/dynamips/nodes/ethernet_switch.py | 4 +- .../dynamips/nodes/frame_relay_switch.py | 4 +- gns3server/modules/dynamips/nodes/router.py | 22 +++---- gns3server/modules/iou/iou_vm.py | 6 +- gns3server/modules/port_manager.py | 26 +++++++-- gns3server/modules/project.py | 57 +++++++++++++++++-- gns3server/modules/qemu/qemu_vm.py | 14 ++--- .../modules/virtualbox/virtualbox_vm.py | 6 +- gns3server/modules/vpcs/vpcs_vm.py | 6 +- tests/conftest.py | 6 +- tests/modules/iou/test_iou_vm.py | 2 +- tests/modules/qemu/test_qemu_vm.py | 4 +- tests/modules/test_port_manager.py | 7 ++- tests/modules/vpcs/test_vpcs_vm.py | 12 ++-- 17 files changed, 127 insertions(+), 65 deletions(-) diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index 4d315167..1dac9d02 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -50,9 +50,9 @@ class BaseVM: self._temporary_directory = None if self._console is not None: - self._console = self._manager.port_manager.reserve_tcp_port(self._console) + self._console = self._manager.port_manager.reserve_tcp_port(self._console, self._project) else: - self._console = self._manager.port_manager.get_free_tcp_port() + self._console = self._manager.port_manager.get_free_tcp_port(self._project) log.debug("{module}: {name} [{id}] initialized. Console port {console}".format( module=self.manager.module_name, @@ -203,8 +203,8 @@ class BaseVM: if console == self._console: return if self._console: - self._manager.port_manager.release_tcp_port(self._console) - self._console = self._manager.port_manager.reserve_tcp_port(console) + self._manager.port_manager.release_tcp_port(self._console, self._project) + self._console = self._manager.port_manager.reserve_tcp_port(console, self._project) log.info("{module}: '{name}' [{id}]: console port set to {port}".format( module=self.manager.module_name, name=self.name, diff --git a/gns3server/modules/dynamips/nodes/atm_switch.py b/gns3server/modules/dynamips/nodes/atm_switch.py index 7c1aa495..4064ad07 100644 --- a/gns3server/modules/dynamips/nodes/atm_switch.py +++ b/gns3server/modules/dynamips/nodes/atm_switch.py @@ -109,7 +109,7 @@ class ATMSwitch(Device): for nio in self._nios.values(): if nio and isinstance(nio, NIOUDP): - self.manager.port_manager.release_udp_port(nio.lport) + self.manager.port_manager.release_udp_port(nio.lport, self._project) try: yield from self._hypervisor.send('atmsw delete "{}"'.format(self._name)) @@ -162,7 +162,7 @@ class ATMSwitch(Device): nio = self._nios[port_number] if isinstance(nio, NIOUDP): - self.manager.port_manager.release_udp_port(nio.lport) + self.manager.port_manager.release_udp_port(nio.lport, self._project) log.info('ATM switch "{name}" [{id}]: NIO {nio} removed from port {port}'.format(name=self._name, id=self._id, nio=nio, diff --git a/gns3server/modules/dynamips/nodes/ethernet_hub.py b/gns3server/modules/dynamips/nodes/ethernet_hub.py index d41e5117..92cfccab 100644 --- a/gns3server/modules/dynamips/nodes/ethernet_hub.py +++ b/gns3server/modules/dynamips/nodes/ethernet_hub.py @@ -76,7 +76,7 @@ class EthernetHub(Bridge): for nio in self._nios: if nio and isinstance(nio, NIOUDP): - self.manager.port_manager.release_udp_port(nio.lport) + self.manager.port_manager.release_udp_port(nio.lport, self._project) try: yield from Bridge.delete(self) @@ -121,7 +121,7 @@ class EthernetHub(Bridge): nio = self._mappings[port_number] if isinstance(nio, NIOUDP): - self.manager.port_manager.release_udp_port(nio.lport) + self.manager.port_manager.release_udp_port(nio.lport, self._project) yield from Bridge.remove_nio(self, nio) log.info('Ethernet switch "{name}" [{id}]: NIO {nio} removed from port {port}'.format(name=self._name, diff --git a/gns3server/modules/dynamips/nodes/ethernet_switch.py b/gns3server/modules/dynamips/nodes/ethernet_switch.py index a748a9b3..fc74e09c 100644 --- a/gns3server/modules/dynamips/nodes/ethernet_switch.py +++ b/gns3server/modules/dynamips/nodes/ethernet_switch.py @@ -117,7 +117,7 @@ class EthernetSwitch(Device): for nio in self._nios.values(): if nio and isinstance(nio, NIOUDP): - self.manager.port_manager.release_udp_port(nio.lport) + self.manager.port_manager.release_udp_port(nio.lport, self._project) try: yield from self._hypervisor.send('ethsw delete "{}"'.format(self._name)) @@ -164,7 +164,7 @@ class EthernetSwitch(Device): nio = self._nios[port_number] if isinstance(nio, NIOUDP): - self.manager.port_manager.release_udp_port(nio.lport) + self.manager.port_manager.release_udp_port(nio.lport, self._project) yield from self._hypervisor.send('ethsw remove_nio "{name}" {nio}'.format(name=self._name, nio=nio)) log.info('Ethernet switch "{name}" [{id}]: NIO {nio} removed from port {port}'.format(name=self._name, diff --git a/gns3server/modules/dynamips/nodes/frame_relay_switch.py b/gns3server/modules/dynamips/nodes/frame_relay_switch.py index b46055b4..a4bf56e6 100644 --- a/gns3server/modules/dynamips/nodes/frame_relay_switch.py +++ b/gns3server/modules/dynamips/nodes/frame_relay_switch.py @@ -108,7 +108,7 @@ class FrameRelaySwitch(Device): for nio in self._nios.values(): if nio and isinstance(nio, NIOUDP): - self.manager.port_manager.release_udp_port(nio.lport) + self.manager.port_manager.release_udp_port(nio.lport, self._project) try: yield from self._hypervisor.send('frsw delete "{}"'.format(self._name)) @@ -163,7 +163,7 @@ class FrameRelaySwitch(Device): nio = self._nios[port_number] if isinstance(nio, NIOUDP): - self.manager.port_manager.release_udp_port(nio.lport) + self.manager.port_manager.release_udp_port(nio.lport, self._project) log.info('Frame Relay switch "{name}" [{id}]: NIO {nio} removed from port {port}'.format(name=self._name, id=self._id, diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index 63acce4d..9c219a97 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -109,16 +109,16 @@ class Router(BaseVM): self._dynamips_ids[project.id].append(self._dynamips_id) if self._aux is not None: - self._aux = self._manager.port_manager.reserve_tcp_port(self._aux) + self._aux = self._manager.port_manager.reserve_tcp_port(self._aux, self._project) else: allocate_aux = self.manager.config.get_section_config("Dynamips").getboolean("allocate_aux_console_ports", False) if allocate_aux: - self._aux = self._manager.port_manager.get_free_tcp_port() + self._aux = self._manager.port_manager.get_free_tcp_port(self._project) else: log.info("Creating a new ghost IOS instance") if self._console: # Ghost VMs do not need a console port. - self._manager.port_manager.release_tcp_port(self._console) + self._manager.port_manager.release_tcp_port(self._console, self._project) self._console = None self._dynamips_id = 0 self._name = "Ghost" @@ -326,18 +326,18 @@ class Router(BaseVM): self._dynamips_ids[self._project.id].remove(self._dynamips_id) if self._console: - self._manager.port_manager.release_tcp_port(self._console) + self._manager.port_manager.release_tcp_port(self._console, self._project) self._console = None if self._aux: - self._manager.port_manager.release_tcp_port(self._aux) + self._manager.port_manager.release_tcp_port(self._aux, self._project) self._aux = None for adapter in self._slots: if adapter is not None: for nio in adapter.ports.values(): if nio and isinstance(nio, NIOUDP): - self.manager.port_manager.release_udp_port(nio.lport) + self.manager.port_manager.release_udp_port(nio.lport, self._project) if self in self._hypervisor.devices: self._hypervisor.devices.remove(self) @@ -876,8 +876,8 @@ class Router(BaseVM): old_console=self._console, new_console=console)) - self._manager.port_manager.release_tcp_port(self._console) - self._console = self._manager.port_manager.reserve_tcp_port(console) + self._manager.port_manager.release_tcp_port(self._console, self._project) + self._console = self._manager.port_manager.reserve_tcp_port(console, self._project) @property def aux(self): @@ -904,8 +904,8 @@ class Router(BaseVM): old_aux=self._aux, new_aux=aux)) - self._manager.port_manager.release_tcp_port(self._aux) - self._aux = self._manager.port_manager.reserve_tcp_port(aux) + self._manager.port_manager.release_tcp_port(self._aux, self._project) + self._aux = self._manager.port_manager.reserve_tcp_port(aux, self._project) @asyncio.coroutine def get_cpu_usage(self, cpu_id=0): @@ -1228,7 +1228,7 @@ class Router(BaseVM): if nio is None: return if isinstance(nio, NIOUDP): - self.manager.port_manager.release_udp_port(nio.lport) + self.manager.port_manager.release_udp_port(nio.lport, self._project) adapter.remove_nio(port_number) log.info('Router "{name}" [{id}]: NIO {nio_name} removed from port {slot_number}/{port_number}'.format(name=self._name, diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index ef08ec49..0219c559 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -111,7 +111,7 @@ class IOUVM(BaseVM): log.debug('IOU "{name}" [{id}] is closing'.format(name=self._name, id=self._id)) if self._console: - self._manager.port_manager.release_tcp_port(self._console) + self._manager.port_manager.release_tcp_port(self._console, self._project) self._console = None adapters = self._ethernet_adapters + self._serial_adapters @@ -119,7 +119,7 @@ class IOUVM(BaseVM): if adapter is not None: for nio in adapter.ports.values(): if nio and isinstance(nio, NIOUDP): - self.manager.port_manager.release_udp_port(nio.lport) + self.manager.port_manager.release_udp_port(nio.lport, self._project) yield from self.stop() @@ -875,7 +875,7 @@ class IOUVM(BaseVM): nio = adapter.get_nio(port_number) if isinstance(nio, NIOUDP): - self.manager.port_manager.release_udp_port(nio.lport) + self.manager.port_manager.release_udp_port(nio.lport, self._project) adapter.remove_nio(port_number) log.info("IOU {name} [id={id}]: {nio} removed from {adapter_number}/{port_number}".format(name=self._name, id=self._id, diff --git a/gns3server/modules/port_manager.py b/gns3server/modules/port_manager.py index f38e2ea3..52d27933 100644 --- a/gns3server/modules/port_manager.py +++ b/gns3server/modules/port_manager.py @@ -173,9 +173,11 @@ class PortManager: host, last_exception)) - def get_free_tcp_port(self): + def get_free_tcp_port(self, project): """ Get an available TCP port and reserve it + + :param project: Project instance """ port = self.find_unused_port(self._console_port_range[0], @@ -185,36 +187,43 @@ class PortManager: ignore_ports=self._used_tcp_ports) self._used_tcp_ports.add(port) + project.record_tcp_port(port) log.debug("TCP port {} has been allocated".format(port)) return port - def reserve_tcp_port(self, port): + def reserve_tcp_port(self, port, project): """ Reserve a specific TCP port number :param port: TCP port number + :param project: Project instance """ if port in self._used_tcp_ports: raise HTTPConflict(text="TCP port {} already in use on host".format(port, self._console_host)) self._used_tcp_ports.add(port) + project.record_tcp_port(port) log.debug("TCP port {} has been reserved".format(port)) return port - def release_tcp_port(self, port): + def release_tcp_port(self, port, project): """ Release a specific TCP port number :param port: TCP port number + :param project: Project instance """ if port in self._used_tcp_ports: self._used_tcp_ports.remove(port) + project.remove_tcp_port(port) log.debug("TCP port {} has been released".format(port)) - def get_free_udp_port(self): + def get_free_udp_port(self, project): """ Get an available UDP port and reserve it + + :param project: Project instance """ port = self.find_unused_port(self._udp_port_range[0], @@ -224,28 +233,33 @@ class PortManager: ignore_ports=self._used_udp_ports) self._used_udp_ports.add(port) + project.record_udp_port(port) log.debug("UDP port {} has been allocated".format(port)) return port - def reserve_udp_port(self, port): + def reserve_udp_port(self, port, project): """ Reserve a specific UDP port number :param port: UDP port number + :param project: Project instance """ if port in self._used_udp_ports: raise HTTPConflict(text="UDP port {} already in use on host".format(port, self._console_host)) self._used_udp_ports.add(port) + project.record_udp_port(port) log.debug("UDP port {} has been reserved".format(port)) - def release_udp_port(self, port): + def release_udp_port(self, port, project): """ Release a specific UDP port number :param port: UDP port number + :param project: Project instance """ if port in self._used_udp_ports: self._used_udp_ports.remove(port) + project.remove_udp_port(port) log.debug("UDP port {} has been released".format(port)) diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index 4fc14b9b..f308fc12 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -62,6 +62,8 @@ class Project: self._vms = set() self._vms_to_destroy = set() self.temporary = temporary + self._used_tcp_ports = set() + self._used_udp_ports = set() if path is None: path = os.path.join(self._location, self._id) @@ -168,6 +170,46 @@ class Project: self._temporary = temporary self._update_temporary_file() + def record_tcp_port(self, port): + """ + Associate a reserved TCP port number with this project. + + :param port: TCP port number + """ + + if port not in self._used_tcp_ports: + self._used_tcp_ports.add(port) + + def record_udp_port(self, port): + """ + Associate a reserved UDP port number with this project. + + :param port: UDP port number + """ + + if port not in self._used_udp_ports: + self._used_udp_ports.add(port) + + def remove_tcp_port(self, port): + """ + Removes an associated TCP port number from this project. + + :param port: TCP port number + """ + + if port in self._used_tcp_ports: + self._used_tcp_ports.remove(port) + + def remove_udp_port(self, port): + """ + Removes an associated UDP port number from this project. + + :param port: UDP port number + """ + + if port in self._used_udp_ports: + self._used_udp_ports.remove(port) + def _update_temporary_file(self): """ Update the .gns3_temporary file in order to reflect current @@ -309,12 +351,17 @@ class Project: else: log.info("Project {id} with path '{path}' closed".format(path=self._path, id=self._id)) - port_manager = PortManager.instance() - if port_manager.tcp_ports: - log.debug("TCP ports still in use: {}".format(port_manager.tcp_ports)) + if self._used_tcp_ports: + log.warning("Project {} has TCP ports still in use: {}".format(self.id, self._used_tcp_ports)) + if self._used_udp_ports: + log.warning("Project {} has UDP ports still in use: {}".format(self.id, self._used_udp_ports)) - if port_manager.udp_ports: - log.debug("UDP ports still in use: {}".format(port_manager.udp_ports)) + # clean the remaining ports that have not been cleaned by their respective VM or device. + port_manager = PortManager.instance() + for port in self._used_tcp_ports.copy(): + port_manager.release_tcp_port(port, self) + for port in self._used_udp_ports.copy(): + port_manager.release_udp_port(port, self) @asyncio.coroutine def commit(self): diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index 92616d8d..ba5b098b 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -93,9 +93,9 @@ class QemuVM(BaseVM): self._process_priority = "low" if self._monitor is not None: - self._monitor = self._manager.port_manager.reserve_tcp_port(self._monitor) + self._monitor = self._manager.port_manager.reserve_tcp_port(self._monitor, self._project) else: - self._monitor = self._manager.port_manager.get_free_tcp_port() + self._monitor = self._manager.port_manager.get_free_tcp_port(self._project) self.adapters = 1 # creates 1 adapter by default log.info("QEMU VM {name} [id={id}] has been created".format(name=self._name, @@ -122,8 +122,8 @@ class QemuVM(BaseVM): if monitor == self._monitor: return if self._monitor: - self._manager.port_manager.release_monitor_port(self._monitor) - self._monitor = self._manager.port_manager.reserve_monitor_port(monitor) + self._manager.port_manager.release_monitor_port(self._monitor, self._project) + self._monitor = self._manager.port_manager.reserve_monitor_port(monitor, self._project) log.info("{module}: '{name}' [{id}]: monitor port set to {port}".format( module=self.manager.module_name, name=self.name, @@ -699,10 +699,10 @@ class QemuVM(BaseVM): log.debug('QEMU VM "{name}" [{id}] is closing'.format(name=self._name, id=self._id)) yield from self.stop() if self._console: - self._manager.port_manager.release_tcp_port(self._console) + self._manager.port_manager.release_tcp_port(self._console, self._project) self._console = None if self._monitor: - self._manager.port_manager.release_tcp_port(self._monitor) + self._manager.port_manager.release_tcp_port(self._monitor, self._project) self._monitor = None @asyncio.coroutine @@ -825,7 +825,7 @@ class QemuVM(BaseVM): nio = adapter.get_nio(0) if isinstance(nio, NIOUDP): - self.manager.port_manager.release_udp_port(nio.lport) + self.manager.port_manager.release_udp_port(nio.lport, self._project) adapter.remove_nio(0) log.info("QEMU VM {name} [id={id}]: {nio} removed from adapter {adapter_id}".format(name=self._name, id=self._id, diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 83dc86d3..6d610923 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -305,14 +305,14 @@ class VirtualBoxVM(BaseVM): log.debug("VirtualBox VM '{name}' [{id}] is closing".format(name=self.name, id=self.id)) if self._console: - self._manager.port_manager.release_tcp_port(self._console) + self._manager.port_manager.release_tcp_port(self._console, self._project) self._console = None for adapter in self._ethernet_adapters: if adapter is not None: for nio in adapter.ports.values(): if nio and isinstance(nio, NIOUDP): - self.manager.port_manager.release_udp_port(nio.lport) + self.manager.port_manager.release_udp_port(nio.lport, self._project) yield from self.stop() @@ -828,7 +828,7 @@ class VirtualBoxVM(BaseVM): nio = adapter.get_nio(0) if isinstance(nio, NIOUDP): - self.manager.port_manager.release_udp_port(nio.lport) + self.manager.port_manager.release_udp_port(nio.lport, self._project) adapter.remove_nio(0) log.info("VirtualBox VM '{name}' [{id}]: {nio} removed from adapter {adapter_number}".format(name=self.name, diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index dad5e87c..18a08077 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -74,12 +74,12 @@ class VPCSVM(BaseVM): log.debug("VPCS {name} [{id}] is closing".format(name=self._name, id=self._id)) if self._console: - self._manager.port_manager.release_tcp_port(self._console) + self._manager.port_manager.release_tcp_port(self._console, self._project) self._console = None nio = self._ethernet_adapter.get_nio(0) if isinstance(nio, NIOUDP): - self.manager.port_manager.release_udp_port(nio.lport) + self.manager.port_manager.release_udp_port(nio.lport, self._project) self._terminate_process() @@ -334,7 +334,7 @@ class VPCSVM(BaseVM): nio = self._ethernet_adapter.get_nio(port_number) if isinstance(nio, NIOUDP): - self.manager.port_manager.release_udp_port(nio.lport) + self.manager.port_manager.release_udp_port(nio.lport, self._project) self._ethernet_adapter.remove_nio(port_number) log.info("VPCS {name} [{id}]: {nio} removed from port {port_number}".format(name=self._name, diff --git a/tests/conftest.py b/tests/conftest.py index 378773b5..74aee0cb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -114,14 +114,14 @@ def port_manager(): @pytest.fixture(scope="function") -def free_console_port(request, port_manager): +def free_console_port(request, port_manager, project): """Get a free TCP port""" # In case of already use ports we will raise an exception - port = port_manager.get_free_tcp_port() + port = port_manager.get_free_tcp_port(project) # We release the port immediately in order to allow # the test do whatever the test want - port_manager.release_tcp_port(port) + port_manager.release_tcp_port(port, project) return port diff --git a/tests/modules/iou/test_iou_vm.py b/tests/modules/iou/test_iou_vm.py index 272541cb..42ddba80 100644 --- a/tests/modules/iou/test_iou_vm.py +++ b/tests/modules/iou/test_iou_vm.py @@ -185,7 +185,7 @@ def test_close(vm, port_manager, loop): port = vm.console loop.run_until_complete(asyncio.async(vm.close())) # Raise an exception if the port is not free - port_manager.reserve_tcp_port(port) + port_manager.reserve_tcp_port(port, vm.project) assert vm.is_running() is False diff --git a/tests/modules/qemu/test_qemu_vm.py b/tests/modules/qemu/test_qemu_vm.py index 537b4947..1e078c26 100644 --- a/tests/modules/qemu/test_qemu_vm.py +++ b/tests/modules/qemu/test_qemu_vm.py @@ -156,9 +156,9 @@ def test_close(vm, port_manager, loop): loop.run_until_complete(asyncio.async(vm.close())) # Raise an exception if the port is not free - port_manager.reserve_tcp_port(console_port) + port_manager.reserve_tcp_port(console_port, vm.project) # Raise an exception if the port is not free - port_manager.reserve_tcp_port(monitor_port) + port_manager.reserve_tcp_port(monitor_port, vm.project) assert vm.is_running() is False diff --git a/tests/modules/test_port_manager.py b/tests/modules/test_port_manager.py index 7b2f9193..2590b826 100644 --- a/tests/modules/test_port_manager.py +++ b/tests/modules/test_port_manager.py @@ -18,10 +18,11 @@ import aiohttp import pytest from gns3server.modules.port_manager import PortManager - +from gns3server.modules.project import Project def test_reserve_tcp_port(): pm = PortManager() - pm.reserve_tcp_port(4242) + project = Project() + pm.reserve_tcp_port(4242, project) with pytest.raises(aiohttp.web.HTTPConflict): - pm.reserve_tcp_port(4242) + pm.reserve_tcp_port(4242, project) diff --git a/tests/modules/vpcs/test_vpcs_vm.py b/tests/modules/vpcs/test_vpcs_vm.py index 0ac186f7..6b539a89 100644 --- a/tests/modules/vpcs/test_vpcs_vm.py +++ b/tests/modules/vpcs/test_vpcs_vm.py @@ -191,14 +191,14 @@ def test_get_startup_script_using_default_script(vm): def test_change_console_port(vm, port_manager): - port1 = port_manager.get_free_tcp_port() - port2 = port_manager.get_free_tcp_port() - port_manager.release_tcp_port(port1) - port_manager.release_tcp_port(port2) + port1 = port_manager.get_free_tcp_port(vm.project) + port2 = port_manager.get_free_tcp_port(vm.project) + port_manager.release_tcp_port(port1, vm.project) + port_manager.release_tcp_port(port2, vm.project) vm.console = port1 vm.console = port2 assert vm.console == port2 - port_manager.reserve_tcp_port(port1) + port_manager.reserve_tcp_port(port1, vm.project) def test_change_name(vm, tmpdir): @@ -219,5 +219,5 @@ def test_close(vm, port_manager, loop): port = vm.console loop.run_until_complete(asyncio.async(vm.close())) # Raise an exception if the port is not free - port_manager.reserve_tcp_port(port) + port_manager.reserve_tcp_port(port, vm.project) assert vm.is_running() is False From 24300b2502568408f441b03b5cf701380e2d48e6 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 21 Mar 2015 22:27:40 -0600 Subject: [PATCH 479/485] Adds project id when requesting UDP port. --- gns3server/handlers/api/network_handler.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/gns3server/handlers/api/network_handler.py b/gns3server/handlers/api/network_handler.py index e84cdb4f..8eb5a364 100644 --- a/gns3server/handlers/api/network_handler.py +++ b/gns3server/handlers/api/network_handler.py @@ -17,6 +17,7 @@ from ...web.route import Route from ...modules.port_manager import PortManager +from ...modules.project_manager import ProjectManager from ...utils.interfaces import interfaces @@ -24,15 +25,21 @@ class NetworkHandler: @classmethod @Route.post( - r"/ports/udp", + r"/projects/{project_id}/ports/udp", + parameters={ + "project_id": "The UUID of the project", + }, status_codes={ 201: "UDP port allocated", + 404: "The project doesn't exist" }, description="Allocate an UDP port on the server") def allocate_udp_port(request, response): + pm = ProjectManager.instance() + project = pm.get_project(request.match_info["project_id"]) m = PortManager.instance() - udp_port = m.get_free_udp_port() + udp_port = m.get_free_udp_port(project) response.set_status(201) response.json({"udp_port": udp_port}) From a77023d1ee256211d523d88be262c81837cbc4f8 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 21 Mar 2015 22:47:12 -0600 Subject: [PATCH 480/485] Bump version to 1.3.0.dev2 --- gns3server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/version.py b/gns3server/version.py index a9b6e119..d3dc3679 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,5 +23,5 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "1.3.0.dev1" +__version__ = "1.3.0.dev2" __version_info__ = (1, 3, 0, -99) From f451ed144e06f2fb2df4df2c7860ad5887d08d56 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 22 Mar 2015 20:40:19 -0600 Subject: [PATCH 481/485] Prevent error when suspend/resume is not supported in QEMU VM. --- gns3server/modules/qemu/qemu_vm.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index ba5b098b..7d1675ee 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -722,6 +722,8 @@ class QemuVM(BaseVM): b"restore-vm", b"running", b"save-vm", b"shutdown", b"suspended", b"watchdog", b"guest-panicked" ]) + if result is None: + return result return result.rsplit(' ', 1)[1] @asyncio.coroutine @@ -731,7 +733,9 @@ class QemuVM(BaseVM): """ vm_status = yield from self._get_vm_status() - if vm_status == "running": + if vm_status is None: + raise QemuError("Suspending a QEMU VM is not supported") + elif vm_status == "running": yield from self._control_vm("stop") log.debug("QEMU VM has been suspended") else: @@ -753,7 +757,9 @@ class QemuVM(BaseVM): """ vm_status = yield from self._get_vm_status() - if vm_status == "paused": + if vm_status is None: + raise QemuError("Resuming a QEMU VM is not supported") + elif vm_status == "paused": yield from self._control_vm("cont") log.debug("QEMU VM has been resumed") else: From f4c7212e3303ec961b05af5d324f2e9a0b7656dc Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 23 Mar 2015 15:24:57 +0100 Subject: [PATCH 482/485] Update sentry key for the RC2 This allow to revoke crash report for old releases --- gns3server/crash_report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py index d26f9343..8373d1d7 100644 --- a/gns3server/crash_report.py +++ b/gns3server/crash_report.py @@ -34,7 +34,7 @@ class CrashReport: Report crash to a third party service """ - DSN = "sync+https://50af75d8641d4ea7a4ea6b38c7df6cf9:41d54936f8f14e558066262e2ec8bbeb@app.getsentry.com/38482" + DSN = "sync+https://2f1f465755f3482993eec637cae95f4c:f71b4e8ecec54ea48666c09d68790558@app.getsentry.com/38482" if hasattr(sys, "frozen"): cacert = os.path.join(os.getcwd(), "cacert.pem") if os.path.isfile(cacert): From cde5c3d9948b707692ec5133dc85be137eb12a92 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 23 Mar 2015 15:56:18 +0100 Subject: [PATCH 483/485] Fix tests --- tests/handlers/api/test_network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/handlers/api/test_network.py b/tests/handlers/api/test_network.py index 24231366..d675d4f6 100644 --- a/tests/handlers/api/test_network.py +++ b/tests/handlers/api/test_network.py @@ -16,8 +16,8 @@ # along with this program. If not, see . -def test_udp_allocation(server): - response = server.post('/ports/udp', {}, example=True) +def test_udp_allocation(server, project): + response = server.post('/projects/{}/ports/udp'.format(project.id), {}, example=True) assert response.status == 201 assert response.json == {'udp_port': 10000} From b5aabd4cc53e319e76bbdc54482b10a6631448dc Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 23 Mar 2015 12:30:27 -0600 Subject: [PATCH 484/485] Fixes initial-config not loading for IOU L2. --- gns3server/modules/iou/iou_vm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 0219c559..fd0cba1f 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -726,7 +726,7 @@ class IOUVM(BaseVM): initial_config_file = self.initial_config_file if initial_config_file: - command.extend(["-c", initial_config_file]) + command.extend(["-c", os.path.basename(initial_config_file)]) if self._l1_keepalives: yield from self._enable_l1_keepalives(command) command.extend([str(self.application_id)]) From 6d0d945d6cc5fc4995ff9cb742837809b335de43 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 23 Mar 2015 20:25:23 +0100 Subject: [PATCH 485/485] Changelog --- CHANGELOG | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 345a8693..df5d1ffd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,15 @@ # Change Log +## 1.3.0rc2 23/03/2015 + +* Update sentry key +* Prevent error when suspend/resume is not supported in QEMU VM. +* Adds project id when requesting UDP port. +* Make sure used ports in a project are cleaned up when closing it. +* Save configs when project is committed. +* Initialize chassis when creating an IOS router. Fixes #107. +* Lock the dynamips reader an writer + ## 1.3.0rc1 19/03/2015 * Save IOS router config when saving the project