diff --git a/.gitignore b/.gitignore index 638a9fe7..d63cf29f 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,10 @@ nosetests.xml # Gedit Backup Files *~ + +#Documentation build +docs/_build + +#VPCS +vpcs.hist +startup.vpcs diff --git a/.travis.yml b/.travis.yml index 883004a4..dadbe57c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,29 +1,32 @@ language: python - -env: - - TOX_ENV=py33 - - TOX_ENV=py34 +python: + - "3.3" + - "3.4" before_install: - sudo add-apt-repository ppa:gns3/ppa -y - sudo apt-get update -q - + - sudo apt-get install dynamips + 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 --cov gns3server --cov-report term-missing -branches: - only: - - master +#branches: +# only: +# - master notifications: - email: false - irc: - channels: - - "chat.freenode.net#gns3" - on_success: change - on_failure: always + email: + - julien@gns3.net +# irc: +# channels: +# - "chat.freenode.net#gns3" +# on_success: change +# on_failure: always +after_success: + - coveralls diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 00000000..df5d1ffd --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,105 @@ +# 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 +* 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. +* 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 + +* Optional IOU license key check. +* Relative path support of IOU, IOS and Qemu images. +* Do not give attachment warning for generic attachments in VirtualBox. +* Support for HDC and HDD disk images in Qemu. +* 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. +* 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 + +* 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. + diff --git a/README.rst b/README.rst index 7cf681e0..e1c04e6b 100644 --- a/README.rst +++ b/README.rst @@ -1,10 +1,16 @@ 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. -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. @@ -17,18 +23,16 @@ You must be connected to the Internet in order to install the dependencies. Dependencies: - Python 3.3 or above -- Setuptools -- PyZMQ library -- Netifaces library -- Tornado -- Jsonschema +- aiohttp +- setuptools +- netifaces +- jsonschema 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: @@ -39,6 +43,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 ------- 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 b7b1fec1..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 diff --git a/dev-requirements.txt b/dev-requirements.txt index efb4e7a0..af76ab92 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,4 +1,9 @@ -rrequirements.txt -pytest -ws4py +sphinx==1.2.3 +pytest==2.6.4 +pep8==1.5.7 +pytest-timeout +pytest-capturelog +pytest-cov +python-coveralls 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/gns3dms/cloud/__init__.py b/docs/__init__.py similarity index 100% rename from gns3dms/cloud/__init__.py rename to docs/__init__.py diff --git a/gns3server/builtins/__init__.py b/docs/_static/.keep similarity index 100% rename from gns3server/builtins/__init__.py rename to docs/_static/.keep diff --git a/docs/api/examples/delete_projectsprojectid.txt b/docs/api/examples/delete_projectsprojectid.txt new file mode 100644 index 00000000..7d59321b --- /dev/null +++ b/docs/api/examples/delete_projectsprojectid.txt @@ -0,0 +1,13 @@ +curl -i -X DELETE 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80' + +DELETE /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80 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..c47266ee --- /dev/null +++ b/docs/api/examples/delete_projectsprojectidiouvmsvmid.txt @@ -0,0 +1,13 @@ +curl -i -X DELETE 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/ac98c05c-dbf1-4157-8f4c-6a0319a0bcdc' + +DELETE /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/ac98c05c-dbf1-4157-8f4c-6a0319a0bcdc 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..289cdd57 --- /dev/null +++ b/docs/api/examples/delete_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -0,0 +1,13 @@ +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/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/acf7667f-91d4-4be5-9eec-f453783bb983/adapters/1/ports/0/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..aee9ddd3 --- /dev/null +++ b/docs/api/examples/delete_projectsprojectidqemuvmsvmid.txt @@ -0,0 +1,13 @@ +curl -i -X DELETE 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/da298f63-4d5b-44a7-8672-ec6642009725' + +DELETE /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/da298f63-4d5b-44a7-8672-ec6642009725 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..2648effe --- /dev/null +++ b/docs/api/examples/delete_projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -0,0 +1,13 @@ +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/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/75fa07c2-fb3d-4a19-815d-2dee5aa5325c/adapters/1/ports/0/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..ecefe5ab --- /dev/null +++ b/docs/api/examples/delete_projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -0,0 +1,13 @@ +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/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/c99f4293-b8b1-40a6-8535-014c4afc2fe7/adapters/0/ports/0/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..44002739 --- /dev/null +++ b/docs/api/examples/delete_projectsprojectidvpcsvmsvmid.txt @@ -0,0 +1,13 @@ +curl -i -X DELETE 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/879f1789-bde0-4e64-ac68-f61a9b114347' + +DELETE /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/879f1789-bde0-4e64-ac68-f61a9b114347 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..edc3f792 --- /dev/null +++ b/docs/api/examples/delete_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -0,0 +1,13 @@ +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/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/5238c683-bb17-49f2-8796-a60668fc5955/adapters/0/ports/0/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..464d9f9e --- /dev/null +++ b/docs/api/examples/get_projectsprojectid.txt @@ -0,0 +1,20 @@ +curl -i -X GET 'http://localhost:8000/projects/00010203-0405-0607-0809-0a0b0c0d0e02' + +GET /projects/00010203-0405-0607-0809-0a0b0c0d0e02 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/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 new file mode 100644 index 00000000..d85e9376 --- /dev/null +++ b/docs/api/examples/get_projectsprojectidiouvmsvmid.txt @@ -0,0 +1,27 @@ +curl -i -X GET 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/c5c0075d-0b10-4401-8bed-d9897814237c' + +GET /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/c5c0075d-0b10-4401-8bed-d9897814237c 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-3783/test_iou_get0/iou.bin", + "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", + "ram": 256, + "serial_adapters": 2, + "vm_id": "c5c0075d-0b10-4401-8bed-d9897814237c" +} diff --git a/docs/api/examples/get_projectsprojectidiouvmsvmidinitialconfig.txt b/docs/api/examples/get_projectsprojectidiouvmsvmidinitialconfig.txt new file mode 100644 index 00000000..44f27589 --- /dev/null +++ b/docs/api/examples/get_projectsprojectidiouvmsvmidinitialconfig.txt @@ -0,0 +1,17 @@ +curl -i -X GET 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/f7220f9c-3334-43e3-9ef4-37f09ba6fcab/initial_config' + +GET /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/f7220f9c-3334-43e3-9ef4-37f09ba6fcab/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..553d0eb6 --- /dev/null +++ b/docs/api/examples/get_projectsprojectidqemuvmsvmid.txt @@ -0,0 +1,34 @@ +curl -i -X GET 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/4532d770-23a0-4858-bbab-d8a8b3a17deb' + +GET /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/4532d770-23a0-4858-bbab-d8a8b3a17deb 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/tmph25p8xei/qemu_x42", + "ram": 256, + "vm_id": "4532d770-23a0-4858-bbab-d8a8b3a17deb" +} diff --git a/docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt b/docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt new file mode 100644 index 00000000..e2d9cc0f --- /dev/null +++ b/docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt @@ -0,0 +1,26 @@ +curl -i -X GET 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/f526503a-8d24-4513-a5e3-1ebf4159aa70' + +GET /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/f526503a-8d24-4513-a5e3-1ebf4159aa70 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": "f526503a-8d24-4513-a5e3-1ebf4159aa70", + "vmname": "VMTEST" +} diff --git a/docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt b/docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt new file mode 100644 index 00000000..c3530f18 --- /dev/null +++ b/docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt @@ -0,0 +1,22 @@ +curl -i -X GET 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/6f3c227f-86f5-4f54-bc4b-74597744b904' + +GET /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/6f3c227f-86f5-4f54-bc4b-74597744b904 HTTP/1.1 + + + +HTTP/1.1 200 +CONNECTION: keep-alive +CONTENT-LENGTH: 220 +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, + "startup_script_path": null, + "vm_id": "6f3c227f-86f5-4f54-bc4b-74597744b904" +} 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..416dbed2 --- /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 201 +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/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 new file mode 100644 index 00000000..b42beff6 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidclose.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/close' -d '{}' + +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/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..261c616f --- /dev/null +++ b/docs/api/examples/post_projectsprojectidcommit.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/commit' -d '{}' + +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/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..2e254f47 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidiouvms.txt @@ -0,0 +1,36 @@ +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/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-3783/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-3783/test_iou_create_with_params0/iou.bin", + "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", + "ram": 1024, + "serial_adapters": 4, + "vm_id": "3c02aa01-46d2-4a62-97d6-dc5829afdf39" +} diff --git a/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt b/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt new file mode 100644 index 00000000..b8cc08d9 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -0,0 +1,21 @@ +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/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" +} + + +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..417bfb5e --- /dev/null +++ b/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.txt @@ -0,0 +1,20 @@ +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/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" +} + + +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/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 new file mode 100644 index 00000000..c1987d70 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.txt @@ -0,0 +1,13 @@ +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/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/550b8f35-e258-4354-a74c-cd35a48c08ed/adapters/0/ports/0/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..05ab639e --- /dev/null +++ b/docs/api/examples/post_projectsprojectidiouvmsvmidreload.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/d1b38099-46a3-4405-a354-85faeb76bd0e/reload' -d '{}' + +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/d1b38099-46a3-4405-a354-85faeb76bd0e/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..d10d8c5d --- /dev/null +++ b/docs/api/examples/post_projectsprojectidiouvmsvmidstart.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/fc1ea907-eb0b-4857-9ad2-759f780afdb4/start' -d '{}' + +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/fc1ea907-eb0b-4857-9ad2-759f780afdb4/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..d97a0400 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidiouvmsvmidstop.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/0228866f-a286-44e2-9688-a8cab4e75cc3/stop' -d '{}' + +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/0228866f-a286-44e2-9688-a8cab4e75cc3/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..b0b4dc89 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidqemuvms.txt @@ -0,0 +1,39 @@ +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/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/tmph25p8xei/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/tmph25p8xei/qemu_x42", + "ram": 1024, + "vm_id": "4e47d2c8-d591-4508-9c3e-00e366f7c22d" +} diff --git a/docs/api/examples/post_projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.txt b/docs/api/examples/post_projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.txt new file mode 100644 index 00000000..a12017e1 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -0,0 +1,21 @@ +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/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" +} + + +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..0082474e --- /dev/null +++ b/docs/api/examples/post_projectsprojectidqemuvmsvmidreload.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/2a143130-2445-4a4b-9ba2-f49071eed5f4/reload' -d '{}' + +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/2a143130-2445-4a4b-9ba2-f49071eed5f4/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..7a30d607 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidqemuvmsvmidresume.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/9272bffc-7a73-449a-aa71-96c48b1e5a3d/resume' -d '{}' + +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/9272bffc-7a73-449a-aa71-96c48b1e5a3d/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..9071b899 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidqemuvmsvmidstart.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/e4f04682-d749-49c8-be3e-8c4752483cc1/start' -d '{}' + +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/e4f04682-d749-49c8-be3e-8c4752483cc1/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..a94f31e4 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidqemuvmsvmidstop.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/1cb28121-2b13-4c53-a0fc-ab8c6d028d0f/stop' -d '{}' + +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/1cb28121-2b13-4c53-a0fc-ab8c6d028d0f/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..ba5dd175 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidqemuvmsvmidsuspend.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/777077ba-025a-49dc-9773-cedc425bdb6d/suspend' -d '{}' + +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/777077ba-025a-49dc-9773-cedc425bdb6d/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..c770d6ff --- /dev/null +++ b/docs/api/examples/post_projectsprojectidvirtualboxvms.txt @@ -0,0 +1,30 @@ +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/a1e920ca-338a-4e9f-b363-aa607b09dd80/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": "1404a6c5-c0f5-4bc6-bb94-477d7e631781", + "vmname": "VM1" +} diff --git a/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.txt b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.txt new file mode 100644 index 00000000..1d5dbc65 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -0,0 +1,25 @@ +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/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", + "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..df9a754b --- /dev/null +++ b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidreload.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/4c9fdef4-8990-44a1-8a98-1b7c510821ee/reload' -d '{}' + +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/4c9fdef4-8990-44a1-8a98-1b7c510821ee/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..40cb8a63 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidresume.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/f6487814-a3a9-4e82-867c-83662c7bed48/resume' -d '{}' + +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/f6487814-a3a9-4e82-867c-83662c7bed48/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..77ab559b --- /dev/null +++ b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidstart.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/bedd61d4-b264-47d9-b290-920d5ba70f6d/start' -d '{}' + +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/bedd61d4-b264-47d9-b290-920d5ba70f6d/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..fb421804 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidstop.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/a1960146-9be9-4f54-b594-67a6ab40f436/stop' -d '{}' + +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/a1960146-9be9-4f54-b594-67a6ab40f436/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..8b89fcd4 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidsuspend.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/4d16597f-fa75-430a-9b94-faf7f93bb0a3/suspend' -d '{}' + +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/4d16597f-fa75-430a-9b94-faf7f93bb0a3/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..f01fddfa --- /dev/null +++ b/docs/api/examples/post_projectsprojectidvpcsvms.txt @@ -0,0 +1,24 @@ +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms' -d '{"name": "PC TEST 1"}' + +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms HTTP/1.1 +{ + "name": "PC TEST 1" +} + + +HTTP/1.1 201 +CONNECTION: keep-alive +CONTENT-LENGTH: 220 +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, + "startup_script_path": null, + "vm_id": "b0d1df2e-ebd1-4783-bb62-871e24f01543" +} diff --git a/docs/api/examples/post_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt b/docs/api/examples/post_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt new file mode 100644 index 00000000..4ba387e6 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -0,0 +1,25 @@ +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/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", + "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..5658f3cd --- /dev/null +++ b/docs/api/examples/post_projectsprojectidvpcsvmsvmidreload.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/b12c5612-147d-4229-86ca-103174ba8fd1/reload' -d '{}' + +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/b12c5612-147d-4229-86ca-103174ba8fd1/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..4b903863 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidvpcsvmsvmidstart.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/01037fd2-9533-42db-bd19-df5b21e47fcf/start' -d '{}' + +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/01037fd2-9533-42db-bd19-df5b21e47fcf/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..01898946 --- /dev/null +++ b/docs/api/examples/post_projectsprojectidvpcsvmsvmidstop.txt @@ -0,0 +1,13 @@ +curl -i -X POST 'http://localhost:8000/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/20eefc85-f1b6-4e36-9601-cd0dc91faaa0/stop' -d '{}' + +POST /projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/20eefc85-f1b6-4e36-9601-cd0dc91faaa0/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..0e276071 --- /dev/null +++ b/docs/api/examples/put_projectsprojectid.txt @@ -0,0 +1,20 @@ +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/ef58b29e-59df-42c0-9492-5a766a13cb62 HTTP/1.1 +{ + "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-3783/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..810284c6 --- /dev/null +++ b/docs/api/examples/put_projectsprojectidiouvmsvmid.txt @@ -0,0 +1,36 @@ +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/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/324dab07-d86c-4a3b-8390-7a8e9a506006 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-3783/test_iou_update0/iou.bin", + "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", + "ram": 512, + "serial_adapters": 0, + "vm_id": "324dab07-d86c-4a3b-8390-7a8e9a506006" +} diff --git a/docs/api/examples/put_projectsprojectidqemuvmsvmid.txt b/docs/api/examples/put_projectsprojectidqemuvmsvmid.txt new file mode 100644 index 00000000..292fd2b1 --- /dev/null +++ b/docs/api/examples/put_projectsprojectidqemuvmsvmid.txt @@ -0,0 +1,39 @@ +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/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/a68a00de-a264-4cc0-821c-301af5059ea4 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/tmph25p8xei/qemu_x42", + "ram": 1024, + "vm_id": "a68a00de-a264-4cc0-821c-301af5059ea4" +} diff --git a/docs/api/examples/put_projectsprojectidvirtualboxvmsvmid.txt b/docs/api/examples/put_projectsprojectidvirtualboxvmsvmid.txt new file mode 100644 index 00000000..480c4cf1 --- /dev/null +++ b/docs/api/examples/put_projectsprojectidvirtualboxvmsvmid.txt @@ -0,0 +1,29 @@ +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/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/4a6375b9-14b8-406c-ae8e-df6d242088f9 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": "4a6375b9-14b8-406c-ae8e-df6d242088f9", + "vmname": "VMTEST" +} diff --git a/docs/api/examples/put_projectsprojectidvpcsvmsvmid.txt b/docs/api/examples/put_projectsprojectidvpcsvmsvmid.txt new file mode 100644 index 00000000..b90c37ca --- /dev/null +++ b/docs/api/examples/put_projectsprojectidvpcsvmsvmid.txt @@ -0,0 +1,26 @@ +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/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/39951883-cbfb-4aff-9cbc-06895749e571 HTTP/1.1 +{ + "console": 2011, + "name": "test", + "startup_script": "ip 192.168.1.1" +} + + +HTTP/1.1 200 +CONNECTION: keep-alive +CONTENT-LENGTH: 236 +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", + "startup_script_path": "startup.vpc", + "vm_id": "39951883-cbfb-4aff-9cbc-06895749e571" +} diff --git a/docs/api/v1/dynamips_device.rst b/docs/api/v1/dynamips_device.rst new file mode 100644 index 00000000..83c17b94 --- /dev/null +++ b/docs/api/v1/dynamips_device.rst @@ -0,0 +1,8 @@ +Dynamips device +--------------------- + +.. toctree:: + :glob: + :maxdepth: 2 + + dynamips_device/* diff --git a/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevices.rst b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevices.rst new file mode 100644 index 00000000..d43ed5bf --- /dev/null +++ b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevices.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/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceid.rst b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceid.rst new file mode 100644 index 00000000..7f0e65e4 --- /dev/null +++ b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceid.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 +********** +- **project_id**: UUID for the project +- **device_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
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 +********** +- **project_id**: UUID for the project +- **device_id**: UUID for the instance + +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 +********** +- **project_id**: UUID for the project +- **device_id**: UUID for the instance + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance deleted + diff --git a/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst new file mode 100644 index 00000000..706b5c06 --- /dev/null +++ b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.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 +********** +- **project_id**: UUID for the project +- **device_id**: UUID for the instance +- **port_number**: Port on the device + +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 +********** +- **project_id**: UUID for the project +- **device_id**: UUID for the instance +- **port_number**: Port on the device + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: NIO deleted + diff --git a/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst new file mode 100644 index 00000000..19852f49 --- /dev/null +++ b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.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 +********** +- **project_id**: UUID for the project +- **device_id**: UUID for the instance +- **port_number**: Port on the device + +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/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst new file mode 100644 index 00000000..cc312e43 --- /dev/null +++ b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.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 +********** +- **project_id**: UUID for the project +- **device_id**: UUID for the instance +- **port_number**: Port on the device + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Capture stopped + diff --git a/docs/api/v1/dynamips_vm.rst b/docs/api/v1/dynamips_vm.rst new file mode 100644 index 00000000..f32d26b7 --- /dev/null +++ b/docs/api/v1/dynamips_vm.rst @@ -0,0 +1,8 @@ +Dynamips vm +--------------------- + +.. toctree:: + :glob: + :maxdepth: 2 + + dynamips_vm/* diff --git a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvms.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvms.rst new file mode 100644 index 00000000..b50dfdad --- /dev/null +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvms.rst @@ -0,0 +1,120 @@ +/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
chassis string router chassis model
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
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
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
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
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
chassis string router chassis model
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/v1/dynamips_vm/projectsprojectiddynamipsvmsvmid.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmid.rst new file mode 100644 index 00000000..11b3c065 --- /dev/null +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmid.rst @@ -0,0 +1,202 @@ +/v1/projects/{project_id}/dynamips/vms/{vm_id} +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +GET /v1/projects/**{project_id}**/dynamips/vms/**{vm_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Get a Dynamips VM 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
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
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 +********** +- **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
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
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
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
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
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
wic2 Network module WIC slot 0
+ +Output +******* +.. 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
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 +********** +- **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/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst new file mode 100644 index 00000000..25d0a246 --- /dev/null +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.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 +********** +- **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 + +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 +********** +- **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 + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: NIO deleted + diff --git a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst new file mode 100644 index 00000000..0d54a8c6 --- /dev/null +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.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 +********** +- **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 + +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/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst new file mode 100644 index 00000000..f89a083c --- /dev/null +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.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 +********** +- **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) + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Capture stopped + diff --git a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidautoidlepc.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidautoidlepc.rst new file mode 100644 index 00000000..24c17864 --- /dev/null +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidautoidlepc.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/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidconfigs.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidconfigs.rst new file mode 100644 index 00000000..547b9e25 --- /dev/null +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidconfigs.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/v1/dynamips_vm/projectsprojectiddynamipsvmsvmididlepcproposals.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmididlepcproposals.rst new file mode 100644 index 00000000..18722f8a --- /dev/null +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmididlepcproposals.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/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidreload.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidreload.rst new file mode 100644 index 00000000..23bb67f3 --- /dev/null +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidreload.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 +********** +- **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/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidresume.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidresume.rst new file mode 100644 index 00000000..73ce9d01 --- /dev/null +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidresume.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 +********** +- **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/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidstart.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidstart.rst new file mode 100644 index 00000000..24c3d2af --- /dev/null +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidstart.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 +********** +- **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/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidstop.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidstop.rst new file mode 100644 index 00000000..fca471b6 --- /dev/null +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidstop.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 +********** +- **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/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidsuspend.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidsuspend.rst new file mode 100644 index 00000000..54394e01 --- /dev/null +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidsuspend.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 +********** +- **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/docs/api/v1/network.rst b/docs/api/v1/network.rst new file mode 100644 index 00000000..38366abe --- /dev/null +++ b/docs/api/v1/network.rst @@ -0,0 +1,8 @@ +Network +--------------------- + +.. toctree:: + :glob: + :maxdepth: 2 + + network/* diff --git a/docs/api/v1/network/interfaces.rst b/docs/api/v1/network/interfaces.rst new file mode 100644 index 00000000..35036d57 --- /dev/null +++ b/docs/api/v1/network/interfaces.rst @@ -0,0 +1,19 @@ +/v1/interfaces +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +GET /v1/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/v1/network/portsudp.rst b/docs/api/v1/network/portsudp.rst new file mode 100644 index 00000000..c37318ed --- /dev/null +++ b/docs/api/v1/network/portsudp.rst @@ -0,0 +1,19 @@ +/v1/ports/udp +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/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/v1/project.rst b/docs/api/v1/project.rst new file mode 100644 index 00000000..95453d81 --- /dev/null +++ b/docs/api/v1/project.rst @@ -0,0 +1,8 @@ +Project +--------------------- + +.. toctree:: + :glob: + :maxdepth: 2 + + project/* diff --git a/docs/api/v1/project/projects.rst b/docs/api/v1/project/projects.rst new file mode 100644 index 00000000..53f234b7 --- /dev/null +++ b/docs/api/v1/project/projects.rst @@ -0,0 +1,43 @@ +/v1/projects +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Create a new project on the server + +Response status codes +********************** +- **201**: Project created +- **409**: Project already created + +Input +******* +.. raw:: html + + + + + + +
Name Mandatory Type Description
path ['string', 'null'] Project directory
project_id ['string', 'null'] Project UUID
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
path string Directory of the project on the server
project_id string Project UUID
temporary boolean If project is a temporary project
+ +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projects.txt + diff --git a/docs/api/v1/project/projectsprojectid.rst b/docs/api/v1/project/projectsprojectid.rst new file mode 100644 index 00000000..bed089de --- /dev/null +++ b/docs/api/v1/project/projectsprojectid.rst @@ -0,0 +1,99 @@ +/v1/projects/{project_id} +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +GET /v1/projects/**{project_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Get project information + +Parameters +********** +- **project_id**: The UUID of the project + +Response status codes +********************** +- **200**: Success +- **404**: The project doesn't exist + +Output +******* +.. raw:: html + + + + + + + +
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
+ +Sample session +*************** + + +.. literalinclude:: ../../examples/get_projectsprojectid.txt + + +PUT /v1/projects/**{project_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Update a project + +Parameters +********** +- **project_id**: The UUID of the project + +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 +******* +.. raw:: html + + + + + +
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
+ +Output +******* +.. raw:: html + + + + + + + +
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
+ +Sample session +*************** + + +.. literalinclude:: ../../examples/put_projectsprojectid.txt + + +DELETE /v1/projects/**{project_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Delete a project from 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 + +Sample session +*************** + + +.. literalinclude:: ../../examples/delete_projectsprojectid.txt + diff --git a/docs/api/v1/project/projectsprojectidclose.rst b/docs/api/v1/project/projectsprojectidclose.rst new file mode 100644 index 00000000..5f9b867f --- /dev/null +++ b/docs/api/v1/project/projectsprojectidclose.rst @@ -0,0 +1,24 @@ +/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 + +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidclose.txt + diff --git a/docs/api/v1/project/projectsprojectidcommit.rst b/docs/api/v1/project/projectsprojectidcommit.rst new file mode 100644 index 00000000..f08f2a33 --- /dev/null +++ b/docs/api/v1/project/projectsprojectidcommit.rst @@ -0,0 +1,24 @@ +/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 + +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidcommit.txt + diff --git a/docs/api/v1/qemu.rst b/docs/api/v1/qemu.rst new file mode 100644 index 00000000..70fd8fc2 --- /dev/null +++ b/docs/api/v1/qemu.rst @@ -0,0 +1,8 @@ +Qemu +--------------------- + +.. toctree:: + :glob: + :maxdepth: 2 + + qemu/* diff --git a/docs/api/v1/qemu/projectsprojectidqemuvms.rst b/docs/api/v1/qemu/projectsprojectidqemuvms.rst new file mode 100644 index 00000000..155fa581 --- /dev/null +++ b/docs/api/v1/qemu/projectsprojectidqemuvms.rst @@ -0,0 +1,76 @@ +/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
+ +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidqemuvms.txt + diff --git a/docs/api/v1/qemu/projectsprojectidqemuvmsvmid.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmid.rst new file mode 100644 index 00000000..0270bbd9 --- /dev/null +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmid.rst @@ -0,0 +1,147 @@ +/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
+ +Sample session +*************** + + +.. literalinclude:: ../../examples/get_projectsprojectidqemuvmsvmid.txt + + +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
+ +Sample session +*************** + + +.. literalinclude:: ../../examples/put_projectsprojectidqemuvmsvmid.txt + + +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 + +Sample session +*************** + + +.. literalinclude:: ../../examples/delete_projectsprojectidqemuvmsvmid.txt + diff --git a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst new file mode 100644 index 00000000..e1c94c53 --- /dev/null +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -0,0 +1,52 @@ +/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 + +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 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +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 + +Sample session +*************** + + +.. literalinclude:: ../../examples/delete_projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.txt + diff --git a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidreload.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidreload.rst new file mode 100644 index 00000000..5b29cec3 --- /dev/null +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidreload.rst @@ -0,0 +1,26 @@ +/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 + +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidqemuvmsvmidreload.txt + diff --git a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidresume.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidresume.rst new file mode 100644 index 00000000..59b5c1f7 --- /dev/null +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidresume.rst @@ -0,0 +1,26 @@ +/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 + +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidqemuvmsvmidresume.txt + diff --git a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstart.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstart.rst new file mode 100644 index 00000000..f5306892 --- /dev/null +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstart.rst @@ -0,0 +1,26 @@ +/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 + +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidqemuvmsvmidstart.txt + diff --git a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstop.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstop.rst new file mode 100644 index 00000000..d5c41c96 --- /dev/null +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstop.rst @@ -0,0 +1,26 @@ +/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 + +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidqemuvmsvmidstop.txt + diff --git a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidsuspend.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidsuspend.rst new file mode 100644 index 00000000..63e8c94a --- /dev/null +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidsuspend.rst @@ -0,0 +1,26 @@ +/v1/projects/{project_id}/qemu/vms/{vm_id}/suspend +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/qemu/vms/**{vm_id}**/suspend +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Suspend 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 + +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidqemuvmsvmidsuspend.txt + diff --git a/docs/api/v1/qemu/qemubinaries.rst b/docs/api/v1/qemu/qemubinaries.rst new file mode 100644 index 00000000..6f851195 --- /dev/null +++ b/docs/api/v1/qemu/qemubinaries.rst @@ -0,0 +1,21 @@ +/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 + +Sample session +*************** + + +.. literalinclude:: ../../examples/get_qemubinaries.txt + diff --git a/docs/api/v1/version.rst b/docs/api/v1/version.rst new file mode 100644 index 00000000..adc4c1f0 --- /dev/null +++ b/docs/api/v1/version.rst @@ -0,0 +1,8 @@ +Version +--------------------- + +.. toctree:: + :glob: + :maxdepth: 2 + + version/* diff --git a/docs/api/v1/version/version.rst b/docs/api/v1/version/version.rst new file mode 100644 index 00000000..8733379d --- /dev/null +++ b/docs/api/v1/version/version.rst @@ -0,0 +1,62 @@ +/v1/version +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +GET /v1/version +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Retrieve the server version number + +Response status codes +********************** +- **200**: OK + +Output +******* +.. raw:: html + + + + +
Name Mandatory Type Description
version string Version number human readable
+ +Sample session +*************** + + +.. literalinclude:: ../../examples/get_version.txt + + +POST /v1/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/v1/virtualbox.rst b/docs/api/v1/virtualbox.rst new file mode 100644 index 00000000..517624b2 --- /dev/null +++ b/docs/api/v1/virtualbox.rst @@ -0,0 +1,8 @@ +Virtualbox +--------------------- + +.. toctree:: + :glob: + :maxdepth: 2 + + virtualbox/* diff --git a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvms.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvms.rst new file mode 100644 index 00000000..03d96831 --- /dev/null +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvms.rst @@ -0,0 +1,61 @@ +/v1/projects/{project_id}/virtualbox/vms +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{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_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
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)
+ +Output +******* +.. raw:: html + + + + + + + + + + + + + +
Name Mandatory Type Description
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
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)
+ +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidvirtualboxvms.txt + diff --git a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmid.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmid.rst new file mode 100644 index 00000000..8ec38157 --- /dev/null +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmid.rst @@ -0,0 +1,117 @@ +/v1/projects/{project_id}/virtualbox/vms/{vm_id} +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +GET /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Get a VirtualBox VM 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 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
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)
+ +Sample session +*************** + + +.. literalinclude:: ../../examples/get_projectsprojectidvirtualboxvmsvmid.txt + + +PUT /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Update a VirtualBox VM 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 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)
+ +Output +******* +.. raw:: html + + + + + + + + + + + + + +
Name Mandatory Type Description
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
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)
+ +Sample session +*************** + + +.. literalinclude:: ../../examples/put_projectsprojectidvirtualboxvmsvmid.txt + + +DELETE /v1/projects/**{project_id}**/virtualbox/vms/**{vm_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Delete a VirtualBox VM 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/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst new file mode 100644 index 00000000..0ba49c65 --- /dev/null +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -0,0 +1,52 @@ +/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 +********** +- **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) + +Response status codes +********************** +- **400**: Invalid request +- **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 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +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) + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: NIO deleted + +Sample session +*************** + + +.. literalinclude:: ../../examples/delete_projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.txt + diff --git a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst new file mode 100644 index 00000000..402ccc5e --- /dev/null +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.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 +********** +- **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) + +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/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst new file mode 100644 index 00000000..63e1f22d --- /dev/null +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.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 +********** +- **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) + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Capture stopped + diff --git a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidreload.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidreload.rst new file mode 100644 index 00000000..9ae84c29 --- /dev/null +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidreload.rst @@ -0,0 +1,26 @@ +/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 +********** +- **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_projectsprojectidvirtualboxvmsvmidreload.txt + diff --git a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidresume.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidresume.rst new file mode 100644 index 00000000..0fb9d427 --- /dev/null +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidresume.rst @@ -0,0 +1,26 @@ +/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 +********** +- **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 + +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidvirtualboxvmsvmidresume.txt + diff --git a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidstart.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidstart.rst new file mode 100644 index 00000000..5e6a6c42 --- /dev/null +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidstart.rst @@ -0,0 +1,26 @@ +/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 +********** +- **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_projectsprojectidvirtualboxvmsvmidstart.txt + diff --git a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidstop.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidstop.rst new file mode 100644 index 00000000..1eaac889 --- /dev/null +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidstop.rst @@ -0,0 +1,26 @@ +/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 +********** +- **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_projectsprojectidvirtualboxvmsvmidstop.txt + diff --git a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidsuspend.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidsuspend.rst new file mode 100644 index 00000000..ad7f469b --- /dev/null +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidsuspend.rst @@ -0,0 +1,26 @@ +/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 +********** +- **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 + +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidvirtualboxvmsvmidsuspend.txt + diff --git a/docs/api/v1/virtualbox/virtualboxvms.rst b/docs/api/v1/virtualbox/virtualboxvms.rst new file mode 100644 index 00000000..6499008b --- /dev/null +++ b/docs/api/v1/virtualbox/virtualboxvms.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/v1/vpcs.rst b/docs/api/v1/vpcs.rst new file mode 100644 index 00000000..ab00c921 --- /dev/null +++ b/docs/api/v1/vpcs.rst @@ -0,0 +1,8 @@ +Vpcs +--------------------- + +.. toctree:: + :glob: + :maxdepth: 2 + + vpcs/* diff --git a/docs/api/v1/vpcs/projectsprojectidvpcsvms.rst b/docs/api/v1/vpcs/projectsprojectidvpcsvms.rst new file mode 100644 index 00000000..3bc1e779 --- /dev/null +++ b/docs/api/v1/vpcs/projectsprojectidvpcsvms.rst @@ -0,0 +1,51 @@ +/v1/projects/{project_id}/vpcs/vms +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{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 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
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
+ +Sample session +*************** + + +.. literalinclude:: ../../examples/post_projectsprojectidvpcsvms.txt + diff --git a/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmid.rst b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmid.rst new file mode 100644 index 00000000..08762d16 --- /dev/null +++ b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmid.rst @@ -0,0 +1,110 @@ +/v1/projects/{project_id}/vpcs/vms/{vm_id} +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +GET /v1/projects/**{project_id}**/vpcs/vms/**{vm_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Get a VPCS 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
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
+ +Sample session +*************** + + +.. literalinclude:: ../../examples/get_projectsprojectidvpcsvmsvmid.txt + + +PUT /v1/projects/**{project_id}**/vpcs/vms/**{vm_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Update a VPCS 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
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
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
+ +Sample session +*************** + + +.. literalinclude:: ../../examples/put_projectsprojectidvpcsvmsvmid.txt + + +DELETE /v1/projects/**{project_id}**/vpcs/vms/**{vm_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Delete a VPCS 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_projectsprojectidvpcsvmsvmid.txt + diff --git a/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst new file mode 100644 index 00000000..a10f3587 --- /dev/null +++ b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -0,0 +1,52 @@ +/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 +********** +- **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 + +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 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +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 + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: NIO deleted + +Sample session +*************** + + +.. literalinclude:: ../../examples/delete_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt + diff --git a/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidreload.rst b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidreload.rst new file mode 100644 index 00000000..224798fd --- /dev/null +++ b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidreload.rst @@ -0,0 +1,26 @@ +/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 +********** +- **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_projectsprojectidvpcsvmsvmidreload.txt + diff --git a/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidstart.rst b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidstart.rst new file mode 100644 index 00000000..b87f43a6 --- /dev/null +++ b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidstart.rst @@ -0,0 +1,26 @@ +/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 +********** +- **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_projectsprojectidvpcsvmsvmidstart.txt + diff --git a/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidstop.rst b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidstop.rst new file mode 100644 index 00000000..269c8953 --- /dev/null +++ b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidstop.rst @@ -0,0 +1,26 @@ +/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 +********** +- **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_projectsprojectidvpcsvmsvmidstop.txt + diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..1c05ec66 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,269 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# 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 +# 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 gns3server.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 = '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 +# 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' + +html_sidebars = { + '**': ['sourcelink.html', 'searchbox.html'], +} +# html_theme = 'nature' + +# 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 +# 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 = False + +# 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 = 'GNS3doc' + + +# -- 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', 'GNS3.tex', 'GNS3 Documentation', 'GNS3 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', 'GNS3 Documentation', + ['GNS3 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', 'GNS3', 'GNS3 Documentation', + 'GNS3 Team', 'GNS3', '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..b2a1be80 --- /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 + + ./scripts/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..7632459f --- /dev/null +++ b/docs/general.rst @@ -0,0 +1,200 @@ +Communications +=============== + +All the communication are done over HTTP using JSON. + +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" + } + +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 +============ + +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. + + +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. + + 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 new file mode 100644 index 00000000..4f12a125 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,22 @@ +Welcome to API documentation! +====================================== + +.. WARNING:: + The API is not stable, feel free to send comment on GNS3 Jungle + https://community.gns3.com/ + +.. toctree:: + general + glossary + development + + +API Endpoints +~~~~~~~~~~~~~~~ + +.. toctree:: + :glob: + :maxdepth: 2 + + api/v1/* + 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/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/base_cloud_ctrl.py b/gns3dms/cloud/base_cloud_ctrl.py deleted file mode 100644 index 236cdccc..00000000 --- a/gns3dms/cloud/base_cloud_ctrl.py +++ /dev/null @@ -1,341 +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 beeb598d..00000000 --- a/gns3dms/cloud/exceptions.py +++ /dev/null @@ -1,45 +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 6cdad64e..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"] == 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'] == True: - log_level_console = logging.INFO - - if cmd_options['debug'] == 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_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'] == 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"]) == 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: - 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 885d6fa0..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) \ No newline at end of file diff --git a/gns3dms/modules/daemon.py b/gns3dms/modules/daemon.py deleted file mode 100644 index c7245335..00000000 --- a/gns3dms/modules/daemon.py +++ /dev/null @@ -1,138 +0,0 @@ -"""Generic linux daemon base class for python 3.x.""" - -import sys, os, time, atexit, 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 1: - 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 487e6f9f..00000000 --- a/gns3dms/modules/rackspace_cloud.py +++ /dev/null @@ -1,70 +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, 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 == 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/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/_compat.py b/gns3server/_compat.py deleted file mode 100644 index 78bd53f8..00000000 --- a/gns3server/_compat.py +++ /dev/null @@ -1,36 +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 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 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..9bcc9038 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 @@ -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__) @@ -30,66 +31,104 @@ CLOUD_SERVER = 'CLOUD_SERVER' class Config(object): + """ Configuration file management using configparser. + + :params files: Array of configuration files (optional) """ - def __init__(self): + def __init__(self, files=None): + + self._files = files + + # Monitor configuration files for changes + 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 + # 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%") - 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] + filename = "gns3_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] 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 + 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" - 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 = "gns3_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.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._cloud_config = configparser.ConfigParser() - self.read_cloud_config() - def list_cloud_config_file(self): - return self._cloud_file + def _watch_config_file(self): + asyncio.get_event_loop().call_later(1, self._check_config_file_change) - 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 _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): + """ + Reload configuration + """ - def cloud_settings(self): - return self._cloud_config[CLOUD_SERVER] + self.read_config() + for section in self._override_config: + self.set_section_config(section, self._override_config[section]) + + def get_config_files(self): + return self._watched_files def read_config(self): """ @@ -98,7 +137,11 @@ 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") + 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): """ @@ -117,18 +160,59 @@ class Config(object): :returns: configparser section """ - if not section in self._config: + if section not in self._config: 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 dictionary with section content + """ + + 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 + """ + + 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(): + 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 """ - 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/gns3server/crash_report.py b/gns3server/crash_report.py new file mode 100644 index 00000000..8373d1d7 --- /dev/null +++ b/gns3server/crash_report.py @@ -0,0 +1,84 @@ +# -*- 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 os +import sys +import struct +import platform + +from .version import __version__ +from .config import Config + +import logging +log = logging.getLogger(__name__) + + +class CrashReport: + + """ + Report crash to a third party service + """ + + 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): + DSN += "?ca_certs={}".format(cacert) + else: + log.warning("The SSL certificate bundle file '{}' could not be found".format(cacert)) + _instance = None + + def __init__(self): + self._client = None + + 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__, raise_send_errors=True) + if request is not None: + self._client.http_context({ + "method": request.method, + "url": request.path, + "data": request.json, + }) + self._client.tags_context({ + "os:name": platform.system(), + "os:release": platform.release(), + "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]), + "python:bit": struct.calcsize("P") * 8, + "python:encoding": sys.getdefaultencoding(), + "python:frozen": "{}".format(hasattr(sys, "frozen")) + }) + try: + 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 + def instance(cls): + if cls._instance is None: + cls._instance = CrashReport() + return cls._instance diff --git a/gns3server/handlers/__init__.py b/gns3server/handlers/__init__.py index 2f4e0e80..cd99ee1d 100644 --- a/gns3server/handlers/__init__.py +++ b/gns3server/handlers/__init__.py @@ -43,3 +43,6 @@ class HomePage: ) def index(request, response): response.template("homepage.html") + +if sys.platform.startswith("linux") or hasattr(sys, "_called_from_test"): + from gns3server.handlers.api.iou_handler import IOUHandler diff --git a/gns3server/modules/dynamips/backends/__init__.py b/gns3server/handlers/api/__init__.py similarity index 100% rename from gns3server/modules/dynamips/backends/__init__.py rename to gns3server/handlers/api/__init__.py diff --git a/gns3server/handlers/api/config_handler.py b/gns3server/handlers/api/config_handler.py new file mode 100644 index 00000000..fc1a61d0 --- /dev/null +++ b/gns3server/handlers/api/config_handler.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 . + +from ...web.route import Route +from ...config import Config +from aiohttp.web import HTTPForbidden + + +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/gns3server/handlers/api/dynamips_device_handler.py b/gns3server/handlers/api/dynamips_device_handler.py new file mode 100644 index 00000000..3a6f8588 --- /dev/null +++ b/gns3server/handlers/api/dynamips_device_handler.py @@ -0,0 +1,229 @@ +# -*- 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 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/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 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"]) + port_number = int(request.match_info["port_number"]) + yield from device.stop_capture(port_number) + response.set_status(204) diff --git a/gns3server/handlers/api/dynamips_vm_handler.py b/gns3server/handlers/api/dynamips_vm_handler.py new file mode 100644 index 00000000..a52159e6 --- /dev/null +++ b/gns3server/handlers/api/dynamips_vm_handler.py @@ -0,0 +1,408 @@ +# -*- 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 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 + + +class DynamipsVMHandler: + + """ + API entry points for Dynamips VMs. + """ + + @classmethod + @Route.post( + r"/projects/{project_id}/dynamips/vms", + parameters={ + "project_id": "UUID for the project" + }, + status_codes={ + 201: "Instance created", + 400: "Invalid request", + 409: "Conflict" + }, + 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"), + console=request.json.get("console"), + 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) + 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"]) + + yield from dynamips_manager.update_vm_settings(vm, request.json) + yield from dynamips_manager.ghost_ios_support(vm) + 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 resume(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) + + @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) + + @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 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"]) + 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.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 get_configs(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}) + + @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={ + 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() + 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): + + 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({"idlepc": idlepc}) diff --git a/gns3server/handlers/api/iou_handler.py b/gns3server/handlers/api/iou_handler.py new file mode 100644 index 00000000..edebae1b --- /dev/null +++ b/gns3server/handlers/api/iou_handler.py @@ -0,0 +1,321 @@ +# -*- 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 aiohttp.web import HTTPConflict + +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: + + """ + 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"), + serial_adapters=request.json.get("serial_adapters"), + ethernet_adapters=request.json.get("ethernet_adapters"), + 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"), + iourc_content=request.json.get("iourc_content") + ) + vm.path = request.json.get("path", vm.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.path = request.json.get("path", vm.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) + 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) + + @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) + + @Route.post( + 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={ + 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.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}/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={ + 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.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", + 409: "VM not started" + }, + 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"]) + + 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": str(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", + 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) + 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}) diff --git a/gns3server/handlers/api/network_handler.py b/gns3server/handlers/api/network_handler.py new file mode 100644 index 00000000..8eb5a364 --- /dev/null +++ b/gns3server/handlers/api/network_handler.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 ...web.route import Route +from ...modules.port_manager import PortManager +from ...modules.project_manager import ProjectManager +from ...utils.interfaces import interfaces + + +class NetworkHandler: + + @classmethod + @Route.post( + 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(project) + 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/handlers/api/project_handler.py b/gns3server/handlers/api/project_handler.py new file mode 100644 index 00000000..e31d32bc --- /dev/null +++ b/gns3server/handlers/api/project_handler.py @@ -0,0 +1,147 @@ +# -*- 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, PROJECT_CREATE_SCHEMA, PROJECT_UPDATE_SCHEMA +from ...modules.project_manager import ProjectManager +from ...modules import MODULES + + +class ProjectHandler: + + @classmethod + @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): + + 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) + ) + response.set_status(201) + response.json(p) + + @classmethod + @Route.get( + r"/projects/{project_id}", + description="Get project information", + parameters={ + "project_id": "The UUID of the project", + }, + status_codes={ + 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["project_id"]) + response.json(project) + + @classmethod + @Route.put( + r"/projects/{project_id}", + description="Update a project", + parameters={ + "project_id": "The UUID of the project", + }, + 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, + input=PROJECT_UPDATE_SCHEMA) + def update(request, response): + + 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 + for module in MODULES: + yield from module.instance().project_moved(project) + response.json(project) + + @classmethod + @Route.post( + r"/projects/{project_id}/commit", + description="Write changes on disk", + parameters={ + "project_id": "The UUID of the project", + }, + status_codes={ + 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["project_id"]) + yield from project.commit() + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/close", + description="Close a project", + parameters={ + "project_id": "The UUID of the project", + }, + status_codes={ + 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["project_id"]) + yield from project.close() + pm.remove_project(project.id) + response.set_status(204) + + @classmethod + @Route.delete( + r"/projects/{project_id}", + description="Delete a project from disk", + parameters={ + "project_id": "The UUID of the project", + }, + status_codes={ + 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["project_id"]) + yield from project.delete() + pm.remove_project(project.id) + response.set_status(204) diff --git a/gns3server/handlers/api/qemu_handler.py b/gns3server/handlers/api/qemu_handler.py new file mode 100644 index 00000000..659d1b03 --- /dev/null +++ b/gns3server/handlers/api/qemu_handler.py @@ -0,0 +1,290 @@ +# -*- 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.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: + + """ + 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")) + + # 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) + + @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"]) + for field in request.json: + setattr(vm, field, request.json[field]) + + 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="Suspend 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) + + @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={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance", + "adapter_number": "Network adapter where the nio is located", + "port_number": "Port on the adapter (always 0)" + }, + 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) + 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}/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 on the adapter (always 0)" + }, + 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"]) + yield from vm.adapter_remove_nio_binding(int(request.match_info["adapter_number"])) + response.set_status(204) + + @classmethod + @Route.get( + r"/qemu/binaries", + 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): + + binaries = yield from Qemu.binary_list() + response.json(binaries) diff --git a/gns3server/handlers/api/server_handler.py b/gns3server/handlers/api/server_handler.py new file mode 100644 index 00000000..105129f2 --- /dev/null +++ b/gns3server/handlers/api/server_handler.py @@ -0,0 +1,66 @@ +# -*- 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 ...modules.project_manager import ProjectManager +from aiohttp.web import HTTPForbidden + +import asyncio +import logging + +log = logging.getLogger(__name__) + + +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") + + # 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()) + response.set_status(201) diff --git a/gns3server/handlers/api/version_handler.py b/gns3server/handlers/api/version_handler.py new file mode 100644 index 00000000..22a2131c --- /dev/null +++ b/gns3server/handlers/api/version_handler.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 . + +from ...web.route import Route +from ...config import Config +from ...schemas.version import VERSION_SCHEMA +from ...version import __version__ +from aiohttp.web import HTTPConflict + + +class VersionHandler: + + @classmethod + @Route.get( + r"/version", + description="Retrieve the server version number", + output=VERSION_SCHEMA) + def version(request, response): + + config = Config.instance() + local_server = config.get_section_config("Server").getboolean("local", False) + response.json({"version": __version__, "local": local_server}) + + @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(text="Client version {} differs with server version {}".format(request.json["version"], __version__)) + response.json({"version": __version__}) diff --git a/gns3server/handlers/api/virtualbox_handler.py b/gns3server/handlers/api/virtualbox_handler.py new file mode 100644 index 00000000..f0765180 --- /dev/null +++ b/gns3server/handlers/api/virtualbox_handler.py @@ -0,0 +1,364 @@ +# -*- 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.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: + + """ + API entry points for VirtualBox. + """ + + @classmethod + @Route.get( + r"/virtualbox/vms", + 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"/projects/{project_id}/virtualbox/vms", + parameters={ + "project_id": "UUID for the project" + }, + status_codes={ + 201: "Instance created", + 400: "Invalid request", + 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.pop("name"), + request.match_info["project_id"], + 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")) + + 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) + + response.set_status(201) + response.json(vm) + + @classmethod + @Route.get( + r"/projects/{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", + output=VBOX_OBJECT_SCHEMA) + def show(request, response): + + vbox_manager = VirtualBox.instance() + vm = vbox_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}/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" + }, + 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["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")) + + if "adapters" in request.json: + adapters = int(request.json.pop("adapters")) + 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) + + response.json(vm) + + @classmethod + @Route.delete( + r"/projects/{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"/projects/{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 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"], project_id=request.match_info["project_id"]) + yield from vm.start() + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{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 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"], project_id=request.match_info["project_id"]) + yield from vm.stop() + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{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 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"], project_id=request.match_info["project_id"]) + yield from vm.suspend() + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{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 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"], project_id=request.match_info["project_id"]) + yield from vm.resume() + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{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 request", + 404: "Instance doesn't exist" + }, + description="Reload a VirtualBox VM instance") + def reload(request, response): + + vbox_manager = VirtualBox.instance() + 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"/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_number": "Adapter where the nio should be added", + "port_number": "Port on the adapter (always 0)" + }, + status_codes={ + 201: "NIO created", + 400: "Invalid request", + 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["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_number"]), nio) + response.set_status(201) + response.json(nio) + + @classmethod + @Route.delete( + 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_number": "Adapter from where the nio should be removed", + "port_number": "Port on the adapter (always 0)" + }, + status_codes={ + 204: "NIO deleted", + 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"], project_id=request.match_info["project_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_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 (always 0)" + }, + status_codes={ + 200: "Capture started", + 400: "Invalid request", + 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["vm_id"], project_id=request.match_info["project_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_number, pcap_file_path) + response.json({"pcap_file_path": pcap_file_path}) + + @Route.post( + 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_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 VirtualBox VM instance") + 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"]) + vm.stop_capture(int(request.match_info["adapter_number"])) + response.set_status(204) diff --git a/gns3server/handlers/api/vpcs_handler.py b/gns3server/handlers/api/vpcs_handler.py new file mode 100644 index 00000000..588ff50c --- /dev/null +++ b/gns3server/handlers/api/vpcs_handler.py @@ -0,0 +1,225 @@ +# -*- 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.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: + + """ + API entry points for VPCS. + """ + + @classmethod + @Route.post( + r"/projects/{project_id}/vpcs/vms", + parameters={ + "project_id": "UUID for the project" + }, + status_codes={ + 201: "Instance created", + 400: "Invalid request", + 409: "Conflict" + }, + 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"], + request.match_info["project_id"], + request.json.get("vm_id"), + console=request.json.get("console"), + startup_script=request.json.get("startup_script")) + response.set_status(201) + response.json(vm) + + @classmethod + @Route.get( + r"/projects/{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", + output=VPCS_OBJECT_SCHEMA) + def show(request, response): + + vpcs_manager = VPCS.instance() + vm = vpcs_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}/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" + }, + 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["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) + response.json(vm) + + @classmethod + @Route.delete( + r"/projects/{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") + def delete(request, response): + + yield from VPCS.instance().delete_vm(request.match_info["vm_id"]) + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{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 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"], project_id=request.match_info["project_id"]) + yield from vm.start() + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{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 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"], project_id=request.match_info["project_id"]) + yield from vm.stop() + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{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 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"], project_id=request.match_info["project_id"]) + yield from vm.reload() + response.set_status(204) + + @Route.post( + 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={ + 201: "NIO created", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Add a NIO to a VPCS instance", + input=VPCS_NIO_SCHEMA, + output=VPCS_NIO_SCHEMA) + def create_nio(request, response): + + vpcs_manager = VPCS.instance() + 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) + response.json(nio) + + @classmethod + @Route.delete( + 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={ + 204: "NIO deleted", + 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"], 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/handlers/auth_handler.py b/gns3server/handlers/auth_handler.py deleted file mode 100644 index 6db9f4ec..00000000 --- a/gns3server/handlers/auth_handler.py +++ /dev/null @@ -1,92 +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) \ No newline at end of file diff --git a/gns3server/handlers/file_upload_handler.py b/gns3server/handlers/file_upload_handler.py deleted file mode 100644 index 15673604..00000000 --- a/gns3server/handlers/file_upload_handler.py +++ /dev/null @@ -1,92 +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 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/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/upload_handler.py b/gns3server/handlers/upload_handler.py new file mode 100644 index 00000000..a3995f5a --- /dev/null +++ b/gns3server/handlers/upload_handler.py @@ -0,0 +1,86 @@ +# -*- 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 aiohttp +import stat + +from ..config import Config +from ..web.route import Route + + +class UploadHandler: + + @classmethod + @Route.get( + r"/upload", + description="Manage upload of GNS3 images", + api_version=None + ) + def index(request, response): + uploaded_files = [] + try: + for root, _, files in os.walk(UploadHandler.image_directory()): + for filename in files: + image_file = os.path.join(root, filename) + uploaded_files.append(image_file) + except OSError: + pass + 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( + r"/upload", + description="Manage upload of GNS3 images", + api_version=None + ) + def upload(request, response): + data = yield from request.post() + + if not data["file"]: + response.redirect("/upload") + return + + if data["type"] not in ["IOU", "IOURC", "QEMU", "IOS"]: + 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("~/") + 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: + 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: + 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("images_path", "~/GNS3/images")) 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..ac6c6b19 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,24 +19,19 @@ import os import datetime import sys -import multiprocessing import locale -import tornado.options +import argparse + 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 +from gns3server.crash_report import CrashReport import logging log = logging.getLogger(__name__) -# 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) - def locale_check(): """ @@ -50,7 +45,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 @@ -58,58 +53,107 @@ 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 main(): - """ - Entry point for GNS3 server +def parse_arguments(argv, config): """ + Parse command line arguments and override local configuration - if sys.platform.startswith("win"): - # necessary on Windows to freeze the application - multiprocessing.freeze_support() + :params args: Array of command line arguments + :params config: ConfigParser with default variable from configuration + """ - try: - tornado.options.parse_command_line() - except (tornado.options.Error, ValueError): - tornado.options.print_help() - raise SystemExit + defaults = { + "host": config.get("host", "0.0.0.0"), + "port": config.get("port", 8000), + "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), + "debug": config.getboolean("debug", False), + "live": config.getboolean("live", 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") + 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") + 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") + 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) + + +def set_config(args): + + config = Config.instance() + server_config = config.get_section_config("Server") + 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["record"] = args.record + 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) - from tornado.options import options - if options.version: - print(__version__) - raise SystemExit - current_year = datetime.date.today().year +def main(): + """ + Entry point for GNS3 server + """ - 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 + level = logging.INFO + args = parse_arguments(sys.argv[1:], Config.instance().get_section_config("Server")) + if args.debug: + level = logging.DEBUG + 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)) + 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"): + 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): raise RuntimeError("Python 3.3 or higher is required") @@ -118,19 +162,27 @@ 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: os.getcwd() except FileNotFoundError: - log.critical("the current working directory doesn't exist") + 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.run() + Project.clean_project_directory() + + CrashReport.instance() + host = server_config["host"] + port = int(server_config["port"]) + server = Server.instance(host, port) + try: + server.run() + except Exception as e: + log.critical("Critical error while running the server: {}".format(e), exc_info=1) + CrashReport.instance().capture_exception() + return if __name__ == '__main__': 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..4dce8ccc 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,14 @@ # 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 +from .dynamips import Dynamips +from .qemu import Qemu -MODULES = [DeadMan, Dynamips, VPCS, VirtualBox, 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/gns3server/modules/dynamips/schemas/__init__.py b/gns3server/modules/adapters/__init__.py similarity index 100% rename from gns3server/modules/dynamips/schemas/__init__.py rename to gns3server/modules/adapters/__init__.py diff --git a/gns3server/modules/qemu/adapters/adapter.py b/gns3server/modules/adapters/adapter.py similarity index 78% rename from gns3server/modules/qemu/adapters/adapter.py rename to gns3server/modules/adapters/adapter.py index cf439427..33c916c4 100644 --- a/gns3server/modules/qemu/adapters/adapter.py +++ b/gns3server/modules/adapters/adapter.py @@ -17,6 +17,7 @@ class Adapter(object): + """ Base class for adapters. @@ -28,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): """ @@ -41,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. @@ -49,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/qemu/adapters/ethernet_adapter.py b/gns3server/modules/adapters/ethernet_adapter.py similarity index 84% rename from gns3server/modules/qemu/adapters/ethernet_adapter.py rename to gns3server/modules/adapters/ethernet_adapter.py index 27426ec2..d5b5373f 100644 --- a/gns3server/modules/qemu/adapters/ethernet_adapter.py +++ b/gns3server/modules/adapters/ethernet_adapter.py @@ -19,13 +19,14 @@ from .adapter import Adapter class EthernetAdapter(Adapter): + """ - QEMU Ethernet adapter. + Ethernet adapter. """ - def __init__(self): - Adapter.__init__(self, interfaces=1) + def __init__(self, interfaces=1): + Adapter.__init__(self, interfaces=interfaces) def __str__(self): - return "QEMU Ethernet adapter" + return "Ethernet adapter" diff --git a/gns3server/modules/iou/adapters/serial_adapter.py b/gns3server/modules/adapters/serial_adapter.py similarity index 84% rename from gns3server/modules/iou/adapters/serial_adapter.py rename to gns3server/modules/adapters/serial_adapter.py index 9f2851a5..6a674c21 100644 --- a/gns3server/modules/iou/adapters/serial_adapter.py +++ b/gns3server/modules/adapters/serial_adapter.py @@ -19,13 +19,14 @@ from .adapter import Adapter class SerialAdapter(Adapter): + """ - IOU Serial adapter. + Serial adapter. """ - def __init__(self): - Adapter.__init__(self, interfaces=4) + def __init__(self, interfaces=1): + Adapter.__init__(self, interfaces=interfaces) def __str__(self): - return "IOU Serial adapter" + return "Serial adapter" diff --git a/gns3server/modules/attic.py b/gns3server/modules/attic.py deleted file mode 100644 index b059b532..00000000 --- a/gns3server/modules/attic.py +++ /dev/null @@ -1,146 +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 errno -import time - -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. - - :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(". - -""" -Base class (interface) for modules -""" - -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 - -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 - """ - - if not self.name in self.modules: - log.warn("no destinations found for module {}".format(self.name)) - return [] - return self.modules[self.name].keys() - - @classmethod - def route(cls, destination): - """ - Decorator to register a destination routed to a method - - :param destination: destination to be routed - """ - - 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 - - @property - def images_directory(self): - - return self._images_dir diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py new file mode 100644 index 00000000..c41463a7 --- /dev/null +++ b/gns3server/modules/base_manager.py @@ -0,0 +1,373 @@ +# -*- 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 sys +import os +import struct +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 +from .nios.nio_tap import NIOTAP +from .nios.nio_generic_ethernet import NIOGenericEthernet + + +class BaseManager: + + """ + Base class for all Manager. + Responsible of management of a VM pool + """ + + _convert_lock = None + + def __init__(self): + + BaseManager._convert_lock = asyncio.Lock() + self._vms = {} + self._port_manager = None + self._config = Config.instance() + + @classmethod + def instance(cls): + """ + Singleton to return only one instance of BaseManager. + + :returns: instance of BaseManager + """ + + if not hasattr(cls, "_instance") or cls._instance is None: + 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): + """ + Returns the port manager. + + :returns: Port manager + """ + + return self._port_manager + + @port_manager.setter + def port_manager(self, new_port_manager): + + self._port_manager = new_port_manager + + @property + def config(self): + """ + Returns the server config. + + :returns: Config + """ + + return self._config + + @asyncio.coroutine + def unload(self): + + tasks = [] + for vm_id in self._vms.keys(): + 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 + log.debug("Module {} unloaded".format(self.module_name)) + + 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="VM ID {} is not a valid UUID".format(vm_id)) + + if vm_id not in self._vms: + 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 vm + + @asyncio.coroutine + def convert_old_project(self, project, legacy_id, name): + """ + Convert projects made before version 1.3 + + :param project: Project instance + :param legacy_id: old identifier + :param name: node name + + :returns: new identifier + """ + + new_id = str(uuid4()) + 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 + 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)) + + if project.is_local() is False: + 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_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 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 + 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(), new_id) + 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 + + @asyncio.coroutine + def create_vm(self, name, project_id, vm_id, *args, **kwargs): + """ + Create a new VM + + :param name: VM name + :param project_id: Project identifier + :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): + vm_id = yield from self.convert_old_project(project, vm_id, name) + + if not vm_id: + vm_id = str(uuid4()) + + 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.id] = vm + project.add_vm(vm) + return vm + + @asyncio.coroutine + def close_vm(self, vm_id): + """ + Delete a VM + + :param vm_id: VM identifier + + :returns: VM instance + """ + + vm = self.get_vm(vm_id) + if asyncio.iscoroutinefunction(vm.close): + yield from vm.close() + else: + 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): + """ + Called when a project is closed. + + :param project: Project instance + """ + + for vm in project.vms: + if vm.id in self._vms: + del self._vms[vm.id] + + @asyncio.coroutine + def project_moved(self, project): + """ + Called when a project is moved + + :param project: project instance + """ + + 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): + """ + Delete a VM. VM working directory will be destroy when + we receive a commit. + + :param vm_id: VM identifier + :returns: VM instance + """ + + vm = yield from self.close_vm(vm_id) + vm.project.mark_vm_for_destruction(vm) + del self._vms[vm.id] + return vm + + @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 os +import logging +import aiohttp +import shutil +import asyncio +import tempfile + +from ..utils.asyncio import wait_run_in_executor +from .vm_error import VMError + +log = logging.getLogger(__name__) + + +class BaseVM: + + """ + 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 + self._temporary_directory = None + + if self._console is not None: + 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._project) + + 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): + + 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): + """ + Returns the VM current project. + + :returns: Project instance. + """ + + return self._project + + @property + def name(self): + """ + Returns the name for this VM. + + :returns: name + """ + + return self._name + + @name.setter + def name(self, new_name): + """ + Sets the name of this VM. + + :param new_name: 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 id(self): + """ + Returns the ID for this VM. + + :returns: VM identifier (string) + """ + + return self._id + + @property + def manager(self): + """ + Returns the manager for this VM. + + :returns: instance of manager + """ + + return self._manager + + @property + def working_dir(self): + """ + Return VM working directory + """ + + return self._project.vm_working_directory(self) + + @property + def temporary_directory(self): + if self._temporary_directory is None: + 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): + """ + Creates the VM. + """ + + log.info("{module}: {name} [{id}] created".format(module=self.manager.module_name, + name=self.name, + id=self.id)) + + @asyncio.coroutine + def delete(self): + """ + Delete the VM (including all its files). + """ + + directory = self.project.vm_working_directory(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. + """ + + raise NotImplementedError + + def stop(self): + """ + Starts the VM process. + """ + + raise NotImplementedError + + def close(self): + """ + Close the VM process. + """ + + 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_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, + id=self.id, + port=console)) 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 diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index b04aeaa5..b8053c3e 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__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 @@ -19,36 +19,42 @@ Dynamips server module. """ +import aiohttp 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 +import time +import asyncio +import tempfile +import glob +import logging -from .hypervisor import Hypervisor -from .hypervisor_manager import HypervisorManager -from .dynamips_error import DynamipsError +log = logging.getLogger(__name__) -# Nodes +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 +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 .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 +from .dynamips_vm import DynamipsVM +from .dynamips_device import DynamipsDevice + +# NIOs +from .nios.nio_udp import NIOUDP +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 # Adapters from .adapters.c7200_io_2fe import C7200_IO_2FE @@ -68,359 +74,312 @@ 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 .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__) +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(IModule): - """ - Dynamips module. +class Dynamips(BaseManager): - :param name: module name - :param args: arguments for the module - :param kwargs: named arguments for the module - """ + _VM_CLASS = DynamipsVM + _DEVICE_CLASS = DynamipsDevice + _ghost_ios_lock = None - def __init__(self, name, *args, **kwargs): + def __init__(self): - # 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: + super().__init__() + Dynamips._ghost_ios_lock = asyncio.Lock() + self._devices = {} + self._ghost_files = set() + self._dynamips_path = None + + @asyncio.coroutine + def unload(self): + + yield from BaseManager.unload(self) + + 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: - 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: + future.result() + except Exception as e: + log.error("Could not stop device hypervisor {}".format(e), exc_info=1) 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): + @asyncio.coroutine + def project_closing(self, project): """ - Properly stops the module. + Called when a project is about to be closed. - :param signum: signal number (if called by the signal handler) + :param project: Project instance """ - 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 + yield from super().project_closing(project) + # delete the Dynamips devices corresponding to the project + tasks = [] + for device in self._devices.values(): + if device.project.id == project.id: + tasks.append(asyncio.async(device.delete())) - # stop all Dynamips hypervisors - if self._hypervisor_manager: - self._hypervisor_manager.stop_all_hypervisors() + 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) - self.delete_dynamips_files() - IModule.stop(self, signum) # this will stop the I/O loop + @asyncio.coroutine + def project_closed(self, project): + """ + Called when a project is closed. - def _check_hypervisors(self): + :param project: Project instance """ - Periodic callback to check if Dynamips hypervisors are running. - Sends a notification to the client if not. + yield from super().project_closed(project) + # delete useless Dynamips files + 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_*")) + 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 + + @asyncio.coroutine + def project_moved(self, project): """ + Called when a project is moved. - 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): + :param project: Project instance """ - Returns a device instance. - :param device_id: device identifier - :param instance_dict: dictionary containing the instances + 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())) - :returns: device instance + @asyncio.coroutine + def project_committed(self, project): """ + Called when a project has been committed. - 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] + :param project: Project instance + """ - def delete_dynamips_files(self): + # 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): """ - Deletes useless Dynamips files from the working directory + Returns the path to Dynamips. + + :returns: path """ - 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 + return self._dynamips_path - @IModule.route("dynamips.reset") - def reset(self, request=None): + @asyncio.coroutine + def create_device(self, name, project_id, device_id, device_type, *args, **kwargs): """ - Resets the module (JSON-RPC notification). + Create a new Dynamips device. - :param request: JSON request (not used) + :param name: Device name + :param project_id: Project identifier + :param vm_id: restore a VM identifier """ - # 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 + 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) - # 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. + 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 + 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 - # check if Dynamips path exists - if not os.path.isfile(self._dynamips): - raise DynamipsError("Dynamips executable {} doesn't exist".format(self._dynamips)) + :returns: Device instance + """ - # check if Dynamips is executable - if not os.access(self._dynamips, os.X_OK): - raise DynamipsError("Dynamips {} is not executable".format(self._dynamips)) + if project_id: + # check the project_id exists + project = ProjectManager.instance().get_project(project_id) - 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)) + UUID(device_id, version=4) + except ValueError: + raise aiohttp.web.HTTPBadRequest(text="Device ID} is not a valid UUID".format(device_id)) - # check if the working directory is writable - if not os.access(workdir, os.W_OK): - raise DynamipsError("Cannot write to working directory {}".format(workdir)) + if device_id not in self._devices: + raise aiohttp.web.HTTPNotFound(text="Device ID {} doesn't exist".format(device_id)) - 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) + 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)) - 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) + return device - @IModule.route("dynamips.settings") - def settings(self, request): + @asyncio.coroutine + def delete_device(self, device_id): """ - Set or update settings. + Delete a device - Optional request parameters: - - path (path to the Dynamips executable) - - working_dir (path to a working directory) - - project_name + :param device_id: Device identifier - :param request: JSON request + :returns: Device instance """ - if request is None: - self.send_param_error() - return + device = self.get_device(device_id) + yield from device.delete() + del self._devices[device.id] + return device - log.debug("received request {}".format(request)) + def find_dynamips(self): - #TODO: JSON schema validation - if not self._hypervisor_manager: + # look for Dynamips + dynamips_path = self.config.get_section_config("Dynamips").get("dynamips_path") + if not dynamips_path: + dynamips_path = shutil.which("dynamips") - if "path" in request: - self._dynamips = request.pop("path") + 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") - 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._dynamips_path = dynamips_path + return dynamips_path - self._hypervisor_manager_settings = request + @asyncio.coroutine + def start_new_hypervisor(self, working_dir=None): + """ + Creates a new Dynamips process and start it. - 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") + :param working_dir: working directory - try: - self._hypervisor_manager.working_dir = new_working_dir - except DynamipsError as e: - log.error("could not change working directory: {}".format(e)) - return + :returns: the new hypervisor instance + """ - self._working_dir = new_working_dir + if not self._dynamips_path: + self.find_dynamips() - # 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) + if not working_dir: + working_dir = tempfile.gettempdir() - @IModule.route("dynamips.echo") - def echo(self, request): - """ - Echo end point for testing purposes. + # FIXME: hypervisor should always listen to 127.0.0.1 + # See https://github.com/GNS3/dynamips/issues/62 + server_config = self.config.get_section_config("Server") + server_host = server_config.get("host") - :param request: JSON request - """ + 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((server_host, 0)) + port = sock.getsockname()[1] + except OSError as e: + raise DynamipsError("Could not find free port for the Dynamips hypervisor: {}".format(e)) - if request is None: - self.send_param_error() - else: - log.debug("received request {}".format(request)) - self.send_response(request) + 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() + 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 + + @asyncio.coroutine + def ghost_ios_support(self, vm): - def create_nio(self, node, request): + 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) + + @asyncio.coroutine + def create_nio(self, node, nio_settings): """ Creates a new NIO. - :param node: node requesting the NIO - :param request: the original request with the - necessary information to create the NIO + :param node: Dynamips node instance + :param nio_settings: 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"] + if nio_settings["type"] == "nio_udp": + lport = nio_settings["lport"] + 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: 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"] + nio = NIOUDP(node.hypervisor, lport, 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() @@ -432,146 +391,228 @@ class Dynamips(IModule): 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": + 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 = 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) + 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) + + yield from nio.create() return nio - def allocate_udp_port(self, node): + @asyncio.coroutine + def _set_ghost_ios(self, vm): """ - Allocates a UDP port in order to create an UDP NIO. - - :param node: the node that needs to allocate an UDP port + Manages Ghost IOS support. - :returns: dictionary with the allocated host/port info + :param vm: VM instance """ - port = node.hypervisor.allocate_udp_port() - host = node.hypervisor.host + 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 supported 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: + # 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) + try: + yield from ghost.create() + yield from ghost.set_image(vm.image) + 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)) - log.info("{} [id={}] has allocated UDP port {} with host {}".format(node.name, - node.id, - port, - host)) - response = {"lport": port} - return response + 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) - def set_ghost_ios(self, router): + @asyncio.coroutine + def update_vm_settings(self, vm, settings): """ - Manages Ghost IOS support. + Updates the VM settings. - :param router: Router instance + :param vm: VM instance + :param settings: settings to update (dict) """ - if not router.mmap: - raise DynamipsError("mmap support is required to enable ghost IOS support") + 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)) + yield from 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 not isinstance(vm.slots[slot_id], type(adapter)): + yield from vm.slot_remove_binding(slot_id) + 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]) + 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 not isinstance(vm.slots[0].wics[wic_slot_id], type(wic)): + yield from vm.uninstall_wic(wic_slot_id) + 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]) + 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.set_vm_configs(vm, settings) + + @asyncio.coroutine + def set_vm_configs(self, vm, settings): + """ + Set VM configs from pushed content or existing config files. - ghost_instance = router.formatted_ghost_file() - all_ghosts = [] + :param vm: VM instance + :param settings: VM settings + """ - # search of an existing ghost instance across all hypervisors - for hypervisor in self._hypervisor_manager.hypervisors: - all_ghosts.extend(hypervisor.ghosts) + 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 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 + 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_configs(startup_config_path) + else: + 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") + if private_config_path: + yield from vm.set_configs(vm.startup_config, private_config_path) - def create_config_from_file(self, local_base_config, router, destination_config_path): + def _create_config(self, vm, content, path): """ - Creates a config file from a local base config + Creates a config file. - :param local_base_config: path the a local base config - :param router: router instance - :param destination_config_path: path to the destination 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 {} from {}".format(destination_config_path, local_base_config)) - config_path = destination_config_path - config_dir = os.path.dirname(destination_config_path) + 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) - except FileExistsError: - pass + os.makedirs(config_dir, exist_ok=True) except OSError as e: - raise DynamipsError("Could not create configs directory: {}".format(e)) + raise DynamipsError("Could not create Dynamips 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) + with open(path, "w") as f: + f.write(content) 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. + raise DynamipsError("Could not create config file {}: {}".format(path, e)) - :param config_base64: base64 encoded config - :param router: router instance - :param destination_config_path: path to the destination config file + return os.path.join("configs", os.path.basename(path)) - :returns: relative path to the created config file + @asyncio.coroutine + def auto_idlepc(self, vm): """ + Try to find the best possible idle-pc value. - 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)) + :param vm: VM instance + """ - config_path = destination_config_path + yield from vm.set_idlepc("0x0") + was_auto_started = False 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) + 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 diff --git a/gns3server/modules/dynamips/adapters/adapter.py b/gns3server/modules/dynamips/adapters/adapter.py index d963933e..9dd61619 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. @@ -27,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): @@ -43,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. @@ -51,7 +51,7 @@ class Adapter(object): False otherwise. """ - if port_id in self._ports: + if port_number in self._ports: return True return False @@ -83,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): """ @@ -101,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/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/atmsw.py b/gns3server/modules/dynamips/backends/atmsw.py deleted file mode 100644 index 2ce0410b..00000000 --- a/gns3server/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/modules/dynamips/backends/ethhub.py b/gns3server/modules/dynamips/backends/ethhub.py deleted file mode 100644 index 97c9df7f..00000000 --- a/gns3server/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/modules/dynamips/backends/ethsw.py b/gns3server/modules/dynamips/backends/ethsw.py deleted file mode 100644 index e251e158..00000000 --- a/gns3server/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/modules/dynamips/backends/frsw.py b/gns3server/modules/dynamips/backends/frsw.py deleted file mode 100644 index ed63f501..00000000 --- a/gns3server/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/modules/dynamips/backends/vm.py b/gns3server/modules/dynamips/backends/vm.py deleted file mode 100644 index 8348a231..00000000 --- a/gns3server/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 type(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 type(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: - 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/modules/dynamips/dynamips_device.py b/gns3server/modules/dynamips/dynamips_device.py new file mode 100644 index 00000000..e8398c94 --- /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 = {'atm_switch': ATMSwitch, + 'frame_relay_switch': FrameRelaySwitch, + 'ethernet_switch': EthernetSwitch, + 'ethernet_hub': EthernetHub} + + +class DynamipsDevice: + + """ + Factory to create an Device object based on the type + """ + + def __new__(cls, name, device_id, project, manager, device_type, **kwargs): + + if device_type not in DEVICES: + raise DynamipsError("Unknown device type: {}".format(device_type)) + + return DEVICES[device_type](name, device_id, project, manager, **kwargs) diff --git a/gns3server/modules/dynamips/dynamips_error.py b/gns3server/modules/dynamips/dynamips_error.py index 332c6bbb..265b22e6 100644 --- a/gns3server/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 @@ -19,21 +19,9 @@ Custom exceptions for Dynamips module. """ +from ..vm_error import VMError -class DynamipsError(Exception): - def __init__(self, message, original_exception=None): +class DynamipsError(VMError): - 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/dynamips/dynamips_hypervisor.py b/gns3server/modules/dynamips/dynamips_hypervisor.py index 7c93f775..2b0401d7 100644 --- a/gns3server/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 @@ -20,16 +20,18 @@ Interface for Dynamips hypervisor management module ("hypervisor") http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L46 """ -import socket import re +import time 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) @@ -50,22 +52,16 @@ class DynamipsHypervisor(object): 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 - self._socket = None self._uuid = None + self._reader = None + self._writer = None + self._io_lock = asyncio.Lock() - def connect(self): + @asyncio.coroutine + def connect(self, timeout=10): """ Connects to the hypervisor. """ @@ -79,20 +75,34 @@ class DynamipsHypervisor(object): else: host = self._host - try: - self._socket = socket.create_connection((host, self._port), self._timeout) - except OSError as e: - raise DynamipsError("Could not connect to server: {}".format(e)) + 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.open_connection(host, self._port) + 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, self._port, last_exception)) + else: + log.info("Connected to Dynamips hypervisor after {:.4f} seconds".format(time.time() - begin)) 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): @@ -104,66 +114,44 @@ 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 - self._nio_udp_auto_instances.clear() + 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): """ Resets this hypervisor (used to get an empty configuration). """ - 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 + yield from self.send("hypervisor reset") - @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. @@ -171,19 +159,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): @@ -195,17 +183,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): """ @@ -216,234 +193,47 @@ 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 - - @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 jitsharing_groups(self): + def port(self): """ - Returns a list of the JIT sharing groups hosted by this hypervisor. + Returns the port used to start the hypervisor. - :returns: JIT sharing groups dict (image_name -> group number) + :returns: port number (integer) """ - return self._jitsharing_groups + return self._port - def add_jitsharing_group(self, image_name, group_number): + @port.setter + def port(self, port): """ - Adds a JIT blocks sharing group name to the list of groups created on this hypervisor. + Sets the port used to start the hypervisor. - :param image_name: name of the ghost image - :param group_number: group (integer) + :param port: port number (integer) """ - self._jitsharing_groups[image_name] = group_number + 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): - """ - Returns this hypervisor port. - - :returns: port (integer) - """ - - return self._port - - 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) + @host.setter + def host(self, host): """ + Sets the host (binding) used to start the hypervisor. - # 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) - self._nio_udp_auto_instances[nio.lport] = nio - 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) + :param host: host/address (string) """ - result = self.send(string) - return result + self._host = host + @asyncio.coroutine def send(self, command): """ Sends commands to this hypervisor. @@ -466,60 +256,60 @@ 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: - 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.socket.sendall(command.encode('utf-8')) - 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 = self.socket.recv(1024) # match to Dynamips' buffer size - buf += chunk.decode("utf-8") + 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 = '' - - 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:]) - - # 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': + 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("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() - 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 diff --git a/gns3server/modules/dynamips/dynamips_vm.py b/gns3server/modules/dynamips/dynamips_vm.py new file mode 100644 index 00000000..bcdd5636 --- /dev/null +++ b/gns3server/modules/dynamips/dynamips_vm.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 . + + +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, dynamips_id, platform, **kwargs): + + if platform not in PLATFORMS: + raise DynamipsError("Unknown router platform: {}".format(platform)) + + return PLATFORMS[platform](name, vm_id, project, manager, dynamips_id, **kwargs) diff --git a/gns3server/modules/dynamips/hypervisor.py b/gns3server/modules/dynamips/hypervisor.py index 88c577d7..ce1b4ade 100644 --- a/gns3server/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,18 +32,20 @@ log = logging.getLogger(__name__) class Hypervisor(DynamipsHypervisor): + """ Hypervisor. :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 = 0 + _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) @@ -51,17 +53,13 @@ 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 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): """ @@ -102,99 +100,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. @@ -202,38 +108,39 @@ 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(): - DynamipsHypervisor.stop(self) - log.info("stopping Dynamips PID={}".format(self._process.pid)) + log.info("Stopping Dynamips process PID={}".format(self._process.pid)) + 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) 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: - self._process.kill() - if self._process.poll() is None: - log.warn("Dynamips process {} is still running".format(self._process.pid)) + yield from asyncio.wait_for(self._process.wait(), timeout=3) + except asyncio.TimeoutError: + if self._process.returncode is None: + 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: @@ -264,7 +171,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 @@ -276,8 +183,11 @@ 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 - if self._host != "0.0.0.0" and self._host != "::": + command.extend(["-l", "dynamips_i{}_log.txt".format(self._id)]) # log file + # 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)]) diff --git a/gns3server/modules/dynamips/hypervisor_manager.py b/gns3server/modules/dynamips/hypervisor_manager.py deleted file mode 100644 index 3106d98c..00000000 --- a/gns3server/modules/dynamips/hypervisor_manager.py +++ /dev/null @@ -1,654 +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/modules/dynamips/nios/nio.py b/gns3server/modules/dynamips/nios/nio.py index 1fd61bf9..2f978f19 100644 --- a/gns3server/modules/dynamips/nios/nio.py +++ b/gns3server/modules/dynamips/nios/nio.py @@ -20,22 +20,25 @@ 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 = 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 @@ -43,6 +46,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. @@ -50,18 +54,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 @@ -69,13 +76,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. @@ -83,9 +89,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. @@ -100,9 +106,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 @@ -112,6 +118,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. @@ -123,8 +130,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 @@ -134,6 +141,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. @@ -156,9 +164,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 @@ -188,6 +196,7 @@ class NIO(object): return self._output_filter, self._output_filter_options + @asyncio.coroutine def get_stats(self): """ Gets statistics for this NIO. @@ -195,25 +204,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): @@ -225,6 +225,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/modules/dynamips/nios/nio_fifo.py b/gns3server/modules/dynamips/nios/nio_fifo.py index a67f863d..fd10e40f 100644 --- a/gns3server/modules/dynamips/nios/nio_fifo.py +++ b/gns3server/modules/dynamips/nios/nio_fifo.py @@ -19,13 +19,15 @@ Interface for FIFO NIOs. """ +import asyncio from .nio import NIO import logging log = logging.getLogger(__name__) -class NIO_FIFO(NIO): +class NIOFIFO(NIO): + """ Dynamips FIFO NIO. @@ -36,16 +38,11 @@ class NIO_FIFO(NIO): def __init__(self, hypervisor): - NIO.__init__(self, hypervisor) - - # create an unique ID - self._id = NIO_FIFO._instance_count - 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)) + # create an unique ID and name + nio_id = NIOFIFO._instance_count + NIOFIFO._instance_count += 1 + name = 'nio_fifo' + str(nio_id) + NIO.__init__(name, self, hypervisor) @classmethod def reset(cls): @@ -55,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. @@ -62,7 +66,11 @@ 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)) + + 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 58e6ec7f..9690237a 100644 --- a/gns3server/modules/dynamips/nios/nio_generic_ethernet.py +++ b/gns3server/modules/dynamips/nios/nio_generic_ethernet.py @@ -19,13 +19,15 @@ Interface for generic Ethernet NIOs (PCAP library). """ +import asyncio from .nio import NIO import logging log = logging.getLogger(__name__) -class NIO_GenericEthernet(NIO): +class NIOGenericEthernet(NIO): + """ Dynamips generic Ethernet NIO. @@ -37,19 +39,12 @@ class NIO_GenericEthernet(NIO): def __init__(self, hypervisor, ethernet_device): - NIO.__init__(self, hypervisor) - - # create an unique ID - self._id = NIO_GenericEthernet._instance_count - NIO_GenericEthernet._instance_count += 1 - self._name = 'nio_gen_eth' + str(self._id) + # create an unique ID and name + nio_id = NIOGenericEthernet._instance_count + NIOGenericEthernet._instance_count += 1 + name = 'nio_gen_eth' + str(nio_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)) + NIO.__init__(self, name, hypervisor) @classmethod def reset(cls): @@ -59,6 +54,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): """ @@ -68,3 +72,8 @@ class NIO_GenericEthernet(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 a199c264..f121dbb6 100644 --- a/gns3server/modules/dynamips/nios/nio_linux_ethernet.py +++ b/gns3server/modules/dynamips/nios/nio_linux_ethernet.py @@ -19,13 +19,15 @@ Interface for Linux Ethernet NIOs (Linux only). """ +import asyncio from .nio import NIO import logging log = logging.getLogger(__name__) -class NIO_LinuxEthernet(NIO): +class NIOLinuxEthernet(NIO): + """ Dynamips Linux Ethernet NIO. @@ -37,19 +39,12 @@ class NIO_LinuxEthernet(NIO): def __init__(self, hypervisor, ethernet_device): - NIO.__init__(self, hypervisor) - - # create an unique ID - self._id = NIO_LinuxEthernet._instance_count - NIO_LinuxEthernet._instance_count += 1 - self._name = 'nio_linux_eth' + str(self._id) + # create an unique ID and name + nio_id = NIOLinuxEthernet._instance_count + NIOLinuxEthernet._instance_count += 1 + name = 'nio_linux_eth' + str(nio_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)) + NIO.__init__(self, name, hypervisor) @classmethod def reset(cls): @@ -59,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): """ @@ -68,3 +72,8 @@ class NIO_LinuxEthernet(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 4d939d5a..95a9f2ae 100644 --- a/gns3server/modules/dynamips/nios/nio_mcast.py +++ b/gns3server/modules/dynamips/nios/nio_mcast.py @@ -19,13 +19,15 @@ Interface for multicast NIOs. """ +import asyncio from .nio import NIO import logging log = logging.getLogger(__name__) -class NIO_Mcast(NIO): +class NIOMcast(NIO): + """ Dynamips Linux Ethernet NIO. @@ -38,23 +40,14 @@ class NIO_Mcast(NIO): def __init__(self, hypervisor, group, port): - NIO.__init__(self, hypervisor) - - # create an unique ID - self._id = NIO_Mcast._instance_count - NIO_Mcast._instance_count += 1 - self._name = 'nio_mcast' + str(self._id) + # create an unique ID and name + nio_id = NIOMcast._instance_count + NIOMcast._instance_count += 1 + name = 'nio_mcast' + str(nio_id) self._group = group 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)) + NIO.__init__(self, name, hypervisor) @classmethod def reset(cls): @@ -64,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): """ @@ -94,14 +98,19 @@ 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 + + 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 b9350c07..e36fb0e5 100644 --- a/gns3server/modules/dynamips/nios/nio_null.py +++ b/gns3server/modules/dynamips/nios/nio_null.py @@ -19,13 +19,15 @@ Interface for dummy NIOs (mostly for tests). """ +import asyncio from .nio import NIO import logging log = logging.getLogger(__name__) -class NIO_Null(NIO): +class NIONull(NIO): + """ Dynamips NULL NIO. @@ -36,15 +38,11 @@ class NIO_Null(NIO): def __init__(self, hypervisor): - NIO.__init__(self, hypervisor) - - # create an unique ID - self._id = NIO_Null._instance_count - 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)) + # create an unique ID and name + nio_id = NIONull._instance_count + NIONull._instance_count += 1 + name = 'nio_null' + str(nio_id) + NIO.__init__(self, name, hypervisor) @classmethod def reset(cls): @@ -53,3 +51,13 @@ 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)) + + 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 9ee16abb..0e3b5683 100644 --- a/gns3server/modules/dynamips/nios/nio_tap.py +++ b/gns3server/modules/dynamips/nios/nio_tap.py @@ -19,13 +19,15 @@ Interface for TAP NIOs (UNIX based OSes only). """ +import asyncio from .nio import NIO import logging log = logging.getLogger(__name__) -class NIO_TAP(NIO): +class NIOTAP(NIO): + """ Dynamips TAP NIO. @@ -37,19 +39,12 @@ class NIO_TAP(NIO): def __init__(self, hypervisor, tap_device): - NIO.__init__(self, hypervisor) - - # create an unique ID - self._id = NIO_TAP._instance_count - NIO_TAP._instance_count += 1 - self._name = 'nio_tap' + str(self._id) + # create an unique ID and name + nio_id = NIOTAP._instance_count + NIOTAP._instance_count += 1 + name = 'nio_tap' + str(nio_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)) + NIO.__init__(self, name, hypervisor) @classmethod def reset(cls): @@ -59,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): """ @@ -68,3 +69,8 @@ class NIO_TAP(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 bcfd9e4d..c7016e6a 100644 --- a/gns3server/modules/dynamips/nios/nio_udp.py +++ b/gns3server/modules/dynamips/nios/nio_udp.py @@ -19,13 +19,15 @@ Interface for UDP NIOs. """ +import asyncio from .nio import NIO import logging log = logging.getLogger(__name__) -class NIO_UDP(NIO): +class NIOUDP(NIO): + """ Dynamips UDP NIO. @@ -39,25 +41,14 @@ class NIO_UDP(NIO): def __init__(self, hypervisor, lport, rhost, rport): - NIO.__init__(self, hypervisor) - - # create an unique ID - self._id = NIO_UDP._instance_count - NIO_UDP._instance_count += 1 - self._name = 'nio_udp' + str(self._id) + # create an unique ID and name + nio_id = NIOUDP._instance_count + NIOUDP._instance_count += 1 + name = 'nio_udp' + str(nio_id) self._lport = lport 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)) + NIO.__init__(self, name, hypervisor) @classmethod def reset(cls): @@ -67,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): """ @@ -96,3 +100,10 @@ class NIO_UDP(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 deleted file mode 100644 index ccefce2b..00000000 --- a/gns3server/modules/dynamips/nios/nio_udp_auto.py +++ /dev/null @@ -1,126 +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. -""" - -from .nio import NIO - -import logging -log = logging.getLogger(__name__) - - -class NIO_UDP_auto(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): - - NIO.__init__(self, hypervisor) - - # create an unique ID - self._id = NIO_UDP_auto._instance_count - NIO_UDP_auto._instance_count += 1 - 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._raddr = None - self._rport = None - - @classmethod - def reset(cls): - """ - Reset the instance count. - """ - - cls._instance_count = 0 - - @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 - - def connect(self, raddr, rport): - """ - Connects this NIO to a remote socket - - :param raddr: remote address - :param rport: remote port number - """ - - 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)) diff --git a/gns3server/modules/dynamips/nios/nio_unix.py b/gns3server/modules/dynamips/nios/nio_unix.py index f699eead..dddfaf82 100644 --- a/gns3server/modules/dynamips/nios/nio_unix.py +++ b/gns3server/modules/dynamips/nios/nio_unix.py @@ -19,13 +19,15 @@ Interface for UNIX NIOs (Unix based OSes only). """ +import asyncio from .nio import NIO import logging log = logging.getLogger(__name__) -class NIO_UNIX(NIO): +class NIOUNIX(NIO): + """ Dynamips UNIX NIO. @@ -38,22 +40,13 @@ class NIO_UNIX(NIO): def __init__(self, hypervisor, local_file, remote_file): - NIO.__init__(self, hypervisor) - - # create an unique ID - self._id = NIO_UNIX._instance_count - NIO_UNIX._instance_count += 1 - self._name = 'nio_unix' + str(self._id) + # create an unique ID and name + nio_id = NIOUNIX._instance_count + NIOUNIX._instance_count += 1 + name = 'nio_unix' + str(nio_id) 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)) + NIO.__init__(self, name, hypervisor) @classmethod def reset(cls): @@ -63,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): """ @@ -82,3 +86,9 @@ class NIO_UNIX(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 79af96d7..c62ce2ca 100644 --- a/gns3server/modules/dynamips/nios/nio_vde.py +++ b/gns3server/modules/dynamips/nios/nio_vde.py @@ -19,13 +19,15 @@ Interface for VDE (Virtual Distributed Ethernet) NIOs (Unix based OSes only). """ +import asyncio from .nio import NIO import logging log = logging.getLogger(__name__) -class NIO_VDE(NIO): +class NIOVDE(NIO): + """ Dynamips VDE NIO. @@ -38,22 +40,13 @@ class NIO_VDE(NIO): def __init__(self, hypervisor, control_file, local_file): - NIO.__init__(self, hypervisor) - - # create an unique ID - self._id = NIO_VDE._instance_count - NIO_VDE._instance_count += 1 - self._name = 'nio_vde' + str(self._id) + # create an unique ID and name + nio_id = NIOVDE._instance_count + NIOVDE._instance_count += 1 + 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): @@ -63,6 +56,17 @@ class NIO_VDE(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): """ @@ -82,3 +86,9 @@ class NIO_VDE(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/atm_bridge.py b/gns3server/modules/dynamips/nodes/atm_bridge.py deleted file mode 100644 index 10abe1b2..00000000 --- a/gns3server/modules/dynamips/nodes/atm_bridge.py +++ /dev/null @@ -1,171 +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/modules/dynamips/nodes/atm_switch.py b/gns3server/modules/dynamips/nodes/atm_switch.py index 0c382c44..4064ad07 100644 --- a/gns3server/modules/dynamips/nodes/atm_switch.py +++ b/gns3server/modules/dynamips/nodes/atm_switch.py @@ -20,112 +20,67 @@ Interface for Dynamips virtual ATM switch module ("atmsw"). http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L593 """ -import os +import asyncio +import re + +from .device import Device +from ..nios.nio_udp import NIOUDP from ..dynamips_error import DynamipsError import logging log = logging.getLogger(__name__) -class ATMSwitch(object): +class ATMSwitch(Device): + """ Dynamips ATM switch. - :param hypervisor: Dynamips hypervisor instance :param name: name for this switch + :param device_id: Device instance identifier + :param project: Project instance + :param manager: Parent VM Manager + :param hypervisor: Dynamips hypervisor instance """ - _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 + def __init__(self, name, device_id, project, manager, hypervisor=None): - 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) + super().__init__(name, device_id, project, manager, hypervisor) self._nios = {} - self._mapping = {} - - @classmethod - def reset(cls): - """ - Resets the instance count and the allocated instances list. - """ + self._mappings = {} - cls._instances.clear() + def __json__(self): - @property - def id(self): - """ - Returns the unique ID for this ATM switch. + return {"name": self.name, + "device_id": self.id, + "project_id": self.project.id, + "mappings": self._mappings} - :returns: id (integer) - """ + @asyncio.coroutine + def create(self): - return self._id + if self._hypervisor is None: + 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) - @property - def name(self): - """ - Returns the current name of this ATM switch. - - :returns: ATM switch name - """ - - return self._name[1:-1] # remove quotes + 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) - @name.setter - def name(self, new_name): + @asyncio.coroutine + def set_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)) - + 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 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): """ @@ -137,26 +92,34 @@ class ATMSwitch(object): 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): """ Deletes this ATM switch. """ - self._hypervisor.send("atmsw delete {}".format(self._name)) + for nio in self._nios.values(): + if nio and isinstance(nio, NIOUDP): + self.manager.port_manager.release_udp_port(nio.lport, self._project) - 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) + 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)) + 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() def has_port(self, port): """ @@ -169,43 +132,76 @@ class ATMSwitch(object): return True return False - def add_nio(self, nio, port): + def add_nio(self, nio, port_number): """ Adds a NIO as new port on ATM switch. :param nio: NIO instance to add - :param port: port to allocate for the NIO + :param port_number: port to allocate for the NIO """ - if port in self._nios: - raise DynamipsError("Port {} isn't free".format(port)) + 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)) + 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] = nio + self._nios[port_number] = nio - def remove_nio(self, port): + def remove_nio(self, port_number): """ Removes the specified NIO as member of this ATM switch. - :param port: allocated port + :param port_number: allocated port number """ - if port not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port)) + if port_number not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port_number)) - 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)) + nio = self._nios[port_number] + if isinstance(nio, NIOUDP): + 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, + port=port_number)) - del self._nios[port] + 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.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) + 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): """ Creates a new Virtual Path connection. @@ -225,21 +221,22 @@ class ATMSwitch(object): 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)) + 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={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)) + 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) + self._mappings[(port1, vpi1)] = (port2, vpi2) + @asyncio.coroutine def unmap_vp(self, port1, vpi1, port2, vpi2): """ Deletes a new Virtual Path connection. @@ -259,21 +256,22 @@ class ATMSwitch(object): 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)) + 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={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)) + 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)] + del self._mappings[(port1, vpi1)] + @asyncio.coroutine def map_pvc(self, port1, vpi1, vci1, port2, vpi2, vci2): """ Creates a new Virtual Channel connection (unidirectional). @@ -295,25 +293,26 @@ class ATMSwitch(object): 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) - + 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._mappings[(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). @@ -335,71 +334,66 @@ class ATMSwitch(object): 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"): + 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._mappings[(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: allocated port + :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 not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port)) + if port_number not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port_number)) - nio = self._nios[port] + 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)) + raise DynamipsError("Port {} has already a filter applied".format(port_number)) - try: - os.makedirs(os.path.dirname(output_file)) - except FileExistsError: - pass - 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)) - nio.bind_filter("both", "capture") - 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, + port=port_number)) - 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): + @asyncio.coroutine + def stop_capture(self, port_number): """ Stops a packet capture. - :param port: allocated port + :param port_number: allocated port number """ - if port not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port)) + if port_number not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port_number)) - 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)) + nio = self._nios[port_number] + yield from nio.unbind_filter("both") + 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/bridge.py b/gns3server/modules/dynamips/nodes/bridge.py index a87ba029..7dc4e31b 100644 --- a/gns3server/modules/dynamips/nodes/bridge.py +++ b/gns3server/modules/dynamips/nodes/bridge.py @@ -20,66 +20,50 @@ 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): -class Bridge(object): """ Dynamips bridge. - :param hypervisor: Dynamips hypervisor instance :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, hypervisor, name): + def __init__(self, name, node_id, project, manager, hypervisor=None): - 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) + super().__init__(name, node_id, project, manager, hypervisor) self._nios = [] - @property - def name(self): - """ - Returns the current name of this bridge. + @asyncio.coroutine + def create(self): - :returns: bridge name - """ + if self._hypervisor is None: + 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) - return self._name[1:-1] # remove quotes + yield from self._hypervisor.send('nio_bridge create "{}"'.format(self._name)) + self._hypervisor.devices.append(self) - @name.setter - def name(self, new_name): + @asyncio.coroutine + def set_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)) + 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 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): """ @@ -90,14 +74,18 @@ class Bridge(object): return self._nios + @asyncio.coroutine def delete(self): """ Deletes this bridge. """ - self._hypervisor.send("nio_bridge delete {}".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.send('nio_bridge delete "{}"'.format(self._name)) + @asyncio.coroutine def add_nio(self, nio): """ Adds a NIO as new port on this bridge. @@ -105,10 +93,10 @@ class Bridge(object): :param nio: NIO instance to add """ - self._hypervisor.send("nio_bridge add_nio {name} {nio}".format(name=self._name, - nio=nio)) + 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. @@ -116,6 +104,5 @@ class Bridge(object): :param nio: NIO instance to remove """ - self._hypervisor.send("nio_bridge remove_nio {name} {nio}".format(name=self._name, - nio=nio)) + 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/c1700.py b/gns3server/modules/dynamips/nodes/c1700.py index 906abe3e..1f7db1a0 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 @@ -29,65 +30,52 @@ log = logging.getLogger(__name__) 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 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, hypervisor, name, router_id=None, chassis="1720"): - Router.__init__(self, hypervisor, name, router_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 - 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 self._chassis = chassis self._iomem = 15 # percentage self._clock_divisor = 8 - self._sparsemem = False - - if chassis != "1720": - self.chassis = chassis - - self._setup_chassis() - - def defaults(self): - """ - Returns all the default attribute values for this platform. + self._sparsemem = False # never activate sparsemem for c1700 (unstable) - :returns: default values (dictionary) - """ - - router_defaults = Router.defaults(self) + def __json__(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 - - def list(self): - """ - Returns all c1700 instances + router_info = Router.__json__(self) + router_info.update(c1700_router_info) + return router_info - :returns: c1700 instance list - """ + @asyncio.coroutine + def create(self): - 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 +102,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 +111,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 +130,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..ce5721d1 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 @@ -31,12 +32,17 @@ log = logging.getLogger(__name__) 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 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). @@ -55,54 +61,36 @@ 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, 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 - 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 self._chassis = chassis self._iomem = 15 # percentage self._clock_divisor = 8 - self._sparsemem = False - - if chassis != "2610": - self.chassis = chassis - - self._setup_chassis() - - def defaults(self): - """ - Returns all the default attribute values for this platform. + self._sparsemem = False # never activate sparsemem for c2600 (unstable) - :returns: default values (dictionary) - """ - - router_defaults = Router.defaults(self) + def __json__(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 - - def list(self): - """ - Returns all c2600 instances + router_info = Router.__json__(self) + router_info.update(c2600_router_info) + return router_info - :returns: c2600 instance list - """ + @asyncio.coroutine + def create(self): - 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 +111,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 +121,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 +139,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..bafca2e0 100644 --- a/gns3server/modules/dynamips/nodes/c2691.py +++ b/gns3server/modules/dynamips/nodes/c2691.py @@ -20,27 +20,34 @@ 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 +from ..dynamips_error import DynamipsError import logging log = logging.getLogger(__name__) 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 + :param dynamips_id: ID to use with Dynamips + :param console: console port + :param aux: auxiliary console port """ - 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, 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 - 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 @@ -50,34 +57,16 @@ 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) + if chassis is not None: + raise DynamipsError("c2691 routers do not have chassis") - 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 +78,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..9eff5964 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 @@ -28,21 +29,26 @@ log = logging.getLogger(__name__) 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 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, hypervisor, name, router_id=None, chassis="3640"): - Router.__init__(self, hypervisor, name, router_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 - 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 @@ -50,40 +56,22 @@ class C3600(Router): self._chassis = chassis self._clock_divisor = 4 - if chassis != "3640": - self.chassis = chassis - - 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, - "chassis": self._chassis, - "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 + c3600_router_info = {"iomem": self._iomem, + "chassis": self._chassis} - def list(self): - """ - Returns all c3600 instances + router_info = Router.__json__(self) + router_info.update(c3600_router_info) + return router_info - :returns: c3600 instance list - """ + @asyncio.coroutine + def create(self): - 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 +97,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 +124,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..69bab887 100644 --- a/gns3server/modules/dynamips/nodes/c3725.py +++ b/gns3server/modules/dynamips/nodes/c3725.py @@ -20,26 +20,33 @@ 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 +from ..dynamips_error import DynamipsError import logging log = logging.getLogger(__name__) 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 + :param dynamips_id: ID to use with Dynamips + :param console: console port + :param aux: auxiliary console port """ - 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, 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 + # Set default values for this platform (must be the same as Dynamips) self._ram = 128 self._nvram = 112 self._disk0 = 16 @@ -50,34 +57,16 @@ 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) + if chassis is not None: + raise DynamipsError("c3725 routers do not have chassis") - 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 +78,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..53e4b1e3 100644 --- a/gns3server/modules/dynamips/nodes/c3745.py +++ b/gns3server/modules/dynamips/nodes/c3745.py @@ -20,27 +20,34 @@ 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 +from ..dynamips_error import DynamipsError import logging log = logging.getLogger(__name__) 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 + :param dynamips_id: ID to use with Dynamips + :param console: console port + :param aux: auxiliary console port """ - 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, 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 - 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 @@ -50,34 +57,16 @@ 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) + if chassis is not None: + raise DynamipsError("c3745 routers do not have chassis") - 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 +78,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..07e2ff4e 100644 --- a/gns3server/modules/dynamips/nodes/c7200.py +++ b/gns3server/modules/dynamips/nodes/c7200.py @@ -20,39 +20,44 @@ Interface for Dynamips virtual Cisco 7200 instances module ("c7200") http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L294 """ -from ..dynamips_error import DynamipsError +import asyncio + 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__) 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 dynamips_id: ID to use with Dynamips + :param console: console port + :param aux: auxiliary console port + :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, 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 - 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 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 +71,33 @@ 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()) + if chassis is not None: + raise DynamipsError("c7200 routers do not have chassis") - 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) + + if self._npe != "npe-400": + yield from self.set_npe(self._npe) - return self._hypervisor.send("c7200 list") + # 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 +109,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 +122,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 +140,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 +166,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 +181,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 +204,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 +215,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 +236,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/device.py b/gns3server/modules/dynamips/nodes/device.py new file mode 100644 index 00000000..fb5b3595 --- /dev/null +++ b/gns3server/modules/dynamips/nodes/device.py @@ -0,0 +1,104 @@ +# -*- 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 Device: + + """ + Base device for switches and hubs + + :param name: name for this device + :param device_id: Device instance identifier + :param project: Project instance + :param manager: Parent manager + :param hypervisor: Dynamips hypervisor instance + """ + + def __init__(self, name, device_id, project, manager, hypervisor=None): + + self._name = name + self._id = device_id + self._project = project + self._manager = manager + self._hypervisor = hypervisor + + @property + def hypervisor(self): + """ + Returns the current hypervisor. + + :returns: hypervisor instance + """ + + return self._hypervisor + + @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 + """ + + 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 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. + """ + + raise NotImplementedError diff --git a/gns3server/modules/dynamips/nodes/ethernet_hub.py b/gns3server/modules/dynamips/nodes/ethernet_hub.py new file mode 100644 index 00000000..92cfccab --- /dev/null +++ b/gns3server/modules/dynamips/nodes/ethernet_hub.py @@ -0,0 +1,179 @@ +# -*- 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 ..nios.nio_udp import NIOUDP +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 device_id: Device instance identifier + :param project: Project instance + :param manager: Parent VM Manager + :param hypervisor: Dynamips hypervisor instance + """ + + def __init__(self, name, device_id, project, manager, hypervisor=None): + + 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(self) + log.info('Ethernet hub "{name}" [{id}] has been created'.format(name=self._name, id=self._id)) + + @property + def mappings(self): + """ + Returns port mappings + + :returns: mappings list + """ + + return self._mappings + + @asyncio.coroutine + def delete(self): + """ + Deletes this hub. + """ + + for nio in self._nios: + if nio and isinstance(nio, NIOUDP): + self.manager.port_manager.release_udp_port(nio.lport, self._project) + + 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): + """ + 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._mappings: + 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._mappings[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._mappings: + 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, 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, + id=self._id, + nio=nio, + port=port_number)) + + del self._mappings[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._mappings: + raise DynamipsError("Port {} is not allocated".format(port_number)) + + nio = self._mappings[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 {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._mappings: + raise DynamipsError("Port {} is not allocated".format(port_number)) + + nio = self._mappings[port_number] + yield from nio.unbind_filter("both") + 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 45cc25c0..fc74e09c 100644 --- a/gns3server/modules/dynamips/nodes/ethernet_switch.py +++ b/gns3server/modules/dynamips/nodes/ethernet_switch.py @@ -20,112 +20,75 @@ Interface for Dynamips virtual Ethernet switch module ("ethsw"). http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L558 """ -import os +import asyncio + +from .device import Device +from ..nios.nio_udp import NIOUDP from ..dynamips_error import DynamipsError + import logging log = logging.getLogger(__name__) -class EthernetSwitch(object): +class EthernetSwitch(Device): + """ Dynamips Ethernet switch. - :param hypervisor: Dynamips hypervisor instance :param name: name for this switch + :param device_id: Device instance identifier + :param project: Project instance + :param manager: Parent VM Manager + :param hypervisor: Dynamips hypervisor instance """ - _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 + def __init__(self, name, device_id, project, manager, hypervisor=None): - 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) + super().__init__(name, device_id, project, manager, hypervisor) self._nios = {} - self._mapping = {} + self._mappings = {} - @classmethod - def reset(cls): - """ - Resets the instance count and the allocated instances list. - """ + def __json__(self): - cls._instances.clear() + ethernet_switch_info = {"name": self.name, + "device_id": self.id, + "project_id": self.project.id} - @property - def id(self): - """ - Returns the unique ID for this Ethernet switch. - - :returns: id (integer) - """ + ports = [] + for port_number, settings in self._mappings.items(): + ports.append({"port": port_number, + "type": settings[0], + "vlan": settings[1]}) - return self._id + ethernet_switch_info["ports"] = ports + return ethernet_switch_info - @property - def name(self): - """ - Returns the current name of this Ethernet switch. + @asyncio.coroutine + def create(self): - :returns: Ethernet switch name - """ + if self._hypervisor is None: + 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) - return self._name[1:-1] # remove quotes + 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) - @name.setter - def name(self, new_name): + @asyncio.coroutine + def set_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)) - + 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 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): """ @@ -137,142 +100,171 @@ class EthernetSwitch(object): 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): """ Deletes this Ethernet switch. """ - self._hypervisor.send("ethsw delete {}".format(self._name)) + for nio in self._nios.values(): + if nio and isinstance(nio, NIOUDP): + self.manager.port_manager.release_udp_port(nio.lport, self._project) - 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): + 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)) + 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() + + @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: port to allocate for the NIO + :param port_number: port to allocate for the NIO """ - if port in self._nios: - raise DynamipsError("Port {} isn't free".format(port)) + if port_number in self._nios: + raise DynamipsError("Port {} isn't free".format(port_number)) - self._hypervisor.send("ethsw add_nio {name} {nio}".format(name=self._name, - nio=nio)) + yield from 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 + 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 - def remove_nio(self, port): + @asyncio.coroutine + def remove_nio(self, port_number): """ Removes the specified NIO as member of this Ethernet switch. - :param port: allocated port + :param port_number: allocated port number :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)) + if port_number not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port_number)) - log.info("Ethernet switch {name} [id={id}]: NIO {nio} removed from port {port}".format(name=self._name, - id=self._id, - nio=nio, - port=port)) + nio = self._nios[port_number] + if isinstance(nio, NIOUDP): + 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)) - del self._nios[port] + log.info('Ethernet switch "{name}" [{id}]: NIO {nio} removed from port {port}'.format(name=self._name, + id=self._id, + nio=nio, + port=port_number)) - if port in self._mapping: - del self._mapping[port] + del self._nios[port_number] + if port_number in self._mappings: + del self._mappings[port_number] return nio - def set_access_port(self, port, vlan_id): + @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): """ Sets the specified port as an ACCESS port. - :param port: allocated port + :param port_number: allocated port number :param vlan_id: VLAN number membership """ - if port not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port)) + if port_number not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port_number)) - 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)) + 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={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) + 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._mappings[port_number] = ("access", vlan_id) - def set_dot1q_port(self, port, native_vlan): + @asyncio.coroutine + def set_dot1q_port(self, port_number, native_vlan): """ Sets the specified port as a 802.1Q trunk port. - :param port: allocated port + :param port_number: allocated port number :param native_vlan: native VLAN for this trunk port """ - if port not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port)) + if port_number not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port_number)) - 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)) + 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={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)) + 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] = ("dot1q", native_vlan) + self._mappings[port_number] = ("dot1q", native_vlan) - def set_qinq_port(self, port, outer_vlan): + @asyncio.coroutine + def set_qinq_port(self, port_number, outer_vlan): """ Sets the specified port as a trunk (QinQ) port. - :param port: allocated port + :param port_number: allocated port number :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)) + if port_number not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port_number)) - 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)) + 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={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) + 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._mappings[port_number] = ("qinq", outer_vlan) + @asyncio.coroutine def get_mac_addr_table(self): """ Returns the MAC address table for this Ethernet switch. @@ -280,62 +272,59 @@ class EthernetSwitch(object): :returns: list of entries (Ethernet address, VLAN, NIO) """ - return self._hypervisor.send("ethsw show_mac_addr_table {}".format(self._name)) + 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. """ - self._hypervisor.send("ethsw clear_mac_addr_table {}".format(self._name)) + yield from self._hypervisor.send('ethsw clear_mac_addr_table "{}"'.format(self._name)) - def start_capture(self, port, output_file, data_link_type="DLT_EN10MB"): + @asyncio.coroutine + def start_capture(self, port_number, output_file, data_link_type="DLT_EN10MB"): """ Starts a packet capture. - :param port: allocated port + :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 not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port)) + if port_number not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port_number)) - nio = self._nios[port] + 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)) - - try: - os.makedirs(os.path.dirname(output_file)) - except FileExistsError: - pass - except OSError as e: - raise DynamipsError("Could not create captures directory {}".format(e)) + raise DynamipsError("Port {} has already a filter applied".format(port_number)) - nio.bind_filter("both", "capture") - nio.setup_filter("both", "{} {}".format(data_link_type, output_file)) + 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={id}]: starting packet capture on {port}".format(name=self._name, - id=self._id, - port=port)) + log.info('Ethernet switch "{name}" [{id}]: starting packet capture on port {port}'.format(name=self._name, + id=self._id, + port=port_number)) - def stop_capture(self, port): + @asyncio.coroutine + def stop_capture(self, port_number): """ Stops a packet capture. - :param port: allocated port + :param port_number: allocated port number """ - if port not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port)) + if port_number not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port_number)) - 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)) + nio = self._nios[port_number] + yield from nio.unbind_filter("both") + 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 0b44fbea..a4bf56e6 100644 --- a/gns3server/modules/dynamips/nodes/frame_relay_switch.py +++ b/gns3server/modules/dynamips/nodes/frame_relay_switch.py @@ -20,112 +20,66 @@ Interface for Dynamips virtual Frame-Relay switch module. http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L642 """ -import os +import asyncio + +from .device import Device +from ..nios.nio_udp import NIOUDP from ..dynamips_error import DynamipsError import logging log = logging.getLogger(__name__) -class FrameRelaySwitch(object): +class FrameRelaySwitch(Device): + """ Dynamips Frame Relay switch. - :param hypervisor: Dynamips hypervisor instance :param name: name for this switch + :param device_id: Device instance identifier + :param project: Project instance + :param manager: Parent VM Manager + :param hypervisor: Dynamips hypervisor instance """ - _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") + def __init__(self, name, device_id, project, manager, hypervisor=None): - 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) + super().__init__(name, device_id, project, manager, hypervisor) self._nios = {} - self._mapping = {} + self._mappings = {} - @classmethod - def reset(cls): - """ - Resets the instance count and the allocated instances list. - """ + def __json__(self): - cls._instances.clear() + return {"name": self.name, + "device_id": self.id, + "project_id": self.project.id, + "mappings": self._mappings} - @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. + @asyncio.coroutine + def create(self): - :returns: Frame Relay switch name - """ + if self._hypervisor is None: + 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) - return self._name[1:-1] # remove quotes + 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) - @name.setter - def name(self, new_name): + @asyncio.coroutine + def set_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)) - + 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 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): """ @@ -137,26 +91,34 @@ class FrameRelaySwitch(object): 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): """ Deletes this Frame Relay switch. """ - self._hypervisor.send("frsw delete {}".format(self._name)) + for nio in self._nios.values(): + if nio and isinstance(nio, NIOUDP): + self.manager.port_manager.release_udp_port(nio.lport, self._project) - 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) + 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)) + 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() def has_port(self, port): """ @@ -169,45 +131,65 @@ class FrameRelaySwitch(object): return True return False - def add_nio(self, nio, port): + 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: port to allocate for the NIO + :param port_number: port to allocate for the NIO """ - if port in self._nios: - raise DynamipsError("Port {} isn't free".format(port)) + if port_number in self._nios: + raise DynamipsError("Port {} isn't free".format(port_number)) - 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)) + 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] = nio + self._nios[port_number] = nio - def remove_nio(self, port): + def remove_nio(self, port_number): """ Removes the specified NIO as member of this Frame Relay switch. - :param port: allocated port + :param port_number: allocated port number :returns: the NIO that was bound to the allocated port """ - if port not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port)) + if port_number not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port_number)) - 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)) + nio = self._nios[port_number] + if isinstance(nio, NIOUDP): + self.manager.port_manager.release_udp_port(nio.lport, self._project) - del self._nios[port] + 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 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): """ Creates a new Virtual Circuit connection (unidirectional). @@ -227,21 +209,22 @@ class FrameRelaySwitch(object): 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)) + 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={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)) + 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) + self._mappings[(port1, dlci1)] = (port2, dlci2) + @asyncio.coroutine def unmap_vc(self, port1, dlci1, port2, dlci2): """ Deletes a Virtual Circuit connection (unidirectional). @@ -261,67 +244,62 @@ class FrameRelaySwitch(object): 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"): + 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._mappings[(port1, dlci1)] + + @asyncio.coroutine + def start_capture(self, port_number, output_file, data_link_type="DLT_FRELAY"): """ Starts a packet capture. - :param port: allocated port + :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 not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port)) + if port_number not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port_number)) - nio = self._nios[port] + 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)) - - try: - os.makedirs(os.path.dirname(output_file)) - except FileExistsError: - pass - except OSError as e: - raise DynamipsError("Could not create captures directory {}".format(e)) + raise DynamipsError("Port {} has already a filter applied".format(port_number)) - nio.bind_filter("both", "capture") - nio.setup_filter("both", "{} {}".format(data_link_type, output_file)) + 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={id}]: starting packet capture on {port}".format(name=self._name, - id=self._id, - port=port)) + log.info('Frame relay switch "{name}" [{id}]: starting packet capture on port {port}'.format(name=self._name, + id=self._id, + port=port_number)) - def stop_capture(self, port): + @asyncio.coroutine + def stop_capture(self, port_number): """ Stops a packet capture. - :param port: allocated port + :param port_number: allocated port number """ - if port not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port)) + if port_number not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port_number)) - 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)) + nio = self._nios[port_number] + yield from nio.unbind_filter("both") + 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/modules/dynamips/nodes/hub.py b/gns3server/modules/dynamips/nodes/hub.py deleted file mode 100644 index 6f7f0e59..00000000 --- a/gns3server/modules/dynamips/nodes/hub.py +++ /dev/null @@ -1,188 +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/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index d0d1aef3..9c219a97 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.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,65 +20,52 @@ 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 asyncio import time import sys import os +import glob import base64 import logging 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 + + +class Router(BaseVM): -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. + :param name: The name of this 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 console: console port + :param aux: auxiliary console port + :param platform: Platform of this router """ - _instances = [] - _allocated_console_ports = [] - _allocated_aux_ports = [] + _dynamips_ids = {} _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) + def __init__(self, name, vm_id, project, manager, dynamips_id=None, console=None, aux=None, platform="c7200", hypervisor=None, ghost_flag=False): - else: - log.info("creating a new ghost IOS file") - self._id = 0 - name = "Ghost" + super().__init__(name, vm_id, project, manager, console=console) self._hypervisor = hypervisor - self._name = '"' + name + '"' # put name into quotes to protect spaces + self._dynamips_id = dynamips_id + self._closed = False + self._name = name self._platform = platform self._image = "" self._startup_config = "" @@ -97,320 +84,224 @@ class Router(object): 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._aux = aux + self._mac_addr = "" 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)) + self._ghost_flag = ghost_flag 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. - """ + 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) - 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 + if self._aux is not None: + 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._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._project) + self._console = None + 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, + "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, + "console": self._console, + "aux": self._aux, + "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: if slot: slot = str(slot) - router_defaults["slot" + str(slot_id)] = slot - slot_id += 1 + 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_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 + 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]) - @property - def name(self): - """ - Returns the name of this router. - - :returns: name (string) - """ + return router_info - return self._name[1:-1] # remove quotes - - @name.setter - def name(self, new_name): + @classmethod + def reset(cls): """ - Renames this router. - - :param new_name: new name string + Resets the instance count and the allocated instances list. """ - 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 + cls._dynamips_ids.clear() @property - def platform(self): + def dynamips_id(self): """ - Returns the platform of this router. + Returns the Dynamips VM ID. - :returns: platform name (string): - c7200, c3745, c3725, c3600, c2691, c2600 or c1700 + :return: Dynamips VM identifier """ - return self._platform + return self._dynamips_id - @property - def hypervisor(self): - """ - Returns the current hypervisor. + @asyncio.coroutine + def create(self): - :returns: hypervisor instance - """ + 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) - return self._hypervisor + yield from self._hypervisor.send('vm create "{name}" {id} {platform}'.format(name=self._name, + id=self._dynamips_id, + platform=self._platform)) - def list(self): - """ - Returns all VM instances + if not self._ghost_flag: - :returns: list of all VM instances - """ + log.info('Router {platform} "{name}" [{id}] has been created'.format(name=self._name, + platform=self._platform, + id=self._id)) - return self._hypervisor.send("vm list") + yield from self._hypervisor.send('vm set_con_tcp_port "{name}" {console}'.format(name=self._name, console=self._console)) - def list_con_ports(self): - """ - Returns all VM console TCP ports + 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)) - :returns: list of port numbers - """ + # 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] - return self._hypervisor.send("vm list_con_ports") + self._hypervisor.devices.append(self) - def delete(self): - """ - Deletes this router. + @asyncio.coroutine + def get_status(self): """ + Returns the status of 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.) + :returns: inactive, shutting down, running or suspended. """ - 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) + 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 starting it. + At least the IOS image must be set before it can start. """ - status = self.get_status() + status = yield from self.get_status() if status == "suspended": - self.resume() + 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))) + 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)) - self._hypervisor.send("vm start {}".format(self._name)) - log.info("router {name} [id={id}] has been started".format(name=self._name, id=self._id)) + 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 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)) + status = yield from self.get_status() + if status != "inactive": + 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)) - def suspend(self): + @asyncio.coroutine + def reload(self): """ - Suspends this router + Reload 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)) + yield from self.stop() + yield from self.start() - def resume(self): + @asyncio.coroutine + def suspend(self): """ - Resumes this suspended router + Suspends this 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)) + status = yield from self.get_status() + if status == "running": + 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)) - def get_status(self): + @asyncio.coroutine + def resume(self): """ - Returns the status of this router - - :returns: inactive, shutting down, running or suspended. + Resumes this suspended router """ - status_id = int(self._hypervisor.send("vm get_status {}".format(self._name))[0]) - return self._status[status_id] + 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 def is_running(self): """ Checks if this router is running. @@ -418,218 +309,135 @@ class Router(object): :returns: True if running, False otherwise """ - if self.get_status() == "running": + status = yield from self.get_status() + if 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 - """ + @asyncio.coroutine + def close(self): - if not self._image: - raise DynamipsError("Register an IOS image fist") + if self._closed: + # router is already closed + return - 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.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) - log.info("router {name} [id={id}]: set in JIT sharing group {group_id}".format(name=self._name, - id=self._id, - group_id=group_id)) + if self._console: + self._manager.port_manager.release_tcp_port(self._console, self._project) + self._console = None - self._jit_sharing_group = group_id - self._hypervisor.add_jitsharing_group(os.path.basename(self._image), group_id) + if self._aux: + self._manager.port_manager.release_tcp_port(self._aux, self._project) + self._aux = None - def set_debug_level(self, level): - """ - Sets the debug level for this router (default is 0). + 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._project) - :param level: level number - """ + if self in self._hypervisor.devices: + self._hypervisor.devices.remove(self) + 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 + yield from self.hypervisor.stop() - self._hypervisor.send("vm set_debug_level {name} {level}".format(name=self._name, - level=level)) + self._closed = True @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): + def platform(self): """ - Sets the IOS image for this router. - There is no default. + Returns the platform of this router. - :param image: path to IOS image file + :returns: platform name (string): + c7200, c3745, c3725, c3600, c2691, c2600 or c1700 """ - # 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 + return self._platform @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): + def hypervisor(self): """ - Sets the startup-config for this router. + Returns the current hypervisor. - :param startup_config: path to startup-config file + :returns: hypervisor instance """ - self._startup_config = startup_config + return self._hypervisor - @property - def private_config(self): + @asyncio.coroutine + def list(self): """ - Returns the private-config for this router. + Returns all VM instances - :returns: path to private-config file + :returns: list of all VM instances """ - return self._private_config + vm_list = yield from self._hypervisor.send("vm list") + return vm_list - @private_config.setter - def private_config(self, private_config): + @asyncio.coroutine + def list_con_ports(self): """ - Sets the private-config for this router. + Returns all VM console TCP ports - :param private_config: path to private-config file + :returns: list of port numbers """ - self._private_config = private_config + port_list = yield from self._hypervisor.send("vm list_con_ports") + return port_list - def set_config(self, startup_config, private_config=''): + @asyncio.coroutine + def set_debug_level(self, level): """ - Sets the config files that are pushed to startup-config and - private-config in NVRAM when the instance is started. + Sets the debug level for this router (default is 0). - :param startup_config: path to statup-config file - :param private_config: path to private-config file - (keep existing data when if an empty string) + :param level: level number """ - 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 + '"')) + yield from self._hypervisor.send('vm set_debug_level "{name}" {level}'.format(name=self._name, level=level)) - self._private_config = private_config - - def extract_config(self): + @property + def image(self): """ - Gets the contents of the config files - startup-config and private-config from NVRAM. + Returns this IOS image for this router. - :returns: tuple (startup-config, private-config) base64 encoded + :returns: path to IOS image file """ - 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 + return self._image - def push_config(self, startup_config, private_config='(keep)'): + @asyncio.coroutine + def set_image(self, image): """ - 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. + Sets the IOS image for this router. + There is no default. - :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')) + :param image: path to IOS image file """ - self._hypervisor.send("vm push_config {name} {startup} {private}".format(name=self._name, - startup=startup_config, - private=private_config)) + if not os.path.isabs(image): + 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) - log.info("router {name} [id={id}]: new startup-config pushed".format(name=self._name, - id=self._id)) + if not os.path.isfile(image): + raise DynamipsError("IOS image '{}' is not accessible".format(image)) - if private_config != '(keep)': - log.info("router {name} [id={id}]: new private-config pushed".format(name=self._name, - id=self._id)) + yield from self._hypervisor.send('vm set_ios "{name}" "{image}"'.format(name=self._name, image=image)) - def save_configs(self): - """ - Saves the startup-config and private-config to files. - """ + log.info('Router "{name}" [{id}]: has a new IOS image set: "{image}"'.format(name=self._name, + id=self._id, + image=image)) - 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)) + self._image = image @property def ram(self): @@ -641,8 +449,8 @@ class Router(object): return self._ram - @ram.setter - def ram(self, ram): + @asyncio.coroutine + def set_ram(self, ram): """ Sets amount of RAM allocated to this router @@ -652,17 +460,12 @@ class Router(object): 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) + 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 - self._hypervisor.increase_memory_load(ram) @property def nvram(self): @@ -674,8 +477,8 @@ class Router(object): return self._nvram - @nvram.setter - def nvram(self, nvram): + @asyncio.coroutine + def set_nvram(self, nvram): """ Sets amount of NVRAM allocated to this router @@ -685,13 +488,11 @@ class Router(object): 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)) + 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 @@ -704,8 +505,8 @@ class Router(object): return self._mmap - @mmap.setter - def mmap(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. @@ -717,15 +518,13 @@ class Router(object): flag = 1 else: flag = 0 - 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={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={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 @@ -738,8 +537,8 @@ class Router(object): return self._sparsemem - @sparsemem.setter - def sparsemem(self, sparsemem): + @asyncio.coroutine + def set_sparsemem(self, sparsemem): """ Enable/disable use of sparse memory @@ -750,15 +549,12 @@ class Router(object): flag = 1 else: flag = 0 - 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={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={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 @@ -771,8 +567,8 @@ class Router(object): return self._clock_divisor - @clock_divisor.setter - def clock_divisor(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. @@ -780,13 +576,11 @@ class Router(object): :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)) + 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 @@ -799,8 +593,8 @@ class Router(object): return self._idlepc - @idlepc.setter - def idlepc(self, idlepc): + @asyncio.coroutine + def set_idlepc(self, idlepc): """ Sets the idle Pointer Counter (PC) @@ -810,20 +604,17 @@ class Router(object): if not idlepc: idlepc = "0x0" - if not self.is_running(): + is_running = yield from self.is_running() + if not is_running: # router is not running - 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: - 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)) + 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. @@ -833,18 +624,20 @@ class Router(object): :returns: list of idle PC proposal """ - if not self.is_running(): + 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={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 = 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)) + 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). @@ -852,11 +645,13 @@ class Router(object): :returns: list of idle PC proposal """ - if not self.is_running(): + 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)) - return 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 def idlemax(self): @@ -868,22 +663,22 @@ class Router(object): return self._idlemax - @idlemax.setter - def idlemax(self, idlemax): + @asyncio.coroutine + def set_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)) + 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={id}]: idlemax updated from {old_idlemax} to {new_idlemax}".format(name=self._name, - id=self._id, - old_idlemax=self._idlemax, - new_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 @@ -897,34 +692,26 @@ class Router(object): return self._idlesleep - @idlesleep.setter - def idlesleep(self, idlesleep): + @asyncio.coroutine + def set_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)) + 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={id}]: idlesleep updated from {old_idlesleep} to {new_idlesleep}".format(name=self._name, - id=self._id, - old_idlesleep=self._idlesleep, - new_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 - 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): """ @@ -935,27 +722,23 @@ class Router(object): return self._ghost_file - @ghost_file.setter - def ghost_file(self, ghost_file): + @asyncio.coroutine + def set_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)) + 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={id}]: ghost file set to {ghost_file}".format(name=self._name, - id=self._id, - 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 - # 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. @@ -977,8 +760,8 @@ class Router(object): return self._ghost_status - @ghost_status.setter - def ghost_status(self, ghost_status): + @asyncio.coroutine + def set_ghost_status(self, ghost_status): """ Sets ghost RAM status @@ -988,12 +771,12 @@ class Router(object): 2 => Use an existing ghost instance """ - self._hypervisor.send("vm set_ghost_status {name} {ghost_status}".format(name=self._name, - ghost_status=ghost_status)) + 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={id}]: ghost status set to {ghost_status}".format(name=self._name, - id=self._id, - 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 @@ -1006,8 +789,8 @@ class Router(object): return self._exec_area - @exec_area.setter - def exec_area(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 @@ -1017,13 +800,13 @@ class Router(object): :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)) + 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={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)) + 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 @@ -1036,21 +819,20 @@ class Router(object): return self._disk0 - @disk0.setter - def disk0(self, disk0): + @asyncio.coroutine + def set_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)) + yield from 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)) + 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 @@ -1063,7 +845,7 @@ class Router(object): return self._disk1 - @disk1.setter + @asyncio.coroutine def disk1(self, disk1): """ Sets the size (MB) for PCMCIA disk1. @@ -1071,77 +853,31 @@ class Router(object): :param disk1: disk1 size (integer) """ - 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={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)) + 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 - - @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): + @asyncio.coroutine + def set_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)) + yield from self._hypervisor.send('vm set_con_tcp_port "{name}" {console}'.format(name=self._name, console=console)) - 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)) - 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) + 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): @@ -1153,44 +889,25 @@ class Router(object): return self._aux - @aux.setter - def aux(self, aux): + @asyncio.coroutine + def set_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)) + yield from self._hypervisor.send('vm set_aux_tcp_port "{name}" {aux}'.format(name=self._name, aux=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) - """ + 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)) - # FIXME: nothing returned by Dynamips. - return self._hypervisor.send("vm cpu_info {name} {cpu_id}".format(name=self._name, - cpu_id=cpu_id)) + 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): """ Shows cpu usage in seconds, "cpu_id" is ignored. @@ -1198,28 +915,8 @@ class Router(object): :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)) + 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): @@ -1231,22 +928,22 @@ class Router(object): return self._mac_addr - @mac_addr.setter - def mac_addr(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) """ - self._hypervisor.send("{platform} set_mac_addr {name} {mac_addr}".format(platform=self._platform, - name=self._name, - mac_addr=mac_addr)) + 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={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)) + 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 @@ -1259,44 +956,25 @@ class Router(object): return self._system_id - @system_id.setter - def system_id(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) """ - self._hypervisor.send("{platform} set_system_id {name} {system_id}".format(platform=self._platform, - name=self._name, - system_id=system_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={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)) + 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 - 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]) - + @asyncio.coroutine def get_slot_bindings(self): """ Returns slot bindings. @@ -1304,356 +982,368 @@ class Router(object): :returns: slot bindings (adapter names) list """ - return 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 - def slot_add_binding(self, slot_id, adapter): + @asyncio.coroutine + 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} doesn't 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, - adapter=current_adapter)) + 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() # 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)) + 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)) - 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={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 self.is_running(): + if is_running: - 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={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)) - def slot_remove_binding(self, slot_id): + @asyncio.coroutine + 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} doesn't 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() # 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)) + 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 self.is_running(): + if is_running: - 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={id}]: OIR stop event sent to slot {slot_id}".format(name=self._name, - id=self._id, - slot_id=slot_id)) + log.info('Router "{name}" [{id}]: OIR stop event sent to slot {slot_number}'.format(name=self._name, + id=self._id, + slot_number=slot_number)) - 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={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 + 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)) + self._slots[slot_number] = None - def install_wic(self, wic_slot_id, wic): + @asyncio.coroutine + 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(name=self._name, - 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(name=self._name, - 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) - 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)) + 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={id}]: {wic} inserted into WIC slot {wic_slot_id}".format(name=self._name, - id=self._id, - wic=wic, - wic_slot_id=wic_slot_id)) + 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)) - adapter.install_wic(wic_slot_id, wic) + adapter.install_wic(wic_slot_number, wic) - def uninstall_wic(self, wic_slot_id): + @asyncio.coroutine + 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(name=self._name, - 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_number): + raise DynamipsError("No WIC is installed in WIC slot {wic_slot_number}".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(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)) + 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={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) - def get_slot_nio_bindings(self, slot_id): + @asyncio.coroutine + 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 """ - return (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 - def slot_add_nio_binding(self, slot_id, port_id, nio): + @asyncio.coroutine + 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} 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)) + 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)) - self.slot_enable_nio(slot_id, port_id) - adapter.add_nio(port_id, 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)) - def slot_remove_nio_binding(self, slot_id, port_id): + 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_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} 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) + 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) + if nio is None: + return + if isinstance(nio, NIOUDP): + self.manager.port_manager.release_udp_port(nio.lport, self._project) + adapter.remove_nio(port_number) - 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)) + 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 - def slot_enable_nio(self, slot_id, port_id): + @asyncio.coroutine + 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 """ - 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)) + is_running = yield from self.is_running() + if is_running: # running router + 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={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)) - def slot_disable_nio(self, slot_id, port_id): + @asyncio.coroutine + 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 """ - 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)) + is_running = yield from self.is_running() + if is_running: # running router + 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={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)) - def start_capture(self, slot_id, port_id, output_file, data_link_type="DLT_EN10MB"): + @asyncio.coroutine + 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} 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)) + 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)) - 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)) + yield from nio.bind_filter("both", "capture") + yield from 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)) + 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)) - def stop_capture(self, slot_id, port_id): + @asyncio.coroutine + 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} 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)) + 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.unbind_filter("both") + nio = adapter.get_nio(port_number) + yield from 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)) + 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): """ @@ -1673,3 +1363,185 @@ class Router(object): """ 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 + + @property + def private_config(self): + """ + Returns the private-config for this router. + + :returns: path to private-config file + """ + + return self._private_config + + @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_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. + + :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)) + 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 + + @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): + """ + 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))) + + 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)) + 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): + """ + Deletes this router & associated files (nvram, disks etc.) + """ + + yield from self._hypervisor.send('vm clean_delete "{}"'.format(self._name)) + self._hypervisor.devices.remove(self) + log.info('Router "{name}" [{id}] has been deleted (including associated files)'.format(name=self._name, id=self._id)) diff --git a/gns3server/modules/dynamips/schemas/ethhub.py b/gns3server/modules/dynamips/schemas/ethhub.py deleted file mode 100644 index 1002a696..00000000 --- a/gns3server/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/modules/dynamips/schemas/ethsw.py b/gns3server/modules/dynamips/schemas/ethsw.py deleted file mode 100644 index aeac7023..00000000 --- a/gns3server/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/modules/dynamips/schemas/frsw.py b/gns3server/modules/dynamips/schemas/frsw.py deleted file mode 100644 index 835e47a7..00000000 --- a/gns3server/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/modules/iou/__init__.py b/gns3server/modules/iou/__init__.py index 4a6ceec6..3cdddfe7 100644 --- a/gns3server/modules/iou/__init__.py +++ b/gns3server/modules/iou/__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 @@ -20,823 +20,60 @@ IOU server module. """ import os -import base64 -import ntpath -import stat -import tempfile -import socket -import shutil +import asyncio -from gns3server.modules import IModule -from gns3server.config import Config -from gns3dms.cloud.rackspace_ctrl import get_provider -from .iou_device import IOUDevice +from ..base_manager import BaseManager 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 .iou_vm import IOUVM -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(BaseManager): + _VM_CLASS = IOUVM + def __init__(self): + super().__init__() + self._free_application_ids = list(range(1, 512)) + self._used_application_ids = {} -class IOU(IModule): - """ - IOU module. + @asyncio.coroutine + def create_vm(self, *args, **kwargs): - :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"] + vm = yield from super().create_vm(*args, **kwargs) 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 + self._used_application_ids[vm.id] = self._free_application_ids.pop(0) + except IndexError: + raise IOUError("No mac address available") + return vm - try: - iou_instance.slot_add_nio_binding(slot, port, nio) - except IOUError as e: - self.send_custom_error(str(e)) - return + @asyncio.coroutine + def close_vm(self, vm_id, *args, **kwargs): - self.send_response({"port_id": request["port_id"]}) + 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().close_vm(vm_id, *args, **kwargs) + return vm - @IModule.route("iou.delete_nio") - def delete_nio(self, request): + def get_application_id(self, vm_id): """ - Deletes an NIO (Network Input/Output). - - Mandatory request parameters: - - id (IOU instance identifier) - - slot (slot identifier) - - port (port identifier) + Get an unique IOU mac id - Response parameters: - - True on success - - :param request: JSON request + :param vm_id: ID of the IOU VM + :returns: IOU MAC id """ - # 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) + return self._used_application_ids.get(vm_id, 1) - @IModule.route("iou.start_capture") - def start_capture(self, request): + @staticmethod + def get_legacy_vm_workdir(legacy_vm_id, name): """ - Starts a packet capture. - - Mandatory request parameters: - - id (vm identifier) - - slot (slot number) - - port (port number) - - port_id (port identifier) - - capture_file_name + Returns the name of the legacy working directory (pre 1.3) name for a VM. - 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 legacy_vm_id: legacy VM identifier (integer) + :param name: VM name (not used) - :param request: JSON request + :returns: working directory name """ - if request is None: - self.send_param_error() - else: - log.debug("received request {}".format(request)) - self.send_response(request) + return os.path.join("iou", "device-{}".format(legacy_vm_id)) diff --git a/gns3server/modules/iou/adapters/adapter.py b/gns3server/modules/iou/adapters/adapter.py deleted file mode 100644 index 4d2f4053..00000000 --- a/gns3server/modules/iou/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=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/modules/iou/iou_device.py b/gns3server/modules/iou/iou_device.py deleted file mode 100644 index dec395bb..00000000 --- a/gns3server/modules/iou/iou_device.py +++ /dev/null @@ -1,1068 +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/modules/iou/iou_error.py b/gns3server/modules/iou/iou_error.py index 8aac176f..cd43bdb9 100644 --- a/gns3server/modules/iou/iou_error.py +++ b/gns3server/modules/iou/iou_error.py @@ -19,21 +19,8 @@ Custom exceptions for IOU module. """ +from ..vm_error import VMError -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 +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..fd0cba1f --- /dev/null +++ b/gns3server/modules/iou/iou_vm.py @@ -0,0 +1,1064 @@ +# -*- 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 signal +import socket +import re +import asyncio +import subprocess +import shutil +import argparse +import threading +import configparser +import struct +import hashlib +import glob + +from .iou_error import IOUError +from ..adapters.ethernet_adapter import EthernetAdapter +from ..adapters.serial_adapter import SerialAdapter +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 + + +import logging +import sys +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 + :params ethernet_adapters: Number of ethernet adapters + :params serial_adapters: Number of serial adapters + :params ram: Ram MB + :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, + console=None, + ram=None, + nvram=None, + ethernet_adapters=None, + serial_adapters=None, + l1_keepalives=None, + initial_config=None, + iourc_content=None): + + super().__init__(name, vm_id, project, manager, console=console) + + self._command = [] + self._iouyap_process = None + self._iou_process = None + self._iou_stdout_file = "" + self._started = False + self._path = None + self._ioucon_thread = None + + # IOU settings + 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 if nvram is None else nvram # Kilobytes + self._initial_config = "" + 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 + + @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._project) + 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, self._project) + + yield from self.stop() + + @property + def path(self): + """Path of the iou binary""" + + return self._path + + @path.setter + def path(self, path): + """ + Path of the iou binary + + :params path: Path to the binary + """ + + if not os.path.isabs(path): + server_config = self.manager.config.get_section_config("Server") + 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): + 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)) + + @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)) + + 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): + + 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, + "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") + 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): + """ + 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 iourc_path(self): + """ + Returns the IOURC path. + + :returns: path to IOURC + """ + + 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 + # 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 + 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 + + @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) + + @property + def iourc_content(self): + 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: + 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): + """ + Checks for missing shared library dependencies in the IOU image. + """ + + try: + 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)) + return + + p = re.compile("([\.\w]+)\s=>\s+not found") + 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))) + + @asyncio.coroutine + def _check_iou_licence(self): + """ + 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() + 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) + 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] + 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] + + # We can't test this because it's mean distributing a valid licence key + # in tests or generating one + if not hasattr(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): + """ + Starts the IOU process. + """ + + self._check_requirements() + if not self.is_running(): + + yield from self._library_check() + + self._rename_nvram_file() + + 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") + + 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") + + self._create_netmap_config() + # created a environment variable pointing to the iourc file. + env = os.environ.copy() + + if "IOURC" not in os.environ: + env["IOURC"] = iourc_path + 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") + 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._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 + yield from 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) + 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): + """ + 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 = 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: + 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._adapters: + unit_id = 0 + for unit in adapter.ports.keys(): + nio = adapter.get_nio(unit) + if nio: + connection = None + 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, NIOTAP): + # TAP interface + connection = {"tap_dev": "{tap_device}".format(tap_device=nio.tap_device)} + + elif isinstance(nio, NIOGenericEthernet): + # 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): + """ + Stops the IOU process. + """ + + 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() + 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: + if self._iou_process.returncode is None: + 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: + self._terminate_process_iouyap() + try: + yield from gns3server.utils.asyncio.wait_for_process_termination(self._iouyap_process, timeout=3) + except asyncio.TimeoutError: + if self._iouyap_process.returncode is 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): + """Terminate the process if running""" + + if self._iouyap_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: + 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 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. + """ + + 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)) + + @asyncio.coroutine + 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 + + initial_config_file = self.initial_config_file + if 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)]) + 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._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() + 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(interfaces=4)) + + 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._adapters = 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(interfaces=4)) + + 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._adapters = self._ethernet_adapters + self._serial_adapters + + def adapter_add_nio_binding(self, adapter_number, port_number, nio): + """ + Adds a adapter NIO binding. + :param adapter_number: adapter ID + :param port_number: port ID + :param nio: NIO instance to add to the adapter/port + """ + + 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)) + + 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 adapter_remove_nio_binding(self, adapter_number, port_number): + """ + Removes a adapter NIO binding. + :param adapter_number: adapter ID + :param port_number: port ID + :returns: NIO instance + """ + + 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 isinstance(nio, NIOUDP): + 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, + 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) + + 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)) + + @asyncio.coroutine + def _enable_l1_keepalives(self, command): + """ + Enables L1 keepalive messages if supported. + :param command: command line + """ + + env = os.environ.copy() + 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): + 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)) + + @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 IOUError("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 IOUError("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 + + @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 'initial-config.cfg' + else: + return None + + @asyncio.coroutine + 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) + + @asyncio.coroutine + 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/iou/ioucon.py b/gns3server/modules/iou/ioucon.py index cb280fa1..764e81e8 100644 --- a/gns3server/modules/iou/ioucon.py +++ b/gns3server/modules/iou/ioucon.py @@ -55,26 +55,26 @@ 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 +# --[ 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 -#--[ Telnet Options ]---------------------------------------------------------- +# --[ Telnet Options ]---------------------------------------------------------- BINARY = 0 # Transmit Binary ECHO = 1 # Echo characters back to sender RECON = 2 # Reconnection @@ -154,14 +154,11 @@ class FileLock: class Console: + def fileno(self): raise NotImplementedError("Only routers have fileno()") -class Router: - pass - - class TTY(Console): def read(self, fileno, bufsize): @@ -174,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) @@ -243,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 @@ -301,9 +304,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)) @@ -366,7 +372,7 @@ class TelnetServer(Console): return False -class IOU(Router): +class IOU: def __init__(self, ttyC, ttyS, stop_event): self.ttyC = ttyC @@ -420,6 +426,9 @@ class IOU(Router): 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,14 +621,20 @@ 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: - 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(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: diff --git a/gns3server/modules/iou/nios/nio_udp.py b/gns3server/modules/iou/nios/nio_udp.py deleted file mode 100644 index 2c850351..00000000 --- a/gns3server/modules/iou/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/iou/schemas.py b/gns3server/modules/iou/schemas.py deleted file mode 100644 index f1315ec3..00000000 --- a/gns3server/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/modules/iou/adapters/__init__.py b/gns3server/modules/nios/__init__.py similarity index 100% rename from gns3server/modules/iou/adapters/__init__.py rename to gns3server/modules/nios/__init__.py diff --git a/gns3server/modules/iou/nios/nio.py b/gns3server/modules/nios/nio.py similarity index 98% rename from gns3server/modules/iou/nios/nio.py rename to gns3server/modules/nios/nio.py index 059d56a3..b1ab24ae 100644 --- a/gns3server/modules/iou/nios/nio.py +++ b/gns3server/modules/nios/nio.py @@ -21,8 +21,9 @@ Base interface for NIOs. class NIO(object): + """ - Network Input/Output. + IOU NIO. """ def __init__(self): @@ -33,7 +34,6 @@ class NIO(object): 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 """ @@ -52,7 +52,6 @@ class NIO(object): def capturing(self): """ Returns either a capture is configured on this NIO. - :returns: boolean """ @@ -62,7 +61,6 @@ class NIO(object): def pcap_output_file(self): """ Returns the path to the PCAP output file. - :returns: path to the PCAP output file """ @@ -72,7 +70,6 @@ class NIO(object): def pcap_data_link_type(self): """ Returns the PCAP data link type - :returns: PCAP data link type (DLT_* value) """ diff --git a/gns3server/modules/iou/nios/nio_generic_ethernet.py b/gns3server/modules/nios/nio_generic_ethernet.py similarity index 88% rename from gns3server/modules/iou/nios/nio_generic_ethernet.py rename to gns3server/modules/nios/nio_generic_ethernet.py index 068e9fc3..da729565 100644 --- a/gns3server/modules/iou/nios/nio_generic_ethernet.py +++ b/gns3server/modules/nios/nio_generic_ethernet.py @@ -22,7 +22,8 @@ Interface for generic Ethernet NIOs (PCAP library). from .nio import NIO -class NIO_GenericEthernet(NIO): +class NIOGenericEthernet(NIO): + """ Generic Ethernet NIO. @@ -47,3 +48,8 @@ class NIO_GenericEthernet(NIO): def __str__(self): return "NIO Ethernet" + + def __json__(self): + + return {"type": "nio_generic_ethernet", + "ethernet_device": self._ethernet_device} diff --git a/gns3server/modules/iou/nios/nio_tap.py b/gns3server/modules/nios/nio_tap.py similarity index 88% rename from gns3server/modules/iou/nios/nio_tap.py rename to gns3server/modules/nios/nio_tap.py index 95ec631d..9f51ce13 100644 --- a/gns3server/modules/iou/nios/nio_tap.py +++ b/gns3server/modules/nios/nio_tap.py @@ -22,7 +22,8 @@ Interface for TAP NIOs (UNIX based OSes only). from .nio import NIO -class NIO_TAP(NIO): +class NIOTAP(NIO): + """ TAP NIO. @@ -31,7 +32,7 @@ class NIO_TAP(NIO): def __init__(self, tap_device): - NIO.__init__(self) + super().__init__() self._tap_device = tap_device @property @@ -47,3 +48,8 @@ class NIO_TAP(NIO): def __str__(self): return "NIO TAP" + + def __json__(self): + + return {"type": "nio_tap", + "tap_device": self._tap_device} diff --git a/gns3server/modules/qemu/nios/nio_udp.py b/gns3server/modules/nios/nio_udp.py similarity index 87% rename from gns3server/modules/qemu/nios/nio_udp.py rename to gns3server/modules/nios/nio_udp.py index 2c850351..a87875fe 100644 --- a/gns3server/modules/qemu/nios/nio_udp.py +++ b/gns3server/modules/nios/nio_udp.py @@ -22,7 +22,8 @@ Interface for UDP NIOs. from .nio import NIO -class NIO_UDP(NIO): +class NIOUDP(NIO): + """ UDP NIO. @@ -31,11 +32,9 @@ class NIO_UDP(NIO): :param rport: remote port number """ - _instance_count = 0 - def __init__(self, lport, rhost, rport): - NIO.__init__(self) + super().__init__() self._lport = lport self._rhost = rhost self._rport = rport @@ -73,3 +72,10 @@ class NIO_UDP(NIO): 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/port_manager.py b/gns3server/modules/port_manager.py new file mode 100644 index 00000000..52d27933 --- /dev/null +++ b/gns3server/modules/port_manager.py @@ -0,0 +1,265 @@ +# -*- 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 socket +import ipaddress +from aiohttp.web import HTTPConflict +from gns3server.config import Config + +import logging +log = logging.getLogger(__name__) + + +class PortManager: + + """ + :param host: IP address to bind for console connections + """ + + def __init__(self, host="127.0.0.1"): + + self._console_host = host + self._udp_host = host + + self._used_tcp_ports = set() + self._used_udp_ports = set() + + 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) + 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") + if ipaddress.ip_address(host).version == 6: + self._console_host = "::" + else: + self._console_host = "0.0.0.0" + else: + self._console_host = host + + 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): + + return self._console_host + + @console_host.setter + def console_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 + + @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=[]): + """ + 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 HTTPConflict(text="Could not find a free port between {} and {} on host {}, last exception: {}".format(start_port, + end_port, + host, + last_exception)) + + 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], + self._console_port_range[1], + host=self._console_host, + socket_type="TCP", + 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, 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, 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, project): + """ + Get an available UDP port and reserve it + + :param project: Project instance + """ + + 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) + project.record_udp_port(port) + log.debug("UDP port {} has been allocated".format(port)) + return 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, 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 new file mode 100644 index 00000000..f308fc12 --- /dev/null +++ b/gns3server/modules/project.py @@ -0,0 +1,405 @@ +# -*- 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 os +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 + +import logging +log = logging.getLogger(__name__) + + +class Project: + + """ + A project contains a list of VM. + 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, name=None, project_id=None, path=None, location=None, temporary=False): + + self._name = name + if project_id is None: + self._id = str(uuid4()) + else: + try: + UUID(project_id, version=4) + except ValueError: + raise aiohttp.web.HTTPBadRequest(text="{} is not a valid UUID".format(project_id)) + self._id = project_id + + self._location = None + if location is None: + self._location = self._config().get("project_directory", self._get_default_project_directory()) + else: + self.location = location + + 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) + try: + os.makedirs(path, exist_ok=True) + except OSError as e: + raise aiohttp.web.HTTPInternalServerError(text="Could not create project directory: {}".format(e)) + self.path = path + + log.info("Project {id} with path '{path}' created".format(path=self._path, id=self._id)) + + def __json__(self): + + return { + "name": self._name, + "project_id": self._id, + "location": self._location, + "temporary": self._temporary, + "path": self._path, + } + + def _config(self): + + return Config.instance().get_section_config("Server") + + def is_local(self): + + return self._config().getboolean("local", False) + + @classmethod + def _get_default_project_directory(cls): + """ + Return the default location for the project directory + depending of the operating system + """ + + 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: + raise aiohttp.web.HTTPInternalServerError(text="Could not create project directory: {}".format(e)) + return path + + @property + def id(self): + + return self._id + + @property + def location(self): + + return self._location + + @location.setter + def location(self, location): + + 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 + + @property + def path(self): + + return self._path + + @path.setter + def path(self, path): + + if hasattr(self, "_path"): + 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): + + return self._vms + + @property + def temporary(self): + + return self._temporary + + @temporary.setter + def temporary(self, temporary): + + if hasattr(self, 'temporary') and temporary == self._temporary: + return + + 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 + project status. + """ + + if not hasattr(self, "_path"): + return + + 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 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 = 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. + If the directory doesn't exist, the directory is created. + + :param vm: VM instance + :returns: VM working directory + """ + + 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: + 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, "project-files", "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): + """ + :param vm: An instance of VM + """ + + self.remove_vm(vm) + self._vms_to_destroy.add(vm) + + def add_vm(self, vm): + """ + Add a VM to the project. + In theory this should be called by the VM manager. + + :param vm: 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. + + :param vm: VM instance + """ + + if vm in self._vms: + self._vms.remove(vm) + + @asyncio.coroutine + 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): + """ + Close the project, and cleanup the disk if cleanup is True + + :param cleanup: If True drop the project directory + """ + + tasks = [] + for vm in self._vms: + tasks.append(asyncio.async(vm.manager.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 or device {}".format(e), exc_info=1) + + 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)) + + 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)) + + # 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): + """Write project changes on disk""" + + while self._vms_to_destroy: + 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): + """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): + """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) + + 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/gns3server/modules/project_manager.py b/gns3server/modules/project_manager.py new file mode 100644 index 00000000..4ea21612 --- /dev/null +++ b/gns3server/modules/project_manager.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 aiohttp +from .project import Project +from uuid import UUID + + +class ProjectManager: + + """ + This singleton keeps track of available projects. + """ + + def __init__(self): + + self._projects = {} + + @classmethod + def instance(cls): + """ + Singleton to return only one instance of ProjectManager. + + :returns: instance of ProjectManager + """ + + if not hasattr(cls, "_instance"): + 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. + + :param project_id: Project identifier + + :returns: Project instance + """ + + try: + UUID(project_id, version=4) + except ValueError: + 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)) + return self._projects[project_id] + + 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. + + See documentation of Project for arguments + """ + + if project_id is not None and project_id in self._projects: + 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(name=name, 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/__init__.py b/gns3server/modules/qemu/__init__.py index beaac4ef..aa9e2f90 100644 --- a/gns3server/modules/qemu/__init__.py +++ b/gns3server/modules/qemu/__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 @@ -16,623 +16,30 @@ # along with this program. If not, see . """ -QEMU server module. +Qemu server module. """ -import sys +import asyncio import os -import socket -import shutil -import subprocess +import sys import re +import subprocess -from gns3server.modules import IModule -from gns3server.config import Config -from .qemu_vm import QemuVM +from ...utils.asyncio import subprocess_check_output +from ..base_manager import BaseManager 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. +from .qemu_vm import QemuVM - :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)) +class Qemu(BaseManager): + _VM_CLASS = QemuVM - @IModule.route("qemu.qemu_list") - def qemu_list(self, request): + @staticmethod + def binary_list(): """ - Gets QEMU binaries list. + Gets QEMU binaries list available on the matchine - Response parameters: - - List of Qemu binaries + :returns: Array of dictionnary {"path": Qemu binaries path, "version": Version of Qemu} """ qemus = [] @@ -663,24 +70,43 @@ class Qemu(IModule): 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) + version = yield from Qemu._get_qemu_version(qemu_path) qemus.append({"path": qemu_path, "version": version}) except OSError: continue - response = {"qemus": qemus} - self.send_response(response) + return qemus - @IModule.route("qemu.echo") - def echo(self, request): + @staticmethod + @asyncio.coroutine + def _get_qemu_version(qemu_path): + """ + Gets the Qemu version. + :param qemu_path: path to Qemu """ - Echo end point for testing purposes. - :param request: JSON request + 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) + 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)) + + @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 """ - if request is None: - self.send_param_error() - else: - log.debug("received request {}".format(request)) - self.send_response(request) + return os.path.join("qemu", "vm-{}".format(legacy_vm_id)) diff --git a/gns3server/modules/qemu/nios/nio.py b/gns3server/modules/qemu/nios/nio.py deleted file mode 100644 index eee5f1d5..00000000 --- a/gns3server/modules/qemu/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/qemu/qemu_error.py b/gns3server/modules/qemu/qemu_error.py index 55135a34..48aca696 100644 --- a/gns3server/modules/qemu/qemu_error.py +++ b/gns3server/modules/qemu/qemu_error.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2014 GNS3 Technologies Inc. +# 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 @@ -16,24 +16,12 @@ # along with this program. If not, see . """ -Custom exceptions for QEMU module. +Custom exceptions for Qemu module. """ +from ..vm_error import VMError -class QemuError(Exception): - def __init__(self, message, original_exception=None): +class QemuError(VMError): - 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/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index 1b271694..7d1675ee 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -25,100 +25,63 @@ 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 +import asyncio from .qemu_error import QemuError -from .adapters.ethernet_adapter import EthernetAdapter -from .nios.nio_udp import NIO_UDP -from ..attic import find_unused_port +from ..adapters.ethernet_adapter import EthernetAdapter +from ..nios.nio_udp import NIOUDP +from ..base_vm import BaseVM +from ...schemas.qemu import QEMU_OBJECT_SCHEMA import logging log = logging.getLogger(__name__) -class QemuVM(object): +class QemuVM(BaseVM): + module_name = 'qemu' + """ QEMU VM implementation. - :param name: name of this QEMU VM + :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 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, + vm_id, + project, + manager, + qemu_path=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 + monitor=None): + + super().__init__(name, vm_id, project, manager, console=console) + + 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._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._hdc_disk_image = "" + self._hdd_disk_image = "" self._options = "" self._ram = 256 - self._console = console self._monitor = monitor self._ethernet_adapters = [] self._adapter_type = "e1000" @@ -129,176 +92,15 @@ class QemuVM(object): 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) + if self._monitor is not None: + 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._project) 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): """ @@ -317,81 +119,16 @@ class QemuVM(object): :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)) + if monitor == self._monitor: 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 + if self._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, + id=self.id, + port=monitor)) @property def qemu_path(self): @@ -411,10 +148,20 @@ class QemuVM(object): :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): @@ -434,6 +181,10 @@ class QemuVM(object): :param hda_disk_image: QEMU hda disk image path """ + 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, id=self._id, disk_image=hda_disk_image)) @@ -457,11 +208,68 @@ class QemuVM(object): :param hdb_disk_image: QEMU hdb disk image path """ + if not os.path.isabs(hdb_disk_image): + 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, id=self._id, 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 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, + 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 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, + id=self._id, + disk_image=hdd_disk_image)) + self._hdd_disk_image = hdd_disk_image @property def adapters(self): @@ -586,7 +394,6 @@ class QemuVM(object): priority=process_priority)) self._process_priority = process_priority - @property def ram(self): """ @@ -651,6 +458,10 @@ class QemuVM(object): :param initrd: QEMU initrd path """ + 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, id=self._id, initrd=initrd)) @@ -674,6 +485,10 @@ class QemuVM(object): :param kernel_image: QEMU kernel image path """ + 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, id=self._id, kernel_image=kernel_image)) @@ -702,6 +517,7 @@ class QemuVM(object): kernel_command_line=kernel_command_line)) self._kernel_command_line = kernel_command_line + @asyncio.coroutine def _set_process_priority(self): """ Changes the process priority @@ -744,7 +560,8 @@ class QemuVM(object): 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)) @@ -753,7 +570,7 @@ class QemuVM(object): 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) @@ -773,13 +590,14 @@ class QemuVM(object): 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. @@ -788,96 +606,32 @@ class QemuVM(object): if self.is_running(): # resume the VM if it is paused - self.resume() + yield from 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() + 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. @@ -888,22 +642,23 @@ class QemuVM(object): 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: + if self._process.returncode 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): + @asyncio.coroutine + 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.) - :param timeout: how long to wait for QEMU monitor + :params expected: An array with the string attended (Default None) :returns: result of the command (Match object or None) """ @@ -912,79 +667,111 @@ class QemuVM(object): 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) + 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)) 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.decode().strip() + break except EOFError as e: log.warn("Could not read from QEMU monitor: {}".format(e)) - tn.close() + writer.close() return result + @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, self._project) + self._console = None + if self._monitor: + self._manager.port_manager.release_tcp_port(self._monitor, self._project) + self._monitor = None + + @asyncio.coroutine def _get_vm_status(self): """ 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 = None - - match = self._control_vm("info status", [b"running", b"paused"]) - if match: - result = match.group(0).decode('ascii') - return result + 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" + ]) + if result is None: + return result + return result.rsplit(' ', 1)[1] + @asyncio.coroutine def suspend(self): """ Suspends this QEMU VM. """ - vm_status = self._get_vm_status() - if vm_status == "running": - self._control_vm("stop") + vm_status = yield from self._get_vm_status() + 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: 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() - if vm_status == "paused": - self._control_vm("cont") + vm_status = yield from self._get_vm_status() + 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: 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): + @asyncio.coroutine + def adapter_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 + :param nio: NIO instance to add to the adapter """ try: @@ -995,22 +782,25 @@ class QemuVM(object): 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: - 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: - #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)) + # 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, @@ -1018,7 +808,8 @@ class QemuVM(object): nio=nio, adapter_id=adapter_id)) - def port_remove_nio_binding(self, adapter_id): + @asyncio.coroutine + def adapter_remove_nio_binding(self, adapter_id): """ Removes a port NIO binding. @@ -1035,15 +826,12 @@ class QemuVM(object): 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)) + 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) + if isinstance(nio, NIOUDP): + 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, @@ -1083,8 +871,11 @@ class QemuVM(object): :returns: True or False """ - if self._process and self._process.poll() is None: - return True + if self._process: + if self._process.returncode is None: + return True + else: + self._process = None return False def command(self): @@ -1099,22 +890,23 @@ class QemuVM(object): 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 [] 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 [] + @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"): @@ -1132,40 +924,79 @@ class QemuVM(object): 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, "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): 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)) + 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): @@ -1190,20 +1021,22 @@ class QemuVM(object): 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) 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 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, @@ -1225,16 +1058,28 @@ class QemuVM(object): 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): """ 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()) @@ -1242,4 +1087,41 @@ class QemuVM(object): if additional_options: command.extend(shlex.split(additional_options)) command.extend(self._network_options()) + 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, + "vm_id": self.id + } + # 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) + + 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 diff --git a/gns3server/modules/qemu/schemas.py b/gns3server/modules/qemu/schemas.py deleted file mode 100644 index 32b09664..00000000 --- a/gns3server/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"] -} diff --git a/gns3server/modules/virtualbox/__init__.py b/gns3server/modules/virtualbox/__init__.py index 09f8054e..18253925 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,133 @@ VirtualBox server module. """ -import sys import os -import socket +import sys import shutil +import asyncio import subprocess +import logging -from gns3server.modules import IModule -from gns3server.config import Config +log = logging.getLogger(__name__) + +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. +class VirtualBox(BaseManager): - :param name: module name - :param args: arguments for the module - :param kwargs: named arguments for the module - """ + _VM_CLASS = VirtualBoxVM - def __init__(self, name, *args, **kwargs): + def __init__(self): - # get the vboxmanage location + super().__init__() 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): + @property + def vboxmanage_path(self): """ - 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) + Returns the path to VBoxManage. - @IModule.route("virtualbox.reload") - def vbox_reload(self, request): + :returns: path """ - Reloads a VirtualBox VM instance. - Mandatory request parameters: - - id (VirtualBox VM identifier) + return self._vboxmanage_path - 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"] + 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) + log.debug("Executing VBoxManage with command: {}".format(command)) 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 + 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(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)) - port = request["port"] try: - vbox_instance.stop_capture(port) - except VirtualBoxError as e: - self.send_custom_error(str(e)) - return + 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)) - response = {"port_id": request["port_id"]} - self.send_response(response) + if process.returncode: + # only the first line of the output is useful + vboxmanage_error = stderr_data.decode("utf-8", errors="ignore") + raise VirtualBoxError("VirtualBox has returned an error: {}".format(vboxmanage_error)) - def _execute_vboxmanage(self, user, command): - """ - Executes VBoxManage and return its result. - - :param command: command to execute (list) + return stdout_data.decode("utf-8", errors="ignore").splitlines() - :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): + @asyncio.coroutine + def get_list(self): """ 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) + result = yield from self.execute("list", ["vms"]) + for line in result: + vmname, _ = 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) + extra_data = yield from self.execute("getextradata", [vmname, "GNS3/Clone"]) + if not extra_data[0].strip() == "Value: yes": + # 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 - @IModule.route("virtualbox.echo") - def echo(self, request): + @staticmethod + def get_legacy_vm_workdir(legacy_vm_id, name): """ - Echo end point for testing purposes. + Returns the name of the legacy working directory name for a VM. + + :param legacy_vm_id: legacy VM identifier (not used) + :param name: VM name - :param request: JSON request + :returns: working directory name """ - if request is None: - self.send_param_error() - else: - log.debug("received request {}".format(request)) - self.send_response(request) + return os.path.join("vbox", "{}".format(name)) diff --git a/gns3server/modules/virtualbox/adapters/adapter.py b/gns3server/modules/virtualbox/adapters/adapter.py deleted file mode 100644 index cf439427..00000000 --- a/gns3server/modules/virtualbox/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/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/schemas.py b/gns3server/modules/virtualbox/schemas.py deleted file mode 100644 index 67c0568c..00000000 --- a/gns3server/modules/virtualbox/schemas.py +++ /dev/null @@ -1,432 +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 . - - -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, - }, - "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", - "type": "integer" - }, - "console": { - "description": "console TCP port", - "minimum": 1, - "maximum": 65535, - "type": "integer" - }, - }, - "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 = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to update a 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)", - "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, - }, - "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": ["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"] -} - diff --git a/gns3server/modules/virtualbox/telnet_server.py b/gns3server/modules/virtualbox/telnet_server.py index 0ccde367..bfbee71b 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. @@ -42,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 @@ -57,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): @@ -226,37 +223,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_error.py b/gns3server/modules/virtualbox/virtualbox_error.py index 74b05171..df481c21 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,21 +19,9 @@ Custom exceptions for VirtualBox module. """ +from ..vm_error import VMError -class VirtualBoxError(Exception): - def __init__(self, message, original_exception=None): +class VirtualBoxError(VMError): - 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/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 22294edf..6d610923 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -23,16 +23,17 @@ import sys import shlex import re import os -import subprocess import tempfile import json import socket -import time +import asyncio +from pkg_resources import parse_version from .virtualbox_error import VirtualBoxError -from .adapters.ethernet_adapter import EthernetAdapter -from ..attic import find_unused_port -from .telnet_server import TelnetServer +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 if sys.platform.startswith('win'): import msvcrt @@ -42,101 +43,51 @@ 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") - 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) + def __init__(self, name, vm_id, project, manager, vmname, linked_clone, console=None, adapters=0): - self._name = name - 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 + super().__init__(name, vm_id, project, manager, console=console) + self._maximum_adapters = 8 + self._linked_clone = linked_clone + self._system_properties = {} self._telnet_server_thread = None self._serial_pipe = None + self._closed = False # VirtualBox settings - self._console = console + self._adapters = adapters 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._use_any_adapter = False + self._ram = 0 self._adapter_type = "Intel PRO/1000 MT Desktop (82540EM)" - working_dir_path = os.path.join(working_dir, "vbox") + def __json__(self): - if vbox_id and not os.path.isdir(working_dir_path): - raise VirtualBoxError("Working directory {} doesn't exist".format(working_dir_path)) + return {"name": self.name, + "vm_id": self.id, + "console": self.console, + "project_id": self.project.id, + "vmname": self.vmname, + "headless": self.headless, + "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} - # create the device own working directory - self.working_dir = working_dir_path + @asyncio.coroutine + def _get_system_properties(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 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"]) + properties = yield from self.manager.execute("list", ["systemproperties"]) for prop in properties: try: name, value = prop.split(':', 1) @@ -144,144 +95,170 @@ class VirtualBoxVM(object): 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)): - 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 _get_vm_state(self): + """ + Returns the VM state (e.g. running, paused etc.) - self._maximum_adapters = 8 - self.adapters = 2 # creates 2 adapters by default + :returns: state (string) + """ - log.info("VirtualBox VM {name} [id={id}] has been created".format(name=self._name, - id=self._id)) + results = yield from self.manager.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)) - def defaults(self): + @asyncio.coroutine + def _control_vm(self, params): """ - Returns all the default attribute values for this VirtualBox VM. + Change setting in this VM when running. - :returns: default values (dictionary) - """ + :param params: params to use with sub-command controlvm - 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} + :returns: result of the command. + """ - return vbox_defaults + args = shlex.split(params) + result = yield from self.manager.execute("controlvm", [self._vmname] + args) + return result - @property - def id(self): + @asyncio.coroutine + def _modify_vm(self, params): """ - Returns the unique ID for this VirtualBox VM. + Change setting in this VM when not running. - :returns: id (integer) + :param params: params to use with sub-command modifyvm """ - return self._id + args = shlex.split(params) + yield from self.manager.execute("modifyvm", [self._vmname] + args) - @classmethod - def reset(cls): - """ - Resets allocated instance list. - """ + @asyncio.coroutine + def create(self): - cls._instances.clear() - cls._allocated_console_ports.clear() + 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}' [{id}] created".format(name=self.name, id=self.id)) - @property - def name(self): - """ - Returns the name of this VirtualBox VM. + if self._linked_clone: + 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() + else: + yield from self._create_linked_clone() - :returns: name - """ + if self._adapters: + yield from self.set_adapters(self._adapters) - return self._name + vm_info = yield from self._get_vm_info() + if "memory" in vm_info: + self._ram = int(vm_info["memory"]) - @name.setter - def name(self, new_name): + @asyncio.coroutine + def start(self): """ - Sets the name of this VirtualBox VM. - - :param new_name: name + Starts this VirtualBox VM. """ - log.info("VirtualBox VM {name} [id={id}]: renamed to {new_name}".format(name=self._name, - id=self._id, - new_name=new_name)) + # resume the VM if it is paused + vm_state = yield from self._get_vm_state() + if vm_state == "paused": + yield from self.resume() + return - self._name = new_name + # 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") - @property - def working_dir(self): - """ - Returns current working directory + yield from self._set_network_options() + yield from self._set_serial_console() - :returns: path to the working directory - """ + args = [self._vmname] + if self._headless: + args.extend(["--type", "headless"]) + result = yield from self.manager.execute("startvm", args) + log.info("VirtualBox VM '{name}' [{id}] started".format(name=self.name, id=self.id)) + log.debug("Start result: {}".format(result)) - return self._working_dir + # add a guest property to let the VM know about the GNS3 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.manager.execute("guestproperty", ["set", self._vmname, "ProjectDirInGNS3", self.working_dir]) - @working_dir.setter - def working_dir(self, working_dir): - """ - Sets the working directory this VirtualBox VM. + if self._enable_remote_console: + self._start_remote_console() - :param working_dir: path to the working directory + @asyncio.coroutine + def stop(self): + """ + Stops 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)) + 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}' [{id}] stopped".format(name=self.name, id=self.id)) + log.debug("Stop result: {}".format(result)) - 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)) + 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 console(self): - """ - Returns the TCP console port. + 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_number + 1)) + yield from self._modify_vm("--cableconnected{} off".format(adapter_number + 1)) + yield from self._modify_vm("--nic{} null".format(adapter_number + 1)) - :returns: console port (integer) + @asyncio.coroutine + def suspend(self): + """ + Suspends this VirtualBox VM. """ - return self._console + vm_state = yield from self._get_vm_state() + if vm_state == "running": + yield from self._control_vm("pause") + log.info("VirtualBox VM '{name}' [{id}] suspended".format(name=self.name, id=self.id)) + else: + log.warn("VirtualBox VM '{name}' [{id}] cannot be suspended, current state: {state}".format(name=self.name, + id=self.id, + state=vm_state)) - @console.setter - def console(self, console): + @asyncio.coroutine + def resume(self): """ - Sets the TCP console port. - - :param console: console port (integer) + Resumes this VirtualBox VM. """ - if console in self._allocated_console_ports: - raise VirtualBoxError("Console port {} is already used by another VirtualBox VM".format(console)) + yield from self._control_vm("resume") + log.info("VirtualBox VM '{name}' [{id}] resumed".format(name=self.name, id=self.id)) - self._allocated_console_ports.remove(self._console) - self._console = console - self._allocated_console_ports.append(self._console) + @asyncio.coroutine + def reload(self): + """ + Reloads this VirtualBox VM. + """ - log.info("VirtualBox VM {name} [id={id}]: console port set to {port}".format(name=self._name, - id=self._id, - port=console)) + result = yield from self._control_vm("reset") + log.info("VirtualBox VM '{name}' [{id}] reloaded".format(name=self.name, id=self.id)) + log.debug("Reload result: {}".format(result)) + @asyncio.coroutine def _get_all_hdd_files(self): hdds = [] - properties = self._execute("list", ["hdds"]) + properties = yield from self.manager.execute("list", ["hdds"]) for prop in properties: try: name, value = prop.split(':', 1) @@ -291,42 +268,59 @@ class VirtualBoxVM(object): 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)) + 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)) - def delete(self): + yield from self._storage_attach('--storagectl "{}" --port {} --device {} --type hdd --medium "{}"'.format(hdd_info["controller"], + hdd_info["port"], + hdd_info["device"], + hdd_file)) + + @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._closed: + # 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._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._project) - if self.console and self.console in self._allocated_console_ports: - self._allocated_console_ports.remove(self.console) + yield from self.stop() if self._linked_clone: hdd_table = [] - if os.path.exists(self._working_dir): - hdd_files = self._get_all_hdd_files() - vm_info = self._get_vm_info() + if os.path.exists(self.working_dir): + hdd_files = yield from self._get_all_hdd_files() + 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: @@ -334,7 +328,14 @@ class VirtualBoxVM(object): 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)) + 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( { "hdd": os.path.basename(value), @@ -344,46 +345,21 @@ class VirtualBoxVM(object): } ) - self._execute("unregistervm", [self._vmname]) + 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: 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) except OSError as e: - raise VirtualBoxError("Could not write HDD info file: {}".format(e)) + 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} [id={id}] has been deleted".format(name=self._name, - id=self._id)) - - def clean_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) - - 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} [id={id}] has been deleted (including associated files)".format(name=self._name, - id=self._id)) + log.info("VirtualBox VM '{name}' [{id}] closed".format(name=self.name, id=self.id)) + self._closed = True @property def headless(self): @@ -404,9 +380,9 @@ class VirtualBoxVM(object): """ 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}' [{id}] has enabled the headless mode".format(name=self.name, id=self.id)) 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}' [{id}] has disabled the headless mode".format(name=self.name, id=self.id)) self._headless = headless @property @@ -419,8 +395,8 @@ class VirtualBoxVM(object): 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 @@ -428,48 +404,77 @@ class VirtualBoxVM(object): """ if enable_remote_console: - log.info("VirtualBox VM {name} [id={id}] has enabled the console".format(name=self._name, id=self._id)) - self._start_remote_console() + log.info("VirtualBox VM '{name}' [{id}] has enabled the console".format(name=self.name, id=self.id)) + vm_state = yield from self._get_vm_state() + if vm_state == "running": + 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}' [{id}] has disabled the console".format(name=self.name, id=self.id)) 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): """ - 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 """ - log.info("VirtualBox VM {name} [id={id}] has set the VM name to {vmname}".format(name=self._name, id=self._id, vmname=vmname)) if self._linked_clone: - self._modify_vm('--name "{}"'.format(vmname)) + 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 @property def adapters(self): """ - Returns the number of Ethernet adapters for this VirtualBox VM instance. + Returns the number of adapters configured for this VirtualBox VM. :returns: number of adapters """ - return len(self._ethernet_adapters) + return self._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. @@ -477,44 +482,42 @@ class VirtualBoxVM(object): """ # 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)) 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_number in range(0, adapters): 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)) + self._adapters = len(self._ethernet_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): + 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 - 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)) + 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): @@ -535,43 +538,11 @@ class VirtualBoxVM(object): """ 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)) - 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 _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() - + @asyncio.coroutine def _get_vm_info(self): """ Returns this VM info. @@ -580,7 +551,7 @@ class VirtualBoxVM(object): """ vm_info = {} - results = 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) @@ -589,20 +560,7 @@ class VirtualBoxVM(object): 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. @@ -611,7 +569,7 @@ class VirtualBoxVM(object): """ # 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": @@ -627,47 +585,27 @@ class VirtualBoxVM(object): 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.manager.execute("modifyvm", args) + @asyncio.coroutine def _storage_attach(self, params): """ Change storage medium in this VM. @@ -676,8 +614,9 @@ class VirtualBoxVM(object): """ args = shlex.split(params) - self._execute("storageattach", [self._vmname] + args) + yield from self.manager.execute("storageattach", [self._vmname] + args) + @asyncio.coroutine def _get_nic_attachements(self, maximum_adapters): """ Returns NIC attachements. @@ -687,85 +626,82 @@ class VirtualBoxVM(object): """ nics = [] - vm_info = self._get_vm_info() - for adapter_id in range(0, maximum_adapters): - entry = "nic{}".format(adapter_id + 1) + vm_info = yield from self._get_vm_info() + 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) + nics.append(value.lower()) else: nics.append(None) return nics + @asyncio.coroutine def _set_network_options(self): """ Configures network options. """ - nic_attachements = 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 - self._modify_vm("--nic{} {}".format(adapter_id + 1, attachement)) - continue + 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: + 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)) + yield from self._modify_vm("--nictrace{} off".format(adapter_number + 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] - self._execute("modifyvm", args) - - 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)) + 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_number + 1), vbox_adapter_type] + yield from self.manager.execute("modifyvm", args) + + 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: - self._modify_vm("--nictrace{} on".format(adapter_id + 1)) - 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("--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)) - 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): """ 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.manager.execute("snapshot", [self._vmname, "take", "GNS3 Linked Base for clones"]) log.debug("GNS3 snapshot created: {}".format(result)) args = [self._vmname, @@ -774,20 +710,20 @@ class VirtualBoxVM(object): "--options", "link", "--name", - self._name, + self.name, "--basefolder", - self._working_dir, + self.working_dir, "--register"] - result = self._execute("clonevm", args) - log.debug("cloned VirtualBox VM: {}".format(result)) + result = yield from self.manager.execute("clonevm", args) + log.debug("VirtualBox VM: {} cloned".format(result)) self._vmname = self._name - 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 = self._execute("snapshot", args) - log.debug("snapshot reset created: {}".format(result)) + args = [self._vmname, "take", "reset"] + result = yield from self.manager.execute("snapshot", args) + log.debug("Snapshot 'reset' created: {}".format(result)) def _start_remote_console(self): """ @@ -796,12 +732,15 @@ class VirtualBoxVM(object): # 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: 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) + 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: @@ -809,7 +748,10 @@ class VirtualBoxVM(object): 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) + 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): @@ -818,211 +760,122 @@ class VirtualBoxVM(object): """ if self._telnet_server_thread: - 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!") + 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() self._serial_pipe = None - def start(self): - """ - Starts this VirtualBox VM. + @asyncio.coroutine + def adapter_add_nio_binding(self, adapter_number, nio): """ + Adds an adapter NIO binding. - # 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 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)) - - def port_add_nio_binding(self, adapter_id, nio): - """ - Adds a port 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 = 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_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={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)) - def port_remove_nio_binding(self, adapter_id): + @asyncio.coroutine + def adapter_remove_nio_binding(self, adapter_number): """ - Removes a port NIO binding. + 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 = 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_number + 1)) + yield from self._control_vm("nic{} null".format(adapter_number + 1)) nio = adapter.get_nio(0) + if isinstance(nio, NIOUDP): + self.manager.port_manager.release_udp_port(nio.lport, self._project) 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}' [{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)) - - try: - os.makedirs(os.path.dirname(output_file)) - except FileExistsError: - pass - except OSError as e: - raise VirtualBoxError("Could not create captures directory {}".format(e)) + 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_number}".format(name=self.name, + id=self.id, + adapter_number=adapter_number)) - 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)) - - 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={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/modules/vpcs/adapters/ethernet_adapter.py b/gns3server/modules/vm_error.py similarity index 62% rename from gns3server/modules/vpcs/adapters/ethernet_adapter.py rename to gns3server/modules/vm_error.py index bbca7f40..55cfc4cf 100644 --- a/gns3server/modules/vpcs/adapters/ethernet_adapter.py +++ b/gns3server/modules/vm_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 @@ -15,17 +15,21 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from .adapter import Adapter +class VMError(Exception): -class EthernetAdapter(Adapter): - """ - VPCS Ethernet adapter. - """ + def __init__(self, message, original_exception=None): - def __init__(self): - Adapter.__init__(self, interfaces=1) + 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 "VPCS Ethernet adapter" + return self._message diff --git a/gns3server/modules/vpcs/__init__.py b/gns3server/modules/vpcs/__init__.py index aa0f216e..bae14fe1 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 @@ -20,633 +20,61 @@ 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 ..base_manager import BaseManager 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 .vpcs_vm import VPCSVM -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(BaseManager): + _VM_CLASS = VPCSVM + def __init__(self): + super().__init__() + self._free_mac_ids = {} + self._used_mac_ids = {} -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 + @asyncio.coroutine + def create_vm(self, *args, **kwargs): + vm = yield from super().create_vm(*args, **kwargs) + self._free_mac_ids.setdefault(vm.project.id, list(range(0, 255))) 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) + 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 - Optional request parameters: - - any setting to update - - script_file_base64 (base64 encoded) + @asyncio.coroutine + def close_vm(self, vm_id, *args, **kwargs): - Response parameters: - - updated settings + 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().close_vm(vm_id, *args, **kwargs) + return vm - :param request: JSON request + def get_mac_id(self, vm_id): """ + Get an unique VPCS mac id - # 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): + :param vm_id: ID of the VPCS VM + :returns: VPCS MAC id """ - Starts a VPCS instance. - - Mandatory request parameters: - - id (VPCS instance identifier) - Response parameters: - - True on success + return self._used_mac_ids.get(vm_id, 1) - :param request: JSON request + @staticmethod + def get_legacy_vm_workdir(legacy_vm_id, name): """ + Returns the name of the legacy working directory name for a VM. - # 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 legacy_vm_id: legacy VM identifier (integer) + :param name: VM name (not used) - :param request: JSON request + :returns: working directory name """ - if request is None: - self.send_param_error() - else: - log.debug("received request {}".format(request)) - self.send_response(request) + return os.path.join("vpcs", "pc-{}".format(legacy_vm_id)) 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/nios/nio_tap.py b/gns3server/modules/vpcs/nios/nio_tap.py deleted file mode 100644 index 4c3ed6b2..00000000 --- a/gns3server/modules/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/vpcs/nios/nio_udp.py b/gns3server/modules/vpcs/nios/nio_udp.py deleted file mode 100644 index 0527f675..00000000 --- a/gns3server/modules/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/vpcs/schemas.py b/gns3server/modules/vpcs/schemas.py deleted file mode 100644 index 6556895b..00000000 --- a/gns3server/modules/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/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py deleted file mode 100644 index 65664f39..00000000 --- a/gns3server/modules/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/vpcs/vpcs_error.py b/gns3server/modules/vpcs/vpcs_error.py index 167129ba..b8e99d4b 100644 --- a/gns3server/modules/vpcs/vpcs_error.py +++ b/gns3server/modules/vpcs/vpcs_error.py @@ -19,21 +19,9 @@ Custom exceptions for VPCS module. """ +from ..vm_error import VMError -class VPCSError(Exception): - def __init__(self, message, original_exception=None): +class VPCSError(VMError): - 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/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py new file mode 100644 index 00000000..18a08077 --- /dev/null +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -0,0 +1,419 @@ +# -*- 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 VM management (creates command line, processes, files etc.) in +order to run an VPCS instance. +""" + +import os +import sys +import subprocess +import signal +import re +import asyncio +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 + + +import logging +log = logging.getLogger(__name__) + + +class VPCSVM(BaseVM): + module_name = 'vpcs' + + """ + VPCS vm implementation. + + :param name: The name of this VM + :param vm_id: VPCS instance identifier + :param project: Project instance + :param manager: Parent VM Manager + :param console: TCP console port + :param startup_script: Content of vpcs startup script file + """ + + def __init__(self, name, vm_id, project, manager, console=None, startup_script=None): + + super().__init__(name, vm_id, project, manager, console=console) + + self._command = [] + self._process = None + self._vpcs_stdout_file = "" + self._started = False + + # VPCS settings + if startup_script is not None: + self.startup_script = startup_script + self._ethernet_adapter = EthernetAdapter() # one adapter with 1 Ethernet interface + + @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._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._project) + + self._terminate_process() + + @asyncio.coroutine + def _check_requirements(self): + """ + Check if VPCS is available with the correct version + """ + path = self.vpcs_path + if not path: + raise VPCSError("No path to a VPCS executable has been set") + + if not os.path.isfile(path): + raise VPCSError("VPCS program '{}' is not accessible".format(path)) + + if not os.access(path, os.X_OK): + raise VPCSError("VPCS program '{}' is not executable".format(path)) + + yield from self._check_vpcs_version() + + def __json__(self): + + return {"name": self.name, + "vm_id": self.id, + "console": self._console, + "project_id": self.project.id, + "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): + """ + Returns the VPCS executable path. + + :returns: path to VPCS + """ + + path = self._manager.config.get_section_config("VPCS").get("vpcs_path", "vpcs") + if path == "vpcs": + path = shutil.which("vpcs") + return path + + @BaseVM.name.setter + def name(self, new_name): + """ + Sets the name of this VPCS vm. + + :param new_name: name + """ + + if self.script_file: + 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): + """Return the content of the current startup script""" + + script_file = self.script_file + if script_file is None: + return None + + try: + with open(script_file) as f: + return f.read() + except OSError as e: + raise VPCSError("Can't read VPCS startup file '{}'".format(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 + """ + + try: + 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)) + + @asyncio.coroutine + def _check_vpcs_version(self): + """ + Checks if the VPCS executable version is >= 0.5b1. + """ + try: + 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) + 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.vpcs_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. + """ + + yield from 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") + + 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.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.vpcs_path, e, vpcs_stdout)) + raise VPCSError("Could not start VPCS {}: {}\n{}".format(self.vpcs_path, e, vpcs_stdout)) + + @asyncio.coroutine + def stop(self): + """ + Stops the VPCS process. + """ + + if self.is_running(): + self._terminate_process() + try: + yield from asyncio.wait_for(self._process.wait(), timeout=3) + except asyncio.TimeoutError: + if self._process.returncode is None: + log.warn("VPCS process {} is still running... killing it".format(self._process.pid)) + self._process.kill() + + 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 _terminate_process(self): + """Terminate 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. + 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_number, nio): + """ + Adds a port NIO binding. + + :param port_number: port number + :param nio: NIO instance to add to the slot/port + """ + + 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_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)) + return nio + + def port_remove_nio_binding(self, port_number): + """ + Removes a port NIO binding. + + :param port_number: port number + + :returns: NIO instance + """ + + 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_number) + if isinstance(nio, NIOUDP): + 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, + id=self.id, + nio=nio, + port_number=port_number)) + 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 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' + + If no 'scriptfile' specified, vpcs will read and execute the file named + 'startup.vpc' if it exsits in the current directory. + + """ + + command = [self.vpcs_path] + command.extend(["-p", str(self._console)]) # listen to console port + + nio = self._ethernet_adapter.get_nio(0) + if nio: + 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 isinstance(nio, NIOTAP): + # TAP interface + command.extend(["-e"]) + command.extend(["-d", nio.tap_vm]) + + 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]) + return command + + @property + def script_file(self): + """ + Returns the script-file for this VPCS instance. + + :returns: path to 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/modules/iou/nios/__init__.py b/gns3server/schemas/__init__.py similarity index 100% rename from gns3server/modules/iou/nios/__init__.py rename to gns3server/schemas/__init__.py diff --git a/gns3server/modules/dynamips/schemas/atmsw.py b/gns3server/schemas/dynamips_device.py similarity index 59% rename from gns3server/modules/dynamips/schemas/atmsw.py rename to gns3server/schemas/dynamips_device.py index 37669478..9f173c49 100644 --- a/gns3server/modules/dynamips/schemas/atmsw.py +++ b/gns3server/schemas/dynamips_device.py @@ -15,77 +15,151 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -ATMSW_CREATE_SCHEMA = { + +DEVICE_CREATE_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to create a new ATM switch instance", + "description": "Request validation to create a new Dynamips device instance", "type": "object", "properties": { "name": { - "description": "ATM switch name", + "description": "Dynamips device name", + "type": "string", + "minLength": 1, + }, + "device_id": { + "description": "Dynamips device 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 + ] + }, + "device_type": { + "description": "Dynamips device type", "type": "string", "minLength": 1, }, }, "additionalProperties": False, - "required": ["name"] + "required": ["name", "device_type"] } -ATMSW_DELETE_SCHEMA = { +DEVICE_UPDATE_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to delete an ATM switch instance", + "description": "Dynamips device instance", "type": "object", - "properties": { - "id": { - "description": "ATM switch instance ID", - "type": "integer" + "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 }, }, - "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", + "description": "Dynamips device instance name", "type": "string", "minLength": 1, }, + "ports": { + "type": "array", + "items": [ + {"type": "object", + "oneOf": [ + {"$ref": "#/definitions/EthernetSwitchPort"} + ]}, + ] + } }, "additionalProperties": False, - "required": ["id"] } -ATMSW_ALLOCATE_UDP_PORT_SCHEMA = { +DEVICE_OBJECT_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to allocate an UDP port for an ATM switch instance", + "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": { - "id": { - "description": "ATM switch instance ID", - "type": "integer" + "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, }, - "port_id": { - "description": "Unique port identifier for the ATM switch instance", - "type": "integer" + "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": ["id", "port_id"] + "required": ["name", "device_id", "project_id"] } -ATMSW_ADD_NIO_SCHEMA = { +DEVICE_NIO_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to add a NIO for an ATM switch instance", + "description": "Request validation to add a NIO for a Dynamips device instance", "type": "object", - "definitions": { "UDP": { "description": "UDP Network Input/Output", @@ -210,27 +284,9 @@ ATMSW_ADD_NIO_SCHEMA = { "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"}, @@ -241,48 +297,37 @@ ATMSW_ADD_NIO_SCHEMA = { {"$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, + "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": ["id", "port"] + "required": ["nio"] } -ATMSW_START_CAPTURE_SCHEMA = { +DEVICE_CAPTURE_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to start a packet capture on an ATM switch instance port", + "description": "Request validation to start a packet capture on an Device 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", @@ -295,28 +340,5 @@ ATMSW_START_CAPTURE_SCHEMA = { }, }, "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"] + "required": ["capture_file_name", "data_link_type"] } diff --git a/gns3server/modules/dynamips/schemas/vm.py b/gns3server/schemas/dynamips_vm.py similarity index 52% rename from gns3server/modules/dynamips/schemas/vm.py rename to gns3server/schemas/dynamips_vm.py index ae261ffa..934646c8 100644 --- a/gns3server/modules/dynamips/schemas/vm.py +++ b/gns3server/schemas/dynamips_vm.py @@ -15,22 +15,33 @@ # 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", + "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}$" @@ -42,14 +53,73 @@ VM_CREATE_SCHEMA = { "pattern": "^[0-9]{4}(XM)?$" }, "image": { - "description": "path to the IOS image file", + "description": "path to the IOS image", + "type": "string", + "minLength": 1, + }, + "startup_config": { + "description": "path to the IOS startup configuration file", + "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", - "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" + }, "console": { "description": "console TCP port", "type": "integer", @@ -68,97 +138,132 @@ VM_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", - } - }, - "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" + "minLength": 1, }, - }, - "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" + "slot0": { + "description": "Network module slot 0", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] }, - }, - "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" + "slot1": { + "description": "Network module slot 1", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] }, - }, - "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" + "slot2": { + "description": "Network module slot 2", + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] }, - }, - "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" + "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"] + "required": ["name", "platform", "image", "ram"] } -#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", + "description": "Request validation to update a Dynamips VM instance", "type": "object", "properties": { - "id": { - "description": "VM instance ID", - "type": "integer" - }, "name": { - "description": "Router name", + "description": "Dynamips VM instance name", "type": "string", "minLength": 1, }, @@ -168,6 +273,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", @@ -178,11 +289,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" @@ -220,10 +339,6 @@ VM_UPDATE_SCHEMA = { "description": "exec area value", "type": "integer", }, - "jit_sharing_group": { - "description": "JIT sharing group", - "type": "integer", - }, "disk0": { "description": "disk0 size in MB", "type": "integer" @@ -232,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", @@ -372,162 +481,12 @@ VM_UPDATE_SCHEMA = { }, }, "additionalProperties": False, - "required": ["id"] } -VM_START_CAPTURE_SCHEMA = { +VM_NIO_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to start a packet capture on a VM instance port", + "description": "Request validation to add a NIO for a Dynamips VM instance", "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", @@ -652,68 +611,290 @@ VM_ADD_NIO_SCHEMA = { "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_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", + "type": "object", "properties": { - "id": { - "description": "VM instance ID", + "dynamips_id": { + "description": "ID to use with Dynamips", "type": "integer" }, - "port_id": { - "description": "Unique port identifier for the VM instance", + "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}$" + }, + "name": { + "description": "Dynamips VM instance name", + "type": "string", + "minLength": 1, + }, + "platform": { + "description": "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", + "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" }, - "slot": { - "description": "Slot number", + "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", - "minimum": 0, - "maximum": 6 }, - "port": { - "description": "Port number", + "idlesleep": { + "description": "idlesleep value", "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", + "exec_area": { + "description": "exec area value", + "type": "integer", + }, + "disk0": { + "description": "disk0 size in MB", + "type": "integer" + }, + "disk1": { + "description": "disk1 size in MB", + "type": "integer" + }, + "console": { + "description": "console TCP port", + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "aux": { + "description": "auxiliary console TCP port", + "type": ["integer", "null"], + "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": [ - {"$ref": "#/definitions/UDP"}, - {"$ref": "#/definitions/Ethernet"}, - {"$ref": "#/definitions/LinuxEthernet"}, - {"$ref": "#/definitions/TAP"}, - {"$ref": "#/definitions/UNIX"}, - {"$ref": "#/definitions/VDE"}, - {"$ref": "#/definitions/NULL"}, + {"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", "port_id", "slot", "port", "nio"] + "required": ["name", "vm_id", "project_id", "dynamips_id"] } -VM_DELETE_NIO_SCHEMA = { +VM_CONFIGS_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to delete a NIO for a VM instance", + "description": "Request validation to get the startup and private configuration file", "type": "object", "properties": { - "id": { - "description": "VM instance ID", - "type": "integer" - }, - "slot": { - "description": "Slot number", - "type": "integer", - "minimum": 0, - "maximum": 6 + "startup_config_content": { + "description": "Content of the startup configuration file", + "type": ["string", "null"], + "minLength": 1, }, - "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 + "private_config_content": { + "description": "Content of the private configuration file", + "type": ["string", "null"], + "minLength": 1, }, }, "additionalProperties": False, - "required": ["id", "slot", "port"] + "required": ["startup_config_content", "private_config_content"] } diff --git a/gns3server/schemas/iou.py b/gns3server/schemas/iou.py new file mode 100644 index 00000000..5793488a --- /dev/null +++ b/gns3server/schemas/iou.py @@ -0,0 +1,318 @@ +# -*- 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"] + }, + "path": { + "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", "null"] + }, + "nvram": { + "description": "Allocated NVRAM KB", + "type": ["integer", "null"] + }, + "l1_keepalives": { + "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"] + }, + "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, + "required": ["name", "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"] + }, + "path": { + "description": "Path of iou binary", + "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"] + }, + "ram": { + "description": "Allocated RAM MB", + "type": ["integer", "null"] + }, + "nvram": { + "description": "Allocated NVRAM KB", + "type": ["integer", "null"] + }, + "l1_keepalives": { + "description": "Always up ethernet interface", + "type": ["boolean", "null"] + }, + "initial_config_content": { + "description": "Initial configuration of the IOU", + "type": ["string", "null"] + }, + "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, +} + +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}$" + }, + "path": { + "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" + }, + "l1_keepalives": { + "description": "Always up ethernet interface", + "type": "boolean" + }, + "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"] + }, + "iourc_path": { + "description": "Path of the iourc file used by remote servers", + "type": ["string", "null"] + } + }, + "additionalProperties": False, + "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 = { + "$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"] +} + +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"] +} + +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, + }, + }, + "additionalProperties": False, + "required": ["content"] +} diff --git a/gns3server/schemas/project.py b/gns3server/schemas/project.py new file mode 100644 index 00000000..3e9dfa6d --- /dev/null +++ b/gns3server/schemas/project.py @@ -0,0 +1,105 @@ +# -*- 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_CREATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/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"], + "minLength": 1 + }, + "project_id": { + "description": "Project UUID", + "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}$" + }, + "temporary": { + "description": "If project is a temporary project", + "type": "boolean" + }, + }, + "additionalProperties": False, +} + +PROJECT_UPDATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to update a Project instance", + "type": "object", + "properties": { + "name": { + "description": "Project name", + "type": ["string", "null"], + "minLength": 1 + }, + "temporary": { + "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, +} + +PROJECT_OBJECT_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/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", + "minLength": 1 + }, + "path": { + "description": "Directory of the project on the server", + "type": "string", + "minLength": 1 + }, + "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}$" + }, + "temporary": { + "description": "If project is a temporary project", + "type": "boolean" + }, + }, + "additionalProperties": False, + "required": ["location", "project_id", "temporary"] +} diff --git a/gns3server/schemas/qemu.py b/gns3server/schemas/qemu.py new file mode 100644 index 00000000..7632525a --- /dev/null +++ b/gns3server/schemas/qemu.py @@ -0,0 +1,417 @@ +# -*- 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": { + "vm_id": { + "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", + "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", "null"] + }, + "monitor": { + "description": "monitor TCP port", + "minimum": 1, + "maximum": 65535, + "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"], + }, + "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"] + }, + "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, + "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", "null"], + "minLength": 1, + }, + "qemu_path": { + "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", "null"], + }, + "hdb_disk_image": { + "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"] + }, + "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, +} + +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 + }, + }, + "oneOf": [ + {"$ref": "#/definitions/UDP"}, + {"$ref": "#/definitions/Ethernet"}, + ], + "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", + }, + "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" + }, + "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", + }, + "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", "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" + ] +} + +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/gns3dms/version.py b/gns3server/schemas/version.py similarity index 58% rename from gns3dms/version.py rename to gns3server/schemas/version.py index 545a0060..95e08507 100644 --- a/gns3dms/version.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,13 +15,19 @@ # 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) +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", + }, + "local": { + "description": "Either this is a local server", + "type": "boolean", + } + } +} diff --git a/gns3server/schemas/virtualbox.py b/gns3server/schemas/virtualbox.py new file mode 100644 index 00000000..930cbbbe --- /dev/null +++ b/gns3server/schemas/virtualbox.py @@ -0,0 +1,264 @@ +# -*- 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 . + + +VBOX_CREATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to create a new VirtualBox VM instance", + "type": "object", + "properties": { + "vm_id": { + "description": "VirtualBox 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 + ] + }, + "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 + }, + "use_any_adapter": { + "description": "allow GNS3 to use any VirtualBox adapter", + "type": "boolean", + }, + "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" + }, + "ram": { + "description": "Amount of RAM", + "minimum": 0, + "maximum": 65535, + "type": "integer" + }, + "headless": { + "description": "headless mode", + "type": "boolean" + }, + }, + "additionalProperties": False, + "required": ["name", "vmname", "linked_clone"], +} + +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 + }, + "use_any_adapter": { + "description": "allow GNS3 to use any VirtualBox adapter", + "type": "boolean", + }, + "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" + }, + "ram": { + "description": "Amount of RAM", + "minimum": 0, + "maximum": 65535, + "type": "integer" + }, + "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_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_file_name": { + "description": "Capture file name", + "type": "string", + "minLength": 1, + }, + }, + "additionalProperties": False, + "required": ["capture_file_name"] +} + +VBOX_OBJECT_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "VirtualBox VM instance", + "type": "object", + "properties": { + "name": { + "description": "VirtualBox VM instance name", + "type": "string", + "minLength": 1, + }, + "vm_id": { + "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}$" + }, + "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}$" + }, + "vmname": { + "description": "VirtualBox VM name (in VirtualBox itself)", + "type": "string", + "minLength": 1, + }, + "enable_remote_console": { + "description": "enable the remote console", + "type": "boolean" + }, + "headless": { + "description": "headless mode", + "type": "boolean" + }, + "adapters": { + "description": "number of adapters", + "type": "integer", + "minimum": 0, + "maximum": 36, # 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", + "type": "string", + "minLength": 1, + }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, + "ram": { + "description": "Amount of RAM", + "minimum": 0, + "maximum": 65535, + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["name", "vm_id", "project_id"] +} diff --git a/gns3server/schemas/vpcs.py b/gns3server/schemas/vpcs.py new file mode 100644 index 00000000..14cad8be --- /dev/null +++ b/gns3server/schemas/vpcs.py @@ -0,0 +1,175 @@ +# -*- 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 VM name", + "type": "string", + "minLength": 1, + }, + "vm_id": { + "description": "VPCS 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"] + }, + "startup_script": { + "description": "Content of the VPCS startup script", + "type": ["string", "null"] + }, + }, + "additionalProperties": False, + "required": ["name"] +} + +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 VM name", + "type": ["string", "null"], + "minLength": 1, + }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": ["integer", "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#", + "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 + }, + "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/TAP"}, + ], + "additionalProperties": True, + "required": ["type"] +} + +VPCS_OBJECT_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "VPCS instance", + "type": "object", + "properties": { + "name": { + "description": "VPCS VM name", + "type": "string", + "minLength": 1, + }, + "vm_id": { + "description": "VPCS 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}$" + }, + "startup_script": { + "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", "startup_script_path"] +} diff --git a/gns3server/server.py b/gns3server/server.py index 5d748dc2..dca8ff29 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,317 +19,214 @@ 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 ipaddress -import base64 -import uuid - -from pkg_resources import parse_version +import asyncio +import aiohttp +import functools +import types +import time + +from .web.route import Route +from .web.request_handler import RequestHandler 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 +from .modules.port_manager import PortManager + +# do not delete this import +import gns3server.handlers 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): self._host = host self._port = port - self._router = None - self._stream = None + self._loop = None + self._handler = None + self._start_time = time.time() + self._port_manager = PortManager(host) - 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 + @staticmethod + def instance(host=None, port=None): + """ + Singleton to return only one instance of Server. - 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 = [] + :returns: instance of 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 - # 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()) + @asyncio.coroutine + def _run_application(self, handler, ssl_context=None): try: - os.makedirs(self._projects_dir) - log.info("projects directory '{}' created".format(self._projects_dir)) - except FileExistsError: - pass + server = yield from self._loop.create_server(handler, self._host, self._port, ssl=ssl_context) except OSError as e: - log.error("could not create the projects directory {}: {}".format(self._projects_dir, e)) + log.critical("Could not start the server: {}".format(e)) + self._loop.stop() + return + return server - def load_modules(self): + @asyncio.coroutine + def shutdown_server(self): """ - Loads the modules. + Cleanly shutdown the server. """ - #======================================================================= - # 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 - #======================================================================= - - # 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) + if self._handler: + yield from self._handler.finish_connections() 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) - - self._modules.append(instance) - destinations = instance.destinations() - for destination in destinations: - JSONRPCWebSocket.register_destination(destination, instance.name) - instance.start() # starts the new process + log.debug("Unloading module {}".format(module.__name__)) + m = module.instance() + yield from m.unload() - def run(self): - """ - Starts the Tornado web server and ZeroMQ 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") + if self._port_manager.tcp_ports: + log.warning("TCP ports are still used {}".format(self._port_manager.tcp_ports)) - cloud_settings = { + if self._port_manager.udp_ports: + log.warning("UDP ports are still used {}".format(self._port_manager.udp_ports)) - "required_user" : cloud_config['WEB_USERNAME'], - "required_pass" : cloud_config['WEB_PASSWORD'], - } + self._loop.stop() - settings.update(cloud_settings) + def _signal_handling(self): - if cloud_config["SSL_ENABLED"] == "yes": - ssl_options = { - "certfile" : cloud_config["SSL_CRT"], - "keyfile" : cloud_config["SSL_KEY"], - } + @asyncio.coroutine + def signal_handler(signame): + log.warning("Server has got signal {}, exiting...".format(signame)) + yield from self.shutdown_server() - 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" + signals = ["SIGTERM", "SIGINT"] + if sys.platform.startswith("win"): + signals.extend(["SIGBREAK"]) else: - templates_dir = pkg_resources.resource_filename("gns3server", "templates") - tornado_app = tornado.web.Application(self.handlers, - template_path=templates_dir, - **settings) # FIXME: debug mode! + signals.extend(["SIGHUP", "SIGQUIT"]) + for signal_name in signals: + 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) + else: + self._loop.add_signal_handler(getattr(signal, signal_name), callback) + + def _reload_hook(self): + + @asyncio.coroutine + def reload(): + + log.info("Reloading") + yield from self.shutdown_server() + 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)) + asyncio.async(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: - 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 - - 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) - + 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 + + @asyncio.coroutine + def start_shell(self): try: - ioloop.start() - except (KeyboardInterrupt, SystemExit): - log.info("\nExiting...") - self._cleanup() + 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 _create_zmq_router(self): + def run(self): """ - Creates the ZeroMQ router socket to send - requests to modules. - - :returns: ZeroMQ router socket + Starts the server. """ - 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) + 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 + 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.5, wakeup) + loop.call_later(0.5, wakeup) + asyncio.set_event_loop(loop) + + ssl_context = None + if server_config.getboolean("ssl"): + if sys.platform.startswith("win"): + log.critical("SSL mode is not supported on Windows") raise SystemExit - log.info("ZeroMQ server listening to 127.0.0.1:{}".format(self._zmq_port)) - return self._router + ssl_context = self._create_ssl_context(server_config) - 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() + 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__)) + m = module.instance() + m.port_manager = self._port_manager - ioloop = tornado.ioloop.IOLoop.instance() - ioloop.stop() + log.info("Starting server on {}:{}".format(self._host, self._port)) + self._handler = app.make_handler(handler=RequestHandler) + self._loop.run_until_complete(self._run_application(self._handler, ssl_context)) + self._signal_handling() - def _cleanup(self, signum=None, graceful=True): - """ - Shutdowns any running module processes - and adds a callback to stop the event loop & ZeroMQ + if server_config.getboolean("live"): + log.info("Code live reload is enabled, watching for file changes") + self._loop.call_later(1, self._reload_hook) - :param signum: signal number (if called by a signal handler) - :param graceful: gracefully stop the modules - """ + if server_config.getboolean("shell"): + asyncio.async(self.start_shell()) - # 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) + try: + self._loop.run_forever() + except TypeError as e: + # This is to ignore an asyncio.windows_events exception + # 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)) diff --git a/gns3server/start_server.py b/gns3server/start_server.py deleted file mode 100644 index c32ed3ca..00000000 --- a/gns3server/start_server.py +++ /dev/null @@ -1,254 +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) - -""" -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) 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..a894e467 100644 --- a/gns3server/templates/upload.html +++ b/gns3server/templates/upload.html @@ -1,20 +1,22 @@ - - - -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 path:
+ File type: +
+
+ +
+ {%if files%} +

Files on {{gns3_host}}

+ {%for file in files%} +

{{file}}

+ {%endfor%} + {%endif%} +{% endblock %} diff --git a/gns3server/modules/qemu/adapters/__init__.py b/gns3server/utils/__init__.py similarity index 100% rename from gns3server/modules/qemu/adapters/__init__.py rename to gns3server/utils/__init__.py diff --git a/gns3server/utils/asyncio.py b/gns3server/utils/asyncio.py new file mode 100644 index 00000000..a977b717 --- /dev/null +++ b/gns3server/utils/asyncio.py @@ -0,0 +1,79 @@ +# -*- 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 shutil + + +@asyncio.coroutine +def wait_run_in_executor(func, *args): + """ + Run blocking code in a different thread and wait + for 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() + + +@asyncio.coroutine +def subprocess_check_output(*args, cwd=None, env=None): + """ + Run a command and capture output + + :param *args: List of command arguments + :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=cwd, env=env) + output = yield from proc.stdout.read() + 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/gns3server/builtins/interfaces.py b/gns3server/utils/interfaces.py similarity index 73% rename from gns3server/builtins/interfaces.py rename to gns3server/utils/interfaces.py index 50efca0e..79e4ef28 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 """ - response = [] + 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. + + :returns: list of network interfaces + """ + + results = [] if not sys.platform.startswith("win"): try: import netifaces for interface in netifaces.interfaces(): - response.append({"id": interface, - "name": 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 + raise aiohttp.web.HTTPInternalServerError(text="Could not import netifaces module") 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/gns3server/version.py b/gns3server/version.py index 7c1b3d38..d3dc3679 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 @@ -23,5 +23,5 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "1.2.3" -__version_info__ = (1, 2, 3, 0) +__version__ = "1.3.0.dev2" +__version_info__ = (1, 3, 0, -99) diff --git a/gns3server/modules/qemu/nios/__init__.py b/gns3server/web/__init__.py similarity index 100% rename from gns3server/modules/qemu/nios/__init__.py rename to gns3server/web/__init__.py diff --git a/gns3server/web/documentation.py b/gns3server/web/documentation.py new file mode 100644 index 00000000..05ae9af3 --- /dev/null +++ b/gns3server/web/documentation.py @@ -0,0 +1,161 @@ +# -*- 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 +import os + +from gns3server.handlers import * +from gns3server.web.route import Route + + +class Documentation(object): + + """Extract API documentation as Sphinx compatible files""" + + 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): + + 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("{}/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"]: + 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") + + 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, api_version) + + def _create_handler_directory(self, handler_name, api_version): + """Create a directory for the handler and add an index inside""" + + directory = "{}/api/v{}/{}".format(self._directory, api_version, handler_name) + os.makedirs(directory, exist_ok=True) + + 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, api_version): + """If a sample session is available we include it in documentation""" + m = method["method"].lower() + 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:: ../../examples/{}\n\n".format(query_path)) + + def _file_path(self, path): + return re.sub("^v1", "", 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): + """ + 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) + f.write("
NameMandatoryTypeDescription
\n\n") + + +if __name__ == '__main__': + print("Generate API documentation") + Documentation(Route, "docs").write() diff --git a/gns3server/web/logger.py b/gns3server/web/logger.py new file mode 100644 index 00000000..6c9c1d2b --- /dev/null +++ b/gns3server/web/logger.py @@ -0,0 +1,92 @@ +#!/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.replace("#RESET#", "") + + 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 = message.replace("#RESET#", 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): + 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 + logging.basicConfig(level=level, handlers=[stream_handler]) + return logging.getLogger('user_facing') diff --git a/gns3server/web/request_handler.py b/gns3server/web/request_handler.py new file mode 100644 index 00000000..17e0ab90 --- /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 + 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 new file mode 100644 index 00000000..42112fa9 --- /dev/null +++ b/gns3server/web/response.py @@ -0,0 +1,100 @@ +# -*- 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 +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): + + def __init__(self, route=None, output_schema=None, headers={}, **kwargs): + + 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) + + def start(self, request): + 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') 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) + + 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 + the content. + + :param anwser The response as a Python object + """ + + 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 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 new file mode 100644 index 00000000..84b06da4 --- /dev/null +++ b/gns3server/web/route.py @@ -0,0 +1,206 @@ +# -*- 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 sys +import json +import jsonschema +import asyncio +import aiohttp +import logging +import traceback + +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 +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)) + else: + request.json = {} + try: + 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="Invalid JSON: {} in schema: {}".format( + e.message, + json.dumps(e.schema))) + return request + + +class Route(object): + + """ Decorator adding: + * json schema verification + * routing inside handlers + * documentation information about endpoints + """ + + _routes = [] + _documentation = {} + + _vm_locks = {} + + @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 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 + 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: + cls._path = "/v{version}{path}".format(path=path, version=api_version) + + def register(func): + route = cls._path + + handler = func.__module__.replace("_handler", "").replace("gns3server.handlers.api.", "") + cls._documentation.setdefault(handler, {}) + cls._documentation[handler].setdefault(route, {"api_version": api_version, + "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", ""), + }) + func = asyncio.coroutine(func) + + @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) + 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: + response = Response(route=route) + 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(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) + 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: + tb = "".join(lines) + response.json({"message": tb, "status": 500}) + else: + tb = "\n".join(lines) + response.html("

Internal error

{}
".format(tb)) + + return response + + @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 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._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 + + cls._routes.append((method, cls._path, vm_concurrency)) + + return vm_concurrency + return register + + @classmethod + def get_routes(cls): + return cls._routes + + @classmethod + def get_documentation(cls): + return cls._documentation diff --git a/requirements.txt b/requirements.txt index 3e267f9a..c12c2071 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,6 @@ -netifaces -tornado==3.2.2 -pyzmq -jsonschema -pycurl -python-dateutil -apache-libcloud -requests - +netifaces==0.10.4 +jsonschema==2.4.0 +python-dateutil==2.3 +aiohttp==0.14.4 +Jinja2==2.7.3 +raven==5.2.0 diff --git a/gns3server/modules/iou/adapters/ethernet_adapter.py b/scripts/documentation.sh old mode 100644 new mode 100755 similarity index 66% rename from gns3server/modules/iou/adapters/ethernet_adapter.py rename to scripts/documentation.sh index 312ef848..41111993 --- a/gns3server/modules/iou/adapters/ethernet_adapter.py +++ b/scripts/documentation.sh @@ -1,6 +1,6 @@ -# -*- coding: utf-8 -*- +#!/bin/sh # -# 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 @@ -15,17 +15,20 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from .adapter import Adapter +# +# Build the documentation +# +set -e -class EthernetAdapter(Adapter): - """ - IOU Ethernet adapter. - """ +echo "WARNING: This script should be run at the root directory of the project" - def __init__(self): - Adapter.__init__(self, interfaces=4) +export PYTEST_BUILD_DOCUMENTATION=1 - def __str__(self): +rm -Rf docs/api/ +mkdir -p docs/api/examples - return "IOU Ethernet adapter" +py.test -v +python3 gns3server/web/documentation.py +cd docs +make html diff --git a/scripts/pep8.sh b/scripts/pep8.sh new file mode 100755 index 00000000..33e0f54a --- /dev/null +++ b/scripts/pep8.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +echo ' + _______ ________ _______ ______ +| \ | \| \ / \ +| $$$$$$$\| $$$$$$$$| $$$$$$$\| $$$$$$\ +| $$__/ $$| $$__ | $$__/ $$| $$__/ $$ +| $$ $$| $$ \ | $$ $$ >$$ $$ +| $$$$$$$ | $$$$$ | $$$$$$$ | $$$$$$ +| $$ | $$_____ | $$ | $$__/ $$ +| $$ | $$ \| $$ \$$ $$ + \$$ \$$$$$$$$ \$$ \$$$$$$ + +' + + +find . -name '*.py' -exec autopep8 --in-place -v --aggressive --aggressive \{\} \; + +echo "It's all clean now!" diff --git a/scripts/ws_client.py b/scripts/ws_client.py deleted file mode 100644 index 675bc7c3..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() \ No newline at end of file diff --git a/setup.py b/setup.py index 5df7944d..23aed54e 100644 --- a/setup.py +++ b/setup.py @@ -28,11 +28,23 @@ 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) + +dependencies = ["aiohttp==0.14.4", + "jsonschema==2.4.0", + "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") + setup( name="gns3-server", version=__import__("gns3server").__version__, @@ -40,17 +52,9 @@ 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", - "jsonschema>=2.3.0", - "apache-libcloud>=0.14.1", - "requests", - ], + install_requires=dependencies, entry_points={ "console_scripts": [ "gns3server = gns3server.main:main", @@ -67,7 +71,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", diff --git a/gns3server/modules/virtualbox/adapters/__init__.py b/tests/__init__.py similarity index 100% rename from gns3server/modules/virtualbox/adapters/__init__.py rename to tests/__init__.py diff --git a/tests/conftest.py b/tests/conftest.py index a7ca89ef..74aee0cb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,19 +1,152 @@ -import sys -import os +# -*- 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 subprocess -import time +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 * +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.handlers.api.base import Query -@pytest.fixture(scope="session", autouse=True) -def server(request): + +@pytest.yield_fixture +def restore_original_path(): """ - Starts GNS3 server for all the tests. + 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""" + 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, monkeypatch): + """A GNS3 server""" + + 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: + 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() + request.addfinalizer(tear_down) + return Query(loop, host=host, port=port) + + +@pytest.fixture(scope="function") +def project(): + """A GNS3 lab""" + + return ProjectManager.instance().create_project(project_id="a1e920ca-338a-4e9f-b363-aa607b09dd80") + + +@pytest.fixture(scope="session") +def port_manager(): + """An instance of port manager""" + + return PortManager("127.0.0.1") + + +@pytest.fixture(scope="function") +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(project) + # We release the port immediately in order to allow + # the test do whatever the test want + port_manager.release_tcp_port(port, project) + return port + + +@pytest.yield_fixture(autouse=True) +def run_around_tests(monkeypatch): + """ + This setup a temporay project file environnement around tests + """ + + tmppath = tempfile.mkdtemp() + + config = Config.instance() + config.clear() + config.set("Server", "project_directory", tmppath) + + # Prevent exectuions of the VM if we forgot to mock something + 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) + + yield - 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 + # An helper should not raise Exception + try: + shutil.rmtree(tmppath) + except: + pass diff --git a/tests/dynamips/.gitignore b/tests/dynamips/.gitignore deleted file mode 100644 index 39ffa4b5..00000000 --- a/tests/dynamips/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/c3725.image diff --git a/tests/dynamips/conftest.py b/tests/dynamips/conftest.py deleted file mode 100644 index ff70cd58..00000000 --- a/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/tests/dynamips/dynamips.stable b/tests/dynamips/dynamips.stable deleted file mode 100755 index 0af011ac..00000000 Binary files a/tests/dynamips/dynamips.stable and /dev/null differ diff --git a/tests/dynamips/test_atm_bridge.py b/tests/dynamips/test_atm_bridge.py deleted file mode 100644 index aed46f70..00000000 --- a/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/tests/dynamips/test_atm_switch.py b/tests/dynamips/test_atm_switch.py deleted file mode 100644 index 6617b199..00000000 --- a/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/tests/dynamips/test_bridge.py b/tests/dynamips/test_bridge.py deleted file mode 100644 index ec415dbf..00000000 --- a/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/tests/dynamips/test_c1700.py b/tests/dynamips/test_c1700.py deleted file mode 100644 index 4ed3a2d2..00000000 --- a/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 != 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/tests/dynamips/test_c2600.py b/tests/dynamips/test_c2600.py deleted file mode 100644 index ae94f1b4..00000000 --- a/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 != 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/tests/dynamips/test_c2691.py b/tests/dynamips/test_c2691.py deleted file mode 100644 index 64acc6c1..00000000 --- a/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 != 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/tests/dynamips/test_c3600.py b/tests/dynamips/test_c3600.py deleted file mode 100644 index 435f1b27..00000000 --- a/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 != 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/tests/dynamips/test_c3725.py b/tests/dynamips/test_c3725.py deleted file mode 100644 index a4a923cf..00000000 --- a/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 != 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/tests/dynamips/test_c3745.py b/tests/dynamips/test_c3745.py deleted file mode 100644 index c58b5c2e..00000000 --- a/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 != 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/tests/dynamips/test_c7200.py b/tests/dynamips/test_c7200.py deleted file mode 100644 index 7b74cc7f..00000000 --- a/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 != 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] == 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/tests/dynamips/test_ethernet_switch.py b/tests/dynamips/test_ethernet_switch.py deleted file mode 100644 index 0f435f38..00000000 --- a/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/tests/dynamips/test_frame_relay_switch.py b/tests/dynamips/test_frame_relay_switch.py deleted file mode 100644 index b6dde5eb..00000000 --- a/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/tests/dynamips/test_hub.py b/tests/dynamips/test_hub.py deleted file mode 100644 index d490cb11..00000000 --- a/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/tests/dynamips/test_hypervisor.py b/tests/dynamips/test_hypervisor.py deleted file mode 100644 index 81a8176e..00000000 --- a/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/tests/dynamips/test_hypervisor_manager.py b/tests/dynamips/test_hypervisor_manager.py deleted file mode 100644 index adaa79a2..00000000 --- a/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/tests/dynamips/test_nios.py b/tests/dynamips/test_nios.py deleted file mode 100644 index 0538c298..00000000 --- a/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 == None # no constraint by default - nio.set_bandwidth(1000) # bandwidth = 1000 Kb/s - assert nio.bandwidth == 1000 - nio.delete() diff --git a/tests/dynamips/test_router.py b/tests/dynamips/test_router.py deleted file mode 100644 index ebf9835c..00000000 --- a/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 == True # default value - router.mmap = False - assert router.mmap == False - - -def test_sparsemem(router): - - assert router.sparsemem == True # 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 != 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/tests/dynamips/test_vmhandler.py b/tests/dynamips/test_vmhandler.py deleted file mode 100644 index cdc4998c..00000000 --- a/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/gns3server/modules/virtualbox/nios/__init__.py b/tests/handlers/api/__init__.py similarity index 100% rename from gns3server/modules/virtualbox/nios/__init__.py rename to tests/handlers/api/__init__.py diff --git a/tests/handlers/api/base.py b/tests/handlers/api/base.py new file mode 100644 index 00000000..fe13a45e --- /dev/null +++ b/tests/handlers/api/base.py @@ -0,0 +1,126 @@ +# -*- 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 aiohttp +import os + + +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 put(self, path, body={}, **kwargs): + return self._fetch("PUT", path, body, **kwargs) + + 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, 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, 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, api_version), 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() + 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: + 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, path, body, response) + return 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, 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)))) + 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") + 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) + return "docs/api/examples/{}_{}.txt".format(method.lower(), path) diff --git a/tests/handlers/api/test_config.py b/tests/handlers/api/test_config.py new file mode 100644 index 00000000..542a1b72 --- /dev/null +++ b/tests/handlers/api/test_config.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 . + +from unittest.mock import MagicMock, patch +from gns3server.config import Config + + +def test_reload_accepted(server): + + gns_config = MagicMock() + config = Config.instance() + config.set("Server", "local", "true") + 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) + + assert response.status == 201 + assert gns_config.reload.called + + +def test_reload_forbidden(server): + + config = Config.instance() + config.set("Server", "local", "false") + + response = server.post('/config/reload') + + assert response.status == 403 diff --git a/tests/handlers/api/test_dynamips.py b/tests/handlers/api/test_dynamips.py new file mode 100644 index 00000000..3f18c6ad --- /dev/null +++ b/tests/handlers/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/handlers/api/test_iou.py b/tests/handlers/api/test_iou.py new file mode 100644 index 00000000..bc0f8da9 --- /dev/null +++ b/tests/handlers/api/test_iou.py @@ -0,0 +1,286 @@ +# -*- 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, MagicMock, PropertyMock + + +@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", "path": fake_iou_bin} + + +@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 initial_config_file(project, vm): + 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): + 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 + assert response.json["l1_keepalives"] is False + + +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 + 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 + 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 + 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"] + + 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) + 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 + assert response.json["serial_adapters"] == 2 + assert response.json["ethernet_adapters"] == 2 + assert response.json["ram"] == 256 + assert response.json["nvram"] == 128 + assert response.json["l1_keepalives"] is False + + +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"]), 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"]), 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"]), 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"]), example=True) + assert mock.called + assert response.status == 204 + + +def test_iou_update(server, vm, tmpdir, free_console_port, project): + params = { + "name": "test", + "console": free_console_port, + "ram": 512, + "nvram": 2048, + "ethernet_adapters": 4, + "serial_adapters": 0, + "l1_keepalives": True, + "initial_config_content": "hostname test", + "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 + 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 + 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" + + 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", + "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" + assert response.json["type"] == "nio_udp" + + +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", + }, + 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" + assert response.json["type"] == "nio_generic_ethernet" + 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", + "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" + + +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"}) + 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, 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.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) + + assert response.status == 200 + + assert start_capture.called + assert "test.pcap" in response.json["pcap_file_path"] + + +def test_iou_start_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.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) + + 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): + + 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 + + +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" diff --git a/gns3server/handlers/version_handler.py b/tests/handlers/api/test_network.py similarity index 61% rename from gns3server/handlers/version_handler.py rename to tests/handlers/api/test_network.py index 30c55d40..d675d4f6 100644 --- a/gns3server/handlers/version_handler.py +++ b/tests/handlers/api/test_network.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,14 @@ # 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 ..version import __version__ +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} -class VersionHandler(GNS3BaseHandler): - def get(self): - response = {'version': __version__} - self.write(response) +def test_interfaces(server): + response = server.get('/interfaces', example=True) + assert response.status == 200 + assert isinstance(response.json, list) diff --git a/tests/handlers/api/test_project.py b/tests/handlers/api/test_project.py new file mode 100644 index 00000000..cd0bc419 --- /dev/null +++ b/tests/handlers/api/test_project.py @@ -0,0 +1,144 @@ +# -*- 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 +""" + +import uuid +from unittest.mock import patch +from tests.utils import asyncio_patch + + +def test_create_project_with_path(server, 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 = {"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 = {"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 = {"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 = {"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()) == 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): + response = server.get("/projects/00010203-0405-0607-0809-0a0b0c0d0e42") + assert response.status == 404 + + +def test_update_temporary_project(server): + query = {"name": "test", "temporary": True} + response = server.post("/projects", query) + assert response.status == 201 + 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 + + +def test_update_path_project(server, tmpdir): + + with patch("gns3server.modules.project.Project.is_local", return_value=True): + response = server.post("/projects", {"name": "first_name"}) + assert response.status == 201 + 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.modules.project.Project.is_local", return_value=False): + response = server.post("/projects", {"name": "first_name"}) + assert response.status == 201 + 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 + + +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.id), example=True) + assert response.status == 204 + assert mock.called + + +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("/projects/{project_id}".format(project_id=project.id), example=True) + assert response.status == 204 + assert mock.called + + +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("/projects/{project_id}/close".format(project_id=project.id), example=True) + assert response.status == 204 + assert mock.called + + +def test_close_project_invalid_uuid(server): + response = server.post("/projects/{project_id}/close".format(project_id=uuid.uuid4())) + assert response.status == 404 diff --git a/tests/handlers/api/test_qemu.py b/tests/handlers/api/test_qemu.py new file mode 100644 index 00000000..b06189f5 --- /dev/null +++ b/tests/handlers/api/test_qemu.py @@ -0,0 +1,177 @@ +# -*- 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 + params["ram"] = 1024 + 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 + 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"] == "/tmp/hda" + + +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"]), 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"]), 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"]), 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"]), 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"]), 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"]), example=True) + 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, + "ram": 1024, + "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"] == "/tmp/hdb" + assert response.json["ram"] == 1024 + + +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, + "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): + 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", + }, + 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_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, + "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" + + +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"]), example=True) + assert mock.called + assert response.status == 200 + assert response.json == ret diff --git a/tests/handlers/api/test_version.py b/tests/handlers/api/test_version.py new file mode 100644 index 00000000..29cdebae --- /dev/null +++ b/tests/handlers/api/test_version.py @@ -0,0 +1,61 @@ +# -*- 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.config import Config + +from gns3server.version import __version__ + + +def test_version_output(server): + config = Config.instance() + config.set("Server", "local", "true") + + response = server.get('/version', example=True) + assert response.status == 200 + assert response.json == {'local': True, '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': 'Client version 0.4.2 differs with server version {}'.format(__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 + + +def test_version_invalid_json(server): + query = "BOUM" + response = server.post('/version', query, raw=True) + assert response.status == 400 diff --git a/tests/handlers/api/test_virtualbox.py b/tests/handlers/api/test_virtualbox.py new file mode 100644 index 00000000..ff69aa4f --- /dev/null +++ b/tests/handlers/api/test_virtualbox.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 pytest +from tests.utils import asyncio_patch + + +@pytest.yield_fixture(scope="function") +def vm(server, project, monkeypatch): + + vboxmanage_path = "/fake/VboxManage" + + 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}) + assert mock.called + assert response.status == 201 + + 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}, + example=True) + assert response.status == 201 + assert response.json["name"] == "VM1" + assert response.json["project_id"] == project.id + + +def test_vbox_get(server, project, vm): + 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 == "/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("/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"]), 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"]), 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"]), 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"]), example=True) + 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/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 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_number:\d+}/ports/{port_number:\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/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_number:\d+}/ports/{port_number:\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}, + 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 new file mode 100644 index 00000000..473a533e --- /dev/null +++ b/tests/handlers/api/test_vpcs.py @@ -0,0 +1,133 @@ +# -*- 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 +from tests.utils import asyncio_patch +from unittest.mock import patch + + +@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 + return response.json + + +def test_vpcs_create(server, project): + 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 == "/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("/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 == "/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): + 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 == "/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" + assert response.json["startup_script_path"] == "startup.vpc" + + +def test_vpcs_create_port(server, project, 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 == "/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("/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}/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}/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}/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}/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}/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" + + +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"]), 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"]), 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"]), 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"]), example=True) + assert mock.called + assert response.status == 204 + + +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"}, + example=True) + assert response.status == 200 + assert response.json["name"] == "test" + assert response.json["console"] == free_console_port + assert response.json["startup_script"] == "ip 192.168.1.1" diff --git a/tests/handlers/test_upload.py b/tests/handlers/test_upload.py new file mode 100644 index 00000000..4e3cebc5 --- /dev/null +++ b/tests/handlers/test_upload.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 . + + +import aiohttp +import os +from unittest.mock import patch + +from gns3server.version import __version__ + + +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("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 / "QEMU" / "test2")) as f: + assert f.read() == "TEST" + + assert "test2" in response.body.decode("utf-8") diff --git a/tests/iou/test_iou_device.py b/tests/iou/test_iou_device.py deleted file mode 100644 index 58581de9..00000000 --- a/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/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..f32b84ba --- /dev/null +++ b/tests/modules/dynamips/test_dynamips_router.py @@ -0,0 +1,57 @@ +# -*- 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 configparser + +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") +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): + + config = Config.instance() + 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) + 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/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..42ddba80 --- /dev/null +++ b/tests/modules/iou/test_iou_vm.py @@ -0,0 +1,379 @@ +# -*- 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 +import socket +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 +from gns3server.config import Config + + +@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, iourc_file): + fake_file = str(tmpdir / "iouyap") + 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 + 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""" + + 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) + 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" + + +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): + vm = IOUVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0e", project, manager) + loop.run_until_complete(asyncio.async(vm.start())) + + +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._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): + + 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._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): + """ + 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") + + 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): + 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("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): + 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("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, 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 + loop.run_until_complete(asyncio.async(vm.close())) + # Raise an exception if the port is not free + port_manager.reserve_tcp_port(port, vm.project) + assert vm.is_running() is False + + +def test_path(vm, fake_iou_bin): + + vm.path = fake_iou_bin + assert vm.path == fake_iou_bin + + +def test_path_relative(vm, fake_iou_bin, tmpdir): + + config = Config.instance() + config.set("Server", "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") + with pytest.raises(IOUError): + vm.path = path + + with open(path, "w+") as f: + f.write("BUG") + + with pytest.raises(IOUError): + vm.path = 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, loop): + + assert loop.run_until_complete(asyncio.async(vm._build_command())) == [vm.path, "-L", str(vm.application_id)] + + +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 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): + + 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" + + +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"] + + +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) + 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, 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) + 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 is False + + +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())) + + +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/qemu/test_qemu_manager.py b/tests/modules/qemu/test_qemu_manager.py new file mode 100644 index 00000000..ee732d11 --- /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="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="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(): + + assert Qemu.get_legacy_vm_workdir(42, "bla") == "qemu/vm-42" diff --git a/tests/modules/qemu/test_qemu_vm.py b/tests/modules/qemu/test_qemu_vm.py new file mode 100644 index 00000000..1e078c26 --- /dev/null +++ b/tests/modules/qemu/test_qemu_vm.py @@ -0,0 +1,320 @@ +# -*- 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 +import re +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): + manager.port_manager.console_host = "127.0.0.1" + 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_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, running_subprocess_mock): + process = running_subprocess_mock + + # 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.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())) + 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_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.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, 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, nio))) + loop.run_until_complete(asyncio.async(vm.adapter_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_tcp_port(console_port, vm.project) + # Raise an exception if the port is not free + port_manager.reserve_tcp_port(monitor_port, vm.project) + + 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"), "256M") + + +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") + + +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, running_subprocess_mock): + + vm._process = running_subprocess_mock + 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(b"epic product") + reader.readline.return_value = future + + res = loop.run_until_complete(asyncio.async(vm._control_vm("test", [b"epic"]))) + assert writer.write.called_with("test") + + assert res == "epic product" + + +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"): + 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:127.0.0.1:{},server,nowait".format(vm.console), + "-monitor", + "tcp: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" + ] + + +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 + + +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 / "QEMU" / "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 / "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_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) diff --git a/tests/modules/test_manager.py b/tests/modules/test_manager.py new file mode 100644 index 00000000..a9d32daf --- /dev/null +++ b/tests/modules/test_manager.py @@ -0,0 +1,84 @@ +# -*- 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_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 + 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.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") + 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") + + 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 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" diff --git a/gns3server/modules/virtualbox/adapters/ethernet_adapter.py b/tests/modules/test_port_manager.py similarity index 63% rename from gns3server/modules/virtualbox/adapters/ethernet_adapter.py rename to tests/modules/test_port_manager.py index 8951ee8d..2590b826 100644 --- a/gns3server/modules/virtualbox/adapters/ethernet_adapter.py +++ b/tests/modules/test_port_manager.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 @@ -15,17 +15,14 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from .adapter import Adapter +import aiohttp +import pytest +from gns3server.modules.port_manager import PortManager +from gns3server.modules.project import Project - -class EthernetAdapter(Adapter): - """ - VirtualBox Ethernet adapter. - """ - - def __init__(self): - Adapter.__init__(self, interfaces=1) - - def __str__(self): - - return "VirtualBox Ethernet adapter" +def test_reserve_tcp_port(): + pm = PortManager() + project = Project() + pm.reserve_tcp_port(4242, project) + with pytest.raises(aiohttp.web.HTTPConflict): + pm.reserve_tcp_port(4242, project) diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py new file mode 100644 index 00000000..60ec1765 --- /dev/null +++ b/tests/modules/test_project.py @@ -0,0 +1,232 @@ +#!/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 asyncio +import pytest +import aiohttp +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 + + +@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, 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(): + p = Project() + assert len(p.id) == 36 + + p = Project(project_id='00010203-0405-0607-0809-0a0b0c0d0e0f') + assert p.id == '00010203-0405-0607-0809-0a0b0c0d0e0f' + + +def test_path(tmpdir): + 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)) + assert not os.path.exists(os.path.join(p.path, ".gns3_temporary")) + + +def test_init_path(tmpdir): + + 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.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")) + 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")) + + +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): + 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.modules.project.Project.is_local", return_value=False): + with pytest.raises(aiohttp.web.HTTPForbidden): + p = Project() + p.path = str(tmpdir) + + +def test_json(tmpdir): + p = Project() + 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.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)) + + +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(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) + 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_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 + assert os.path.exists(directory) + loop.run_until_complete(asyncio.async(project.delete())) + 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) + project.add_vm(vm) + assert len(project.vms) == 1 + + +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 + assert vm.id not in vm.manager._vms + + +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) + loop.run_until_complete(asyncio.async(project.close())) + assert os.path.exists(directory) is False + + +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 + 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)) diff --git a/tests/modules/test_project_manager.py b/tests/modules/test_project_manager.py new file mode 100644 index 00000000..0f2d6ed3 --- /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(project_id='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/virtualbox/test_virtualbox_manager.py b/tests/modules/virtualbox/test_virtualbox_manager.py new file mode 100644 index 00000000..1dc647d6 --- /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 + + +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(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() diff --git a/tests/modules/virtualbox/test_virtualbox_vm.py b/tests/modules/virtualbox/test_virtualbox_vm.py new file mode 100644 index 00000000..8b91431e --- /dev/null +++ b/tests/modules/virtualbox/test_virtualbox_vm.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 +from tests.utils import asyncio_patch + +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.id == "00010203-0405-0607-0809-0a0b0c0d0e0f" + assert vm.vmname == "test" + + +def test_vm_valid_virtualbox_api_version(loop, project, manager): + 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.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 new file mode 100644 index 00000000..c7194de3 --- /dev/null +++ b/tests/modules/vpcs/test_vpcs_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.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_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): + # Cleanup the VPCS object + VPCS._instance = None + vpcs = VPCS.instance() + vpcs.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(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): + # 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_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 new file mode 100644 index 00000000..6b539a89 --- /dev/null +++ b/tests/modules/vpcs/test_vpcs_vm.py @@ -0,0 +1,223 @@ +# -*- 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_vm(project, manager): + vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + assert vm.name == "test" + assert vm.id == "00010203-0405-0607-0809-0a0b0c0d0e0f" + + +def test_vm_invalid_vpcs_version(loop, project, manager): + 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"}) + vm.port_add_nio_binding(0, nio) + loop.run_until_complete(asyncio.async(vm.start())) + assert vm.name == "test" + assert vm.id == "00010203-0405-0607-0809-0a0b0c0d0e0f" + + +@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) + 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.id == "00010203-0405-0607-0809-0a0b0c0d0e0e" + + +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 = 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() + + +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"}) + 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): + 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"}) + 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())) + assert vm.is_running() is True + process.terminate.assert_called_with() + + +def test_add_nio_binding_udp(vm): + 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.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.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 = 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 + + +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.vpc') + 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.vpc') + assert os.path.exists(filepath) + with open(filepath) as f: + 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 + 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_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, vm.project) + + +def test_change_name(vm, tmpdir): + path = os.path.join(vm.working_dir, 'startup.vpc') + vm.name = "world" + with open(path, 'w+') as f: + f.write("name world") + vm.name = "hello" + assert vm.name == "hello" + with open(path) as f: + assert f.read() == "name hello" + + +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 + loop.run_until_complete(asyncio.async(vm.close())) + # Raise an exception if the port is not free + port_manager.reserve_tcp_port(port, vm.project) + assert vm.is_running() is False diff --git a/gns3server/modules/vpcs/adapters/__init__.py b/tests/test_asyncio.py similarity index 100% rename from gns3server/modules/vpcs/adapters/__init__.py rename to tests/test_asyncio.py diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 00000000..a1a0c33e --- /dev/null +++ b/tests/test_config.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 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", + "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("Server", "host", "192.168.1.1") + assert dict(config.get_section_config("Server")) == {"host": "192.168.1.1"} + + +def test_reload(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" + } + }) + + config.reload() + assert dict(config.get_section_config("Server")) == {"host": "192.168.1.1"} + + +def test_reload(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" + } + }) + + config.reload() + assert dict(config.get_section_config("Server")) == {"host": "192.168.1.1"} diff --git a/tests/test_jsonrpc.py b/tests/test_jsonrpc.py deleted file mode 100644 index 155502c5..00000000 --- a/tests/test_jsonrpc.py +++ /dev/null @@ -1,91 +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/test_main.py b/tests/test_main.py new file mode 100644 index 00000000..8762bcd8 --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,143 @@ +#!/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(): + + 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') + + +def test_parse_arguments(capsys, tmpdir): + + Config.reset() + config = Config.instance(str(tmpdir / "test.cfg")) + 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 "{}{}".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 "{}{}".format(out, err) # Depending of the Python version the location of the version change + + 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 == "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" + + 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") diff --git a/tests/test_version_handler.py b/tests/test_version_handler.py deleted file mode 100644 index 0c8c75d9..00000000 --- a/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__ diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 00000000..b8994f66 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,59 @@ +# -*- 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 + + +class _asyncio_patch: + + """ + A wrapper around python patch supporting asyncio. + Like the original patch you can use it as context + manager (with) + + 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()) + mock_class = self._patcher.start() + return mock_class + + def __exit__(self, *exc_info): + """Used when leaving the with block""" + self._patcher.stop() + + def _fake_anwser(self): + future = asyncio.Future() + if "return_value" in self.kwargs: + future.set_result(self.kwargs["return_value"]) + else: + future.set_result(True) + return future + + +def asyncio_patch(function, *args, **kwargs): + return _asyncio_patch(function, *args, **kwargs) diff --git a/tests/utils/test_asyncio.py b/tests/utils/test_asyncio.py new file mode 100644 index 00000000..96fbde7b --- /dev/null +++ b/tests/utils/test_asyncio.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 asyncio +import pytest +from unittest.mock import MagicMock + +from gns3server.utils.asyncio import wait_run_in_executor, subprocess_check_output, wait_for_process_termination + + +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)) + + +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" + + +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)) diff --git a/tests/vpcs/test_vpcs_device.py b/tests/vpcs/test_vpcs_device.py deleted file mode 100644 index 781166b4..00000000 --- a/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/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 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() diff --git a/tox.ini b/tox.ini index 200e7ce4..155cefab 100644 --- a/tox.ini +++ b/tox.ini @@ -5,3 +5,9 @@ envlist = py33, py34 commands = python setup.py test deps = -rdev-requirements.txt +[pep8] +ignore = E501,E402 + +[pytest] +norecursedirs = old_tests .tox +timeout = 2