diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..b824f504 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[paths] +source = gns3server diff --git a/.gitignore b/.gitignore index b4723b84..b8d1878b 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ pip-log.txt # Unit test / coverage reports .coverage .tox +.cache nosetests.xml # Translations diff --git a/.travis.yml b/.travis.yml index a883f74b..770143e4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,32 +1,14 @@ language: python 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 - +- '3.4' +- '3.5' +sudo: false +cache: pip install: - - pip install -rdev-requirements.txt - - python setup.py install - +- python setup.py install +- pip install -rdev-requirements.txt +- pip install coveralls script: - - py.test -v -s tests --cov gns3server --cov-report term-missing - -#branches: -# only: -# - master - -notifications: - email: - - julien@gns3.net -# irc: -# channels: -# - "chat.freenode.net#gns3" -# on_success: change -# on_failure: always - +- py.test -v -s tests --cov gns3server --cov-report term-missing after_success: - - coveralls --data_file .coverage --base_dir gns3server +- coveralls diff --git a/AUTHORS b/AUTHORS index 608cba37..783e04bb 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1 +1,2 @@ -Jeremy Grossmann \ No newline at end of file +Jeremy Grossmann +Julien Duponchelle \ No newline at end of file diff --git a/CHANGELOG b/CHANGELOG index 07a7e489..1693fb3a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,26 @@ # Change Log +## 1.4.0 12/01/2016 +* Release 1.4.0 + +## 1.4.0rc3 05/01/2016 + +* API documentation update +* Fix race condition when killing iouyap +* Catch exception if we can't change process priority on Windows +* Adds a handler for getting the Qemu related capabilities of the server. Currently includes just a check for KVM architectures. +* Fixed showing of Qemu hdb_disk_interface - it showed hda_disk_interface instead, which resulted in an odd visual glitch in the GUI. +* Made the gns3server.bat successfully start the server independent of the CWD at the time of running. It's now relative to the location of the .bat file itself. +* Add more informations in the debug status page +* Fix status link in GNS3 homepage +* Fix tests on Windows +* Fix missing boot priority order +* A debug status page embed in the server +* Fix test on Windows +* Update links for new website. +* Contributing instructions + + ## 1.3.13 11/12/2015 * Update links for new website. @@ -15,6 +36,76 @@ * Fix asyncio error when closing the app * Release UDP ports when closing a Qemu VM. Fixes #323. +## 1.4.0rc2 10/12/2015 + +* Add log about wher iou capture packet +* Replace by another TCP port if port is already used +* Fix ProcessLookupError in Qemu +* Increase vmrun timeout to 120 seconds. Ref #360. +* Fixes termination notification to indicate the right process name (IOU vs iouyap). Ref #359. +* Fixes error with non initialized uBridge. Fixes #367. +* Remove debug that can crash qemu +* Support VM usage for qemu +* Raise an error if psutil version is invalid + +## 1.4.0rc1 12/11/2015 + +* Raise error if server received windows path +* Update sentry key +* Remove NIO FIFO and Mcast (unused). Fixes #348. +* Support VPCS 0.6.1 +* Fix duplicate of -no-kvm options +* Raise an error if user send a non local path to remote server +* Fix minor issues +* Apply pep8 fix +* Sets console end port to 7000. Fixes #343. +* Drop netifaces (replaced by psutil). Fixes #344. +* Correctly display log messages. +* Tentative fix for "WinError 64 The specified network name is no longer available" issues. +* Return relative path for dynamips images +* Fix add existing IOS not working +* Correctly enable faulthandler for dev version +* Avoid test crash if GNS3 is running on the same computer +* Allow to return an empty project name because it's allowed in creation +* Test with python 3.5 +* Add doc on how to got code coverage + +## 1.4.0b5 02/11/2015 + +* Freeze requirements for aiohttp because 0.18 doesn't support Python 3.4. +* Fix crash in IOU config export. +* Raise an error when you use a port outside the ranges. Fixes #739. +* Fixes Windows named pipe issue. Fixes #340. + +## 1.4.0b4 19/10/2015 + +* Support for modifications to a base Qemu VM (not a linked clone). +* Force canceling all task when shutdown server +* Update api documentation +* Enforce console port for VNC +* Fixes issue when loading a project using VMware vmnet interfaces. Fixes #319. +* Support for NAT connection with cloud for VMware VMs. Fixes #322. +* Change message when VMware is not installed on Linux. Ref #326. +* Send a warning notification if there is not enough RAM left to start a VM. Implements #329. +* Asyncio Qemu fix and raise error if coroutine not used +* Fix asyncio error when closing the app +* Removes VMware lock check. Fixes #328. +* Wait for pipe file to be created before starting the remote console for VMware and VirtualBox VMs. Fixes #331. +* Release UDP ports when closing a Qemu VM. Fixes #323. +* Escape other usage of glob +* Fix Dynamips identifier is already used by another router +* Protect Dynamips against bad glob. +* Fix issue with Qemu networking following merge. +* OVA file support +* Support listing images in subdirectories. +* Catch ProcessLookupError in Qemu VM. +* Fixes uncalled coroutine. +* Use the correct UDP tunnel Qemu syntax for version > 1.1.0 when legacy networking is enabled. +* VMware player linux support. +* Prevent launching a packet capture with a non-ASCII path when using Dynamips. +* Do not require a TAP interface to already exist. Fixes #321. +* Do not automatically delete Dynamips bootflash file because they are necessary to restore VLANs on the c3600 platform. + ## 1.3.11 07/10/2015 * Escape other usage of glob @@ -28,6 +119,60 @@ * Clarify error message when we got UTF-8 chars in the iourc file * Check for valid FR or ATM switch mappings. Fixes #300. +## 1.4.0b3 22/09/2015 + +* Fix dynamips configuration lost when you delete a node +* Clarify error message when we got UTF-8 chars in the iourc file +* Use custom VMnet interfaces without host adapter when uBridge is not used. Fixes #673. +* Automatically add the -no-kvm option if -icount is detected to help with the migration of ASA VMs created before version 1.4 +* Check for valid FR or ATM switch mappings. Fixes #300. +* Catch exception when a process cannot be killed. Fixes #296. + + +## 1.4.0beta2 17/09/2015 + +* Fix a crash at vmware stop +* Fix a crash when starting a VMware vm +* Add how to add vmnet interfaces explantion in the error message +* Fix path of VMinventory for fusion +* Force close the keep alive when sending a 401 +* Do not automatically delete Dynamips bootflash file because they are necessary to restore VLANs on the c3600 platform. +* Wait that an user press a key to stop gns3vmnet.exe on Windows. +* Throw an error if ubridge as incorrect permissions. Fixes #312. +* This may fix "The semaphore timeout period has expired" error on Windows. #311. +* Fixes bug with VMware VM connections + moves some uBridge code to BaseVM. +* Support for packet capture on VMware VM links. +* Fix ProcessLookupError on _checkAlive Qemu +* VMware Fusion support with uBridge. +* Updates vmnet script to support Windows. +* Do not block on .lock for VMware OSX +* Require Dynamips version 0.2.16 to change the default QinQ Ethernet type. +* Initial Docker support from Google Summer of Code (not enabled) +* Check for valid FR or ATM switch mappings. Fixes #300. +* VirtualBox VMs can only be started if powered off. Fixes #299. +* Support of VPCS 0.8 +* Allows VMware VMs to use vmnet interfaces for connections without using uBridge. Fixes #295. +* Fixes path to vmnet-cli on Mac OS X. +* Updates vmnet script to support Mac OS X. +* Fix closing project when multiple project is open +* Fix project not closing +* Qemu user options are at the end. It's allow user to add his own net interfaces +* Change the way we look for Qemu path +* Lock qemu vm during start / stop operations +* In the error message explain how to turn off KVM support +* Fix when you stop qemu on windows you have an error +* Fix Qemu cannot be used on Windows +* Allow to start server with python -m gns3server +* Should solve the BufferError by avoiding using thread +* Catch UnicodeEncodeError when passing unicode char as qemu options +* EthernetSwitch: Allow to choose ethertype for QinQ outer tag. +* Backport: fixes NAT NIO for Qemu VMs (do not launch any legacy scripts) +* Fixes NAT NIO for Qemu VMs (do not launch any legacy scripts) +* Lower VMware requirements to Workstation version 10 and Player version 6. +* Fixes Unicode error. Fixes #290. +* Don't delete Dynamips ROM files. They are used to restore the nvram. +* Adds pywin32 dependency in setup.py for Windows. + ## 1.3.10 04/09/2015 * Catch exception when a process cannot be killed. Fixes #296. @@ -35,10 +180,46 @@ * Fixes Unicode error. Fixes #290. * Don't delete Dynamips ROM files. They are used to restore the nvram. +## 1.4.0beta1 07/08/2015 + +* Fix ram setting for Qemu +* Explicit set qemu memory as MB +* Turn off KVM for non x86 architectures +* Send an error when vmware executable cannot be found on Linux. Fixes #288. +* Support for CPUs setting for Qemu VMs. + +## 1.4.0alpha4 04/08/2015 + +* Quote command in qemu debug logs so you can copy/paste them +* Support for Qemu disk interfaces, cd/dvd-rom image and boot priority. Fixes #278. +* Check for VMware Player version >= 7 and VMware Workstation >= 11. Fixes #286. +* Catch GeneratorExit exception when trying to create a Ghost IOS image. +* Backport: removes code that deletes IOS router instance files. + ## 1.3.9 03/08/2015 * Backport: removes code that deletes IOS router instance files. +## 1.4.0alpha3 28/07/2015 + +* Raise error if qemu image already exist when creating disk +* Prevent user to create a qemu to a different directory on non local server +* VMnet manager on Linux: check that VMware has been installed. +* Fixes UnicodeDecodeError when reading a VMware file. +* Fixes KeyError: "ethernet0.connectiontype". Fixes #276. +* Fixes replace errors. Fixes #284. +* Catch ProcessLookupError when updating iouyap config. Fixes #255. +* API for creating a qemu disk image +* Prevent starting different hypervisors that leverage hardware virtualization (VT-x/AMD-V). Fixes #548. +* Fixes IOS adapters and WICS cannot be removed. Fixes #282. +* Makes sure the loop is running when closing the app. +* Catch Permission denied when writing to VMX file while closing VMware VM. Fixes #277. +* Catch GeneratorExit exception. Fixes #231. +* Fixes missing chipset info for VirtualBox VM (maybe some older VirtualBox version don't have it). Fixes #254. +* Changes how to look for the vmrun.exe location. +* Update documentation +* API for listing current projects + ## 1.3.8 27/07/2015 * Catch ProcessLookupError when updating iouyap config. Fixes #255. @@ -56,6 +237,61 @@ * Backport from 1.4: Fixes RuntimeError: Event loop is closed. * Backport from 1.4: Bind host on 0.0.0.0 when checking for a free UDP port. +## 1.4.0alpha2 22/07/2015 + +* Deactivate uBridge process monitoring (process returns 1 on Windows when stopping). +* Prevent using different hypervisors that leverage hardware virtualization. - Implemented for Qemu when a VMware or VirtualBox VM with hardware virtualization is already running. - Implemented for VirtualBox only when a Qemu VM with KVM is already running. +* Check for uBridge version and catch uBridge errors. +* Remove default FLASH when no hda disk for Qemu VMs. Fixes #535. +* Use the registry to find vmrun if the default VMware install path does not exist. +* Bind host on 0.0.0.0 when checking for a free UDP port. +* Fixes RuntimeError: Event loop is closed. Fixes #266. +* Update gns3.conf.upstart +* Implements uBridge hypervisor. +* Take VMware file encoding into account. Fixes #261. + +## 1.4.0alpha1 09/07/2015 + +* Update API documentation +* Allow to send the iourc when starting the VM +* Return stdout when a process crash for IOU, Dynamips, uBridge and VPCS. +* Adds -no-kvm to the ASA template and ignore -no-kvm on platforms other than Linux. Should resolve #472. +* Allow user to change the configuration file +* Fix double loading of config from working directory +* CORS support +* Support server config in current working directory +* List only valid existing IOS images (for IOS router wizard). +* Checks if IOS image exist at startup and not during node creation. Fixes #240. +* When a qemu VM crash send the log to the client. +* Add a vm_directory field +* Check for /dev/kvm. Fixes #245. +* Moves KVM setting to Qemu server preferences. Fixes #244. +* VNC console support for Qemu VMs. +* Test all IOU requirements at VM startup +* ACPI shutdown support for VMware VMs. Fixes #436. +* Compute a md5sum of images for futur purpose +* Adds gns3-netifaces to dependencies only if netifaces isn't already installed otherwise this requires a compilation and therefore the Python development files. +* Adds an IP address for each interface returned by the interfaces API method. +* Add log when we didn't close a project due to another client +* Limit file size during upload +* Convert old -enable-kvm to kvm settings for Qemu +* Cleanup SSL certificate support +* Improve memory consumption of file upload with the HTML form +* systemd start script +* Enable KVM acceleration option. +* Check interface is up before connecting a NIO (Linux only). Fixes #277. +* IPv6 support. +* Import/Export support for IOU nvrams. +* Install qt5 for travis +* Option to drop nvram & disk files for IOS routers in order to save disk space. +* Drop python 3.3 +* Support for base MAC address for Qemu VMs. +* ACPI shutdown support for Qemu VMs. +* ACPI shutdown support for VirtualBox VMs. +* Upload images API +* A notification stream with process monitoring +* VMware support + ## 1.3.7 22/06/2015 * Prevent install on Python 2 diff --git a/README.rst b/README.rst index f2408f9d..f97251ad 100644 --- a/README.rst +++ b/README.rst @@ -20,16 +20,19 @@ Branches master ****** master is the next stable release, you can test it in your day to day activities. -Bug fixes or small improvements pull requests goes here. +Bug fixes or small improvements pull requests go here. -unstable +1.x (1.4 for example) ******** -*Never* use this branch for production. Major new features pull requests goes here. +Next major release + +*Never* use this branch for production. Pull requests for major new features go here. Linux ----- GNS3 is perhaps packaged for your distribution: + * Gentoo: https://packages.gentoo.org/package/net-misc/gns3-server @@ -41,10 +44,10 @@ You must be connected to the Internet in order to install the dependencies. Dependencies: -- Python 3.3 or above +- Python 3.4 or above - aiohttp - setuptools -- netifaces +- psutil - jsonschema The following commands will install some of these dependencies: @@ -52,7 +55,6 @@ The following commands will install some of these dependencies: .. code:: bash sudo apt-get install python3-setuptools - sudo apt-get install python3-netifaces Finally these commands will install the server as well as the rest of the dependencies: @@ -69,12 +71,24 @@ To run tests use: py.test -v -Run as daemon -*************** +Run as daemon (Unix only) +************************** You will found init sample script for various systems inside the init directory. +Usefull options: + +* --daemon: start process as a daemon +* --log logfile: store output in a logfile +* --pid pidfile: store the pid of the running process in a file and prevent double execution + +All the init script require the creation of a GNS3 user. You can change it to another user. + +.. code:: bash + + sudo adduser gns3 + upstart ~~~~~~~ @@ -88,13 +102,51 @@ You need to copy init/gns3.conf.upstart to /etc/init/gns3.conf sudo service gns3 start +systemd +~~~~~~~~ +You need to copy init/gns3.service.systemd to /lib/systemd/system/gns3.service + +.. code:: bash + + sudo chown root /lib/systemd/system/gns3.service + sudo systemctl start gns3 + Windows ------- -Please use our all-in-one installer. -If you install it via source you need to install also: -https://sourceforge.net/projects/pywin32/ +Please use our `all-in-one installer `_ to install the stable build. + +If you install via source you need to first install: + +- Python (3.3 or above) - https://www.python.org/downloads/windows/ +- Pywin32 - https://sourceforge.net/projects/pywin32/ + +Then you can call + +.. code:: bash + + python setup.py install + +to install the remaining dependencies. + +To run the tests, you also need to call + +.. code:: bash + + pip install pytest pytest-capturelog + +before actually running the tests with + +.. code:: bash + + python setup.py test + +or with + +.. code:: bash + + py.test -v Mac OS X -------- @@ -113,6 +165,33 @@ and homebrew: http://brew.sh/. python3 setup.py install gns3server +SSL +--- + +If you want enable SSL support on GNS3 you can generate a self signed certificate: + +.. code:: bash + + bash gns3server/cert_utils/create_cert.sh + +This command will put the files in ~/.config/GNS3/ssl + +After you can start the server in SSL mode with: + +.. code:: bash + + python gns3server/main.py --certfile ~/.config/GNS3/ssl/server.cert --certkey ~/.config/GNS3/ssl/server.key --ssl + + +Or in your gns3_server.conf by adding in the Server section: + +.. code:: ini + + [Server] + certfile=/Users/noplay/.config/GNS3/ssl/server.cert + certkey=/Users/noplay/.config/GNS3/ssl/server.key + ssl=True + Running tests ************* diff --git a/docs/api/examples/delete_projectsprojectid.txt b/docs/api/examples/delete_projectsprojectid.txt index a7ecd00f..36665b8c 100644 --- a/docs/api/examples/delete_projectsprojectid.txt +++ b/docs/api/examples/delete_projectsprojectid.txt @@ -5,9 +5,10 @@ DELETE /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80 HTTP/1.1 HTTP/1.1 204 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 X-ROUTE: /v1/projects/{project_id} diff --git a/docs/api/examples/delete_projectsprojectidiouvmsvmid.txt b/docs/api/examples/delete_projectsprojectidiouvmsvmid.txt index ae31ce71..8270b459 100644 --- a/docs/api/examples/delete_projectsprojectidiouvmsvmid.txt +++ b/docs/api/examples/delete_projectsprojectidiouvmsvmid.txt @@ -1,13 +1,14 @@ -curl -i -X DELETE 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/becc8bf8-1936-4076-b7dd-ee83ba078907' +curl -i -X DELETE 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/0813062d-a796-44ef-a5ed-e690c633012e' -DELETE /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/becc8bf8-1936-4076-b7dd-ee83ba078907 HTTP/1.1 +DELETE /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/0813062d-a796-44ef-a5ed-e690c633012e HTTP/1.1 HTTP/1.1 204 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 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 index 94d35620..9432d0ee 100644 --- a/docs/api/examples/delete_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt +++ b/docs/api/examples/delete_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -1,13 +1,14 @@ -curl -i -X DELETE 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/779f3f81-60ab-4d26-860c-75f915b1d70b/adapters/1/ports/0/nio' +curl -i -X DELETE 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/8759140f-eb6f-4f3b-9a7f-86c79ec747b9/adapters/1/ports/0/nio' -DELETE /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/779f3f81-60ab-4d26-860c-75f915b1d70b/adapters/1/ports/0/nio HTTP/1.1 +DELETE /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/8759140f-eb6f-4f3b-9a7f-86c79ec747b9/adapters/1/ports/0/nio HTTP/1.1 HTTP/1.1 204 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 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 index c8d13190..a884ebc5 100644 --- a/docs/api/examples/delete_projectsprojectidqemuvmsvmid.txt +++ b/docs/api/examples/delete_projectsprojectidqemuvmsvmid.txt @@ -1,13 +1,14 @@ -curl -i -X DELETE 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/6537c17b-2e47-46f9-b96b-6fc66382709f' +curl -i -X DELETE 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/1b1a72b7-97ea-4598-8c02-2edc285f7987' -DELETE /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/6537c17b-2e47-46f9-b96b-6fc66382709f HTTP/1.1 +DELETE /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/1b1a72b7-97ea-4598-8c02-2edc285f7987 HTTP/1.1 HTTP/1.1 204 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 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 index 3689a022..f1b7a599 100644 --- a/docs/api/examples/delete_projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.txt +++ b/docs/api/examples/delete_projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -1,13 +1,14 @@ -curl -i -X DELETE 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/b54e4616-058f-413d-a589-0b7c5da20aa3/adapters/1/ports/0/nio' +curl -i -X DELETE 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/b5c23125-ea22-4008-a89e-72565ae9f746/adapters/1/ports/0/nio' -DELETE /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/b54e4616-058f-413d-a589-0b7c5da20aa3/adapters/1/ports/0/nio HTTP/1.1 +DELETE /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/b5c23125-ea22-4008-a89e-72565ae9f746/adapters/1/ports/0/nio HTTP/1.1 HTTP/1.1 204 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 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 index 75b27845..0097d4f8 100644 --- a/docs/api/examples/delete_projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.txt +++ b/docs/api/examples/delete_projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -1,13 +1,14 @@ -curl -i -X DELETE 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/387fb016-f1fc-4844-a25e-97c08ef77274/adapters/0/ports/0/nio' +curl -i -X DELETE 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/3f79083e-3da8-433e-9e9b-fe72a602ab4e/adapters/0/ports/0/nio' -DELETE /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/387fb016-f1fc-4844-a25e-97c08ef77274/adapters/0/ports/0/nio HTTP/1.1 +DELETE /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/3f79083e-3da8-433e-9e9b-fe72a602ab4e/adapters/0/ports/0/nio HTTP/1.1 HTTP/1.1 204 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 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 index ec2dd646..a65bc4f9 100644 --- a/docs/api/examples/delete_projectsprojectidvpcsvmsvmid.txt +++ b/docs/api/examples/delete_projectsprojectidvpcsvmsvmid.txt @@ -1,13 +1,14 @@ -curl -i -X DELETE 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/6bc73868-fea6-449b-af5c-e7b746e4129d' +curl -i -X DELETE 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/6f9f4ab8-1b77-4eee-b0fb-e8d299b843a7' -DELETE /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/6bc73868-fea6-449b-af5c-e7b746e4129d HTTP/1.1 +DELETE /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/6f9f4ab8-1b77-4eee-b0fb-e8d299b843a7 HTTP/1.1 HTTP/1.1 204 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 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 index 8c8e8be2..b82b051e 100644 --- a/docs/api/examples/delete_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt +++ b/docs/api/examples/delete_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -1,13 +1,14 @@ -curl -i -X DELETE 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/f7d1d6ad-422b-486a-8d23-1ff46cd8bc4b/adapters/0/ports/0/nio' +curl -i -X DELETE 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/6666f3a7-d805-4572-a35c-bc15213f0b52/adapters/0/ports/0/nio' -DELETE /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/f7d1d6ad-422b-486a-8d23-1ff46cd8bc4b/adapters/0/ports/0/nio HTTP/1.1 +DELETE /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/6666f3a7-d805-4572-a35c-bc15213f0b52/adapters/0/ports/0/nio HTTP/1.1 HTTP/1.1 204 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 X-ROUTE: /v1/projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio diff --git a/docs/api/examples/get_interfaces.txt b/docs/api/examples/get_interfaces.txt index 780bc706..546cee82 100644 --- a/docs/api/examples/get_interfaces.txt +++ b/docs/api/examples/get_interfaces.txt @@ -5,60 +5,68 @@ GET /v1/interfaces HTTP/1.1 HTTP/1.1 200 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive -CONTENT-LENGTH: 718 +CONTENT-LENGTH: 1012 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 X-ROUTE: /v1/interfaces [ { - "id": "lo0", - "name": "lo0" - }, - { - "id": "gif0", - "name": "gif0" - }, - { - "id": "stf0", - "name": "stf0" + "id": "bridge0", + "ip_address": "", + "name": "bridge0" }, { "id": "en0", + "ip_address": "", "name": "en0" }, { "id": "en1", + "ip_address": "192.168.84.114", "name": "en1" }, + { + "id": "en2", + "ip_address": "", + "name": "en2" + }, { "id": "fw0", + "ip_address": "", "name": "fw0" }, { - "id": "en2", - "name": "en2" + "id": "lo0", + "ip_address": "127.0.0.1", + "name": "lo0" }, { "id": "p2p0", + "ip_address": "", "name": "p2p0" }, - { - "id": "bridge0", - "name": "bridge0" - }, { "id": "vboxnet0", + "ip_address": "172.16.43.1", "name": "vboxnet0" }, { "id": "vboxnet1", + "ip_address": "192.168.59.3", "name": "vboxnet1" }, { "id": "vboxnet2", + "ip_address": "", "name": "vboxnet2" + }, + { + "id": "vboxnet3", + "ip_address": "192.168.99.1", + "name": "vboxnet3" } ] diff --git a/docs/api/examples/get_iouvms.txt b/docs/api/examples/get_iouvms.txt new file mode 100644 index 00000000..e9f45840 --- /dev/null +++ b/docs/api/examples/get_iouvms.txt @@ -0,0 +1,21 @@ +curl -i -X GET 'http://localhost:8000/v1/iou/vms' + +GET /v1/iou/vms HTTP/1.1 + + + +HTTP/1.1 200 +ACCESS-CONTROL-ALLOW-ORIGIN: * +CONNECTION: keep-alive +CONTENT-LENGTH: 72 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.5 GNS3/1.4.0dev13 +X-ROUTE: /v1/iou/vms + +[ + { + "filename": "iou.bin", + "path": "iou.bin" + } +] diff --git a/docs/api/examples/get_projects.txt b/docs/api/examples/get_projects.txt new file mode 100644 index 00000000..c838d962 --- /dev/null +++ b/docs/api/examples/get_projects.txt @@ -0,0 +1,31 @@ +curl -i -X GET 'http://localhost:8000/v1/projects' + +GET /v1/projects HTTP/1.1 + + + +HTTP/1.1 200 +ACCESS-CONTROL-ALLOW-ORIGIN: * +CONNECTION: keep-alive +CONTENT-LENGTH: 656 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.5 GNS3/1.4.0dev13 +X-ROUTE: /v1/projects + +[ + { + "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpmwnekqxo", + "name": "test", + "path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpmwnekqxo/00010203-0405-0607-0809-0a0b0c0d0e0b", + "project_id": "00010203-0405-0607-0809-0a0b0c0d0e0b", + "temporary": false + }, + { + "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpmwnekqxo", + "name": "test", + "path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpmwnekqxo/00010203-0405-0607-0809-0a0b0c0d0e0f", + "project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f", + "temporary": false + } +] diff --git a/docs/api/examples/get_projectsprojectid.txt b/docs/api/examples/get_projectsprojectid.txt index ebe85d97..abf0fc41 100644 --- a/docs/api/examples/get_projectsprojectid.txt +++ b/docs/api/examples/get_projectsprojectid.txt @@ -5,17 +5,18 @@ GET /v1/projects/00010203-0405-0607-0809-0a0b0c0d0e02 HTTP/1.1 HTTP/1.1 200 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive CONTENT-LENGTH: 297 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 X-ROUTE: /v1/projects/{project_id} { - "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmphbzo0jp9", + "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpchvw88rg", "name": "test", - "path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmphbzo0jp9/00010203-0405-0607-0809-0a0b0c0d0e02", + "path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpchvw88rg/00010203-0405-0607-0809-0a0b0c0d0e02", "project_id": "00010203-0405-0607-0809-0a0b0c0d0e02", "temporary": false } diff --git a/docs/api/examples/get_projectsprojectidfiles.txt b/docs/api/examples/get_projectsprojectidfiles.txt new file mode 100644 index 00000000..fe554479 --- /dev/null +++ b/docs/api/examples/get_projectsprojectidfiles.txt @@ -0,0 +1,25 @@ +curl -i -X GET 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/files' + +GET /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/files HTTP/1.1 + + + +HTTP/1.1 200 +ACCESS-CONTROL-ALLOW-ORIGIN: * +CONNECTION: keep-alive +CONTENT-LENGTH: 204 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.5 GNS3/1.4.0dev13 +X-ROUTE: /v1/projects/{project_id}/files + +[ + { + "md5sum": "ad0234829205b9033196ba818f7a872b", + "path": "test.txt" + }, + { + "md5sum": "098f6bcd4621d373cade4e832627b4f6", + "path": "vm-1/dynamips/test.bin" + } +] diff --git a/docs/api/examples/get_projectsprojectidiouvmsvmid.txt b/docs/api/examples/get_projectsprojectidiouvmsvmid.txt index 688fa3d7..ad02cc8e 100644 --- a/docs/api/examples/get_projectsprojectidiouvmsvmid.txt +++ b/docs/api/examples/get_projectsprojectidiouvmsvmid.txt @@ -1,29 +1,33 @@ -curl -i -X GET 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/ede1bba5-0723-4fd8-9e89-bdfffe5f5c8f' +curl -i -X GET 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/82f6e25c-6960-421d-9567-a7453642935b' -GET /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/ede1bba5-0723-4fd8-9e89-bdfffe5f5c8f HTTP/1.1 +GET /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/82f6e25c-6960-421d-9567-a7453642935b HTTP/1.1 HTTP/1.1 200 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive -CONTENT-LENGTH: 386 +CONTENT-LENGTH: 610 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 X-ROUTE: /v1/projects/{project_id}/iou/vms/{vm_id} { - "console": 2000, + "console": 2001, "ethernet_adapters": 2, - "initial_config": null, "iourc_path": null, "l1_keepalives": false, + "md5sum": null, "name": "PC TEST 1", "nvram": 128, "path": "iou.bin", + "private_config": null, "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "ram": 256, "serial_adapters": 2, + "startup_config": null, "use_default_iou_values": true, - "vm_id": "ede1bba5-0723-4fd8-9e89-bdfffe5f5c8f" + "vm_directory": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmp2yt9sd1l/a1e920ca-338a-4e9f-b363-aa607b09dd80/project-files/iou/82f6e25c-6960-421d-9567-a7453642935b", + "vm_id": "82f6e25c-6960-421d-9567-a7453642935b" } diff --git a/docs/api/examples/get_projectsprojectidiouvmsvmidconfigs.txt b/docs/api/examples/get_projectsprojectidiouvmsvmidconfigs.txt new file mode 100644 index 00000000..fb976908 --- /dev/null +++ b/docs/api/examples/get_projectsprojectidiouvmsvmidconfigs.txt @@ -0,0 +1,18 @@ +curl -i -X GET 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/9ff2d283-1365-4422-93e7-e14c56b809cb/configs' + +GET /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/9ff2d283-1365-4422-93e7-e14c56b809cb/configs HTTP/1.1 + + + +HTTP/1.1 200 +ACCESS-CONTROL-ALLOW-ORIGIN: * +CONNECTION: keep-alive +CONTENT-LENGTH: 40 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.5 GNS3/1.4.0dev13 +X-ROUTE: /v1/projects/{project_id}/iou/vms/{vm_id}/configs + +{ + "startup_config_content": "TEST" +} diff --git a/docs/api/examples/get_projectsprojectidiouvmsvmidinitialconfig.txt b/docs/api/examples/get_projectsprojectidiouvmsvmidinitialconfig.txt deleted file mode 100644 index 4b3abdb5..00000000 --- a/docs/api/examples/get_projectsprojectidiouvmsvmidinitialconfig.txt +++ /dev/null @@ -1,17 +0,0 @@ -curl -i -X GET 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/821b0dc3-4cc0-4899-8184-75bfc22db584/initial_config' - -GET /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/821b0dc3-4cc0-4899-8184-75bfc22db584/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.1.rc2 -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 index 26de1035..afcaa30b 100644 --- a/docs/api/examples/get_projectsprojectidqemuvmsvmid.txt +++ b/docs/api/examples/get_projectsprojectidqemuvmsvmid.txt @@ -1,35 +1,56 @@ -curl -i -X GET 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/e3935d2d-2bf9-4cde-8c7e-0bd1d74c3dad' +curl -i -X GET 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/84777300-eef4-4a3a-9135-d7009bdb0722' -GET /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/e3935d2d-2bf9-4cde-8c7e-0bd1d74c3dad HTTP/1.1 +GET /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/84777300-eef4-4a3a-9135-d7009bdb0722 HTTP/1.1 HTTP/1.1 200 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive -CONTENT-LENGTH: 597 +CONTENT-LENGTH: 1353 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 X-ROUTE: /v1/projects/{project_id}/qemu/vms/{vm_id} { + "acpi_shutdown": false, "adapter_type": "e1000", "adapters": 1, - "console": 2000, + "boot_priority": "c", + "cdrom_image": "", + "cdrom_image_md5sum": null, + "console": 2001, + "console_type": "telnet", "cpu_throttling": 0, + "cpus": 1, "hda_disk_image": "", + "hda_disk_image_md5sum": null, + "hda_disk_interface": "ide", "hdb_disk_image": "", + "hdb_disk_image_md5sum": null, + "hdb_disk_interface": "ide", "hdc_disk_image": "", + "hdc_disk_image_md5sum": null, + "hdc_disk_interface": "ide", "hdd_disk_image": "", + "hdd_disk_image_md5sum": null, + "hdd_disk_interface": "ide", "initrd": "", + "initrd_md5sum": null, "kernel_command_line": "", "kernel_image": "", + "kernel_image_md5sum": null, "legacy_networking": false, + "mac_address": "00:00:ab:07:22:00", "name": "PC TEST 1", "options": "", + "platform": "x86_64", "process_priority": "low", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpyasp9636/qemu_x42", + "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmph3pfurip/qemu-system-x86_64", "ram": 256, - "vm_id": "e3935d2d-2bf9-4cde-8c7e-0bd1d74c3dad" + "usage": "", + "vm_directory": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpqqnauky9/a1e920ca-338a-4e9f-b363-aa607b09dd80/project-files/qemu/84777300-eef4-4a3a-9135-d7009bdb0722", + "vm_id": "84777300-eef4-4a3a-9135-d7009bdb0722" } diff --git a/docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt b/docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt index cd98bab1..b2bee70c 100644 --- a/docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt +++ b/docs/api/examples/get_projectsprojectidvirtualboxvmsvmid.txt @@ -1,27 +1,30 @@ -curl -i -X GET 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/f49ff215-0872-4bf7-90c8-3d9ecc2b2f2b' +curl -i -X GET 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/63b9b470-5a07-4cfe-b9d5-ae5d5518b988' -GET /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/f49ff215-0872-4bf7-90c8-3d9ecc2b2f2b HTTP/1.1 +GET /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/63b9b470-5a07-4cfe-b9d5-ae5d5518b988 HTTP/1.1 HTTP/1.1 200 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive -CONTENT-LENGTH: 361 +CONTENT-LENGTH: 415 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 X-ROUTE: /v1/projects/{project_id}/virtualbox/vms/{vm_id} { + "acpi_shutdown": false, "adapter_type": "Intel PRO/1000 MT Desktop (82540EM)", "adapters": 0, - "console": 2001, + "console": 2002, "enable_remote_console": false, "headless": false, "name": "VMTEST", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "ram": 0, "use_any_adapter": false, - "vm_id": "f49ff215-0872-4bf7-90c8-3d9ecc2b2f2b", + "vm_directory": null, + "vm_id": "63b9b470-5a07-4cfe-b9d5-ae5d5518b988", "vmname": "VMTEST" } diff --git a/docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt b/docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt index 195eb61a..9854b65a 100644 --- a/docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt +++ b/docs/api/examples/get_projectsprojectidvpcsvmsvmid.txt @@ -1,22 +1,25 @@ -curl -i -X GET 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/3ff6ff9e-93de-45ae-a7ec-e296d406ffe1' +curl -i -X GET 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/7e24fcc2-e38c-43e8-a7bb-fc22d7b91547' -GET /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/3ff6ff9e-93de-45ae-a7ec-e296d406ffe1 HTTP/1.1 +GET /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/7e24fcc2-e38c-43e8-a7bb-fc22d7b91547 HTTP/1.1 HTTP/1.1 200 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive -CONTENT-LENGTH: 220 +CONTENT-LENGTH: 422 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 X-ROUTE: /v1/projects/{project_id}/vpcs/vms/{vm_id} { - "console": 2009, + "console": 2010, "name": "PC TEST 1", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "startup_script": null, "startup_script_path": null, - "vm_id": "3ff6ff9e-93de-45ae-a7ec-e296d406ffe1" + "status": "stopped", + "vm_directory": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpqqnauky9/a1e920ca-338a-4e9f-b363-aa607b09dd80/project-files/vpcs/7e24fcc2-e38c-43e8-a7bb-fc22d7b91547", + "vm_id": "7e24fcc2-e38c-43e8-a7bb-fc22d7b91547" } diff --git a/docs/api/examples/get_qemubinaries.txt b/docs/api/examples/get_qemubinaries.txt index 003055b4..bba76e15 100644 --- a/docs/api/examples/get_qemubinaries.txt +++ b/docs/api/examples/get_qemubinaries.txt @@ -1,24 +1,33 @@ -curl -i -X GET 'http://localhost:8000/v1/qemu/binaries' +curl -i -X GET 'http://localhost:8000/v1/qemu/binaries' -d '{"archs": ["i386"]}' GET /v1/qemu/binaries HTTP/1.1 - +{ + "archs": [ + "i386" + ] +} HTTP/1.1 200 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive -CONTENT-LENGTH: 134 +CONTENT-LENGTH: 212 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 X-ROUTE: /v1/qemu/binaries [ { - "path": "/tmp/1", + "path": "/tmp/x86_64", "version": "2.2.0" }, { - "path": "/tmp/2", + "path": "/tmp/alpha", + "version": "2.1.0" + }, + { + "path": "/tmp/i386", "version": "2.1.0" } ] diff --git a/docs/api/examples/get_qemucapabilities.txt b/docs/api/examples/get_qemucapabilities.txt new file mode 100644 index 00000000..1f348cfa --- /dev/null +++ b/docs/api/examples/get_qemucapabilities.txt @@ -0,0 +1,20 @@ +curl -i -X GET 'http://localhost:8000/v1/qemu/capabilities' + +GET /v1/qemu/capabilities HTTP/1.1 + + + +HTTP/1.1 200 +ACCESS-CONTROL-ALLOW-ORIGIN: * +CONNECTION: keep-alive +CONTENT-LENGTH: 39 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.5 GNS3/1.4.0dev13 +X-ROUTE: /v1/qemu/capabilities + +{ + "kvm": [ + "x86_64" + ] +} diff --git a/docs/api/examples/get_version.txt b/docs/api/examples/get_version.txt index d267bb31..76196256 100644 --- a/docs/api/examples/get_version.txt +++ b/docs/api/examples/get_version.txt @@ -5,14 +5,15 @@ GET /v1/version HTTP/1.1 HTTP/1.1 200 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive -CONTENT-LENGTH: 49 +CONTENT-LENGTH: 50 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 X-ROUTE: /v1/version { "local": true, - "version": "1.3.1.rc2" + "version": "1.4.0dev13" } diff --git a/docs/api/examples/post_configreload.txt b/docs/api/examples/post_configreload.txt index 35f71f23..6d0ec2c0 100644 --- a/docs/api/examples/post_configreload.txt +++ b/docs/api/examples/post_configreload.txt @@ -5,9 +5,10 @@ POST /v1/config/reload HTTP/1.1 HTTP/1.1 201 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 X-ROUTE: /v1/config/reload diff --git a/docs/api/examples/post_projects.txt b/docs/api/examples/post_projects.txt index c53e1fd7..e63a90ac 100644 --- a/docs/api/examples/post_projects.txt +++ b/docs/api/examples/post_projects.txt @@ -7,17 +7,18 @@ POST /v1/projects HTTP/1.1 HTTP/1.1 201 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive CONTENT-LENGTH: 297 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 X-ROUTE: /v1/projects { - "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmp_6lclsv7", + "location": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpp319gdgn", "name": "test", - "path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmp_6lclsv7/187543e2-5d46-4108-a623-cfdd31fa300e", - "project_id": "187543e2-5d46-4108-a623-cfdd31fa300e", + "path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpp319gdgn/054e6ecb-1e9d-4c74-ad08-1209264bd96d", + "project_id": "054e6ecb-1e9d-4c74-ad08-1209264bd96d", "temporary": false } diff --git a/docs/api/examples/post_projectsprojectidclose.txt b/docs/api/examples/post_projectsprojectidclose.txt index a73b52d5..e3538f17 100644 --- a/docs/api/examples/post_projectsprojectidclose.txt +++ b/docs/api/examples/post_projectsprojectidclose.txt @@ -5,9 +5,10 @@ POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/close HTTP/1.1 HTTP/1.1 204 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 X-ROUTE: /v1/projects/{project_id}/close diff --git a/docs/api/examples/post_projectsprojectidcommit.txt b/docs/api/examples/post_projectsprojectidcommit.txt index 1bc0885f..18172833 100644 --- a/docs/api/examples/post_projectsprojectidcommit.txt +++ b/docs/api/examples/post_projectsprojectidcommit.txt @@ -5,9 +5,10 @@ POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/commit HTTP/1.1 HTTP/1.1 204 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 X-ROUTE: /v1/projects/{project_id}/commit diff --git a/docs/api/examples/post_projectsprojectidiouvms.txt b/docs/api/examples/post_projectsprojectidiouvms.txt index f7af542c..bf0fe67c 100644 --- a/docs/api/examples/post_projectsprojectidiouvms.txt +++ b/docs/api/examples/post_projectsprojectidiouvms.txt @@ -1,40 +1,38 @@ -curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms' -d '{"ethernet_adapters": 0, "initial_config_content": "hostname test", "iourc_content": "test", "l1_keepalives": true, "name": "PC TEST 1", "nvram": 512, "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-4450/test_iou_create_with_params0/iou.bin", "ram": 1024, "serial_adapters": 4, "use_default_iou_values": true}' +curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms' -d '{"name": "PC TEST 1", "path": "iou.bin", "startup_config_content": "hostname test", "vm_id": "afa904bc-8968-4e7c-87b6-11d92174f1e6"}' POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms HTTP/1.1 { - "ethernet_adapters": 0, - "initial_config_content": "hostname test", - "iourc_content": "test", - "l1_keepalives": true, "name": "PC TEST 1", - "nvram": 512, - "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-4450/test_iou_create_with_params0/iou.bin", - "ram": 1024, - "serial_adapters": 4, - "use_default_iou_values": true + "path": "iou.bin", + "startup_config_content": "hostname test", + "vm_id": "afa904bc-8968-4e7c-87b6-11d92174f1e6" } HTTP/1.1 201 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive -CONTENT-LENGTH: 466 +CONTENT-LENGTH: 626 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 X-ROUTE: /v1/projects/{project_id}/iou/vms { - "console": 2000, - "ethernet_adapters": 0, - "initial_config": "initial-config.cfg", - "iourc_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmps796q8lx/iourc", - "l1_keepalives": true, + "console": 2001, + "ethernet_adapters": 2, + "iourc_path": null, + "l1_keepalives": false, + "md5sum": null, "name": "PC TEST 1", - "nvram": 512, + "nvram": 128, "path": "iou.bin", + "private_config": null, "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "ram": 1024, - "serial_adapters": 4, + "ram": 256, + "serial_adapters": 2, + "startup_config": "startup-config.cfg", "use_default_iou_values": true, - "vm_id": "69f5842a-d4e5-45fe-a500-1010c72f1748" + "vm_directory": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmp2yt9sd1l/a1e920ca-338a-4e9f-b363-aa607b09dd80/project-files/iou/afa904bc-8968-4e7c-87b6-11d92174f1e6", + "vm_id": "afa904bc-8968-4e7c-87b6-11d92174f1e6" } diff --git a/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt b/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt index 97dc6046..2ccdc22e 100644 --- a/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt +++ b/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -1,21 +1,22 @@ -curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/04648c72-4339-471e-aa12-57b42d7ea18b/adapters/1/ports/0/nio' -d '{"ethernet_device": "eth0", "type": "nio_generic_ethernet"}' +curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/eea449ce-e25b-4beb-b50f-fcd77241a97b/adapters/1/ports/0/nio' -d '{"ethernet_device": "bridge0", "type": "nio_generic_ethernet"}' -POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/04648c72-4339-471e-aa12-57b42d7ea18b/adapters/1/ports/0/nio HTTP/1.1 +POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/eea449ce-e25b-4beb-b50f-fcd77241a97b/adapters/1/ports/0/nio HTTP/1.1 { - "ethernet_device": "eth0", + "ethernet_device": "bridge0", "type": "nio_generic_ethernet" } HTTP/1.1 201 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive -CONTENT-LENGTH: 69 +CONTENT-LENGTH: 72 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 X-ROUTE: /v1/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio { - "ethernet_device": "eth0", + "ethernet_device": "bridge0", "type": "nio_generic_ethernet" } diff --git a/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.txt b/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.txt index b91d6987..1a506166 100644 --- a/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.txt +++ b/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.txt @@ -1,6 +1,6 @@ -curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/6056b617-d0c4-4683-bc9e-d0130beec951/adapters/0/ports/0/start_capture' -d '{"capture_file_name": "test.pcap", "data_link_type": "DLT_EN10MB"}' +curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/14a4f240-54e7-47f7-b9e7-3640528415fe/adapters/0/ports/0/start_capture' -d '{"capture_file_name": "test.pcap", "data_link_type": "DLT_EN10MB"}' -POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/6056b617-d0c4-4683-bc9e-d0130beec951/adapters/0/ports/0/start_capture HTTP/1.1 +POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/14a4f240-54e7-47f7-b9e7-3640528415fe/adapters/0/ports/0/start_capture HTTP/1.1 { "capture_file_name": "test.pcap", "data_link_type": "DLT_EN10MB" @@ -8,13 +8,14 @@ POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/6056b617-d0c4-468 HTTP/1.1 200 +ACCESS-CONTROL-ALLOW-ORIGIN: * 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.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 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/tmp367810hd/a1e920ca-338a-4e9f-b363-aa607b09dd80/project-files/captures/test.pcap" + "pcap_file_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmp2yt9sd1l/a1e920ca-338a-4e9f-b363-aa607b09dd80/project-files/captures/test.pcap" } diff --git a/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.txt b/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.txt index 519c2f2c..db79902e 100644 --- a/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.txt +++ b/docs/api/examples/post_projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.txt @@ -1,13 +1,14 @@ -curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/0fb3c393-7cda-4abc-ad76-a9e7af04abea/adapters/0/ports/0/stop_capture' -d '{}' +curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/992e1579-2232-4aac-93ef-970c3bb67966/adapters/0/ports/0/stop_capture' -d '{}' -POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/0fb3c393-7cda-4abc-ad76-a9e7af04abea/adapters/0/ports/0/stop_capture HTTP/1.1 +POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/992e1579-2232-4aac-93ef-970c3bb67966/adapters/0/ports/0/stop_capture HTTP/1.1 {} HTTP/1.1 204 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 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 index 8bce1041..7c2eb3b4 100644 --- a/docs/api/examples/post_projectsprojectidiouvmsvmidreload.txt +++ b/docs/api/examples/post_projectsprojectidiouvmsvmidreload.txt @@ -1,13 +1,14 @@ -curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/b32c38db-3692-4719-b94f-2b3f664cd06f/reload' -d '{}' +curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/7f23517b-b45e-4e7b-b908-c501c36abe16/reload' -d '{}' -POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/b32c38db-3692-4719-b94f-2b3f664cd06f/reload HTTP/1.1 +POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/7f23517b-b45e-4e7b-b908-c501c36abe16/reload HTTP/1.1 {} HTTP/1.1 204 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 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 index 5f5e6079..6e308f54 100644 --- a/docs/api/examples/post_projectsprojectidiouvmsvmidstart.txt +++ b/docs/api/examples/post_projectsprojectidiouvmsvmidstart.txt @@ -1,13 +1,16 @@ -curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/55209a6f-8fe0-49d1-a884-b7cd09547b5c/start' -d '{}' +curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/2923f599-894a-4b18-a9eb-17a389298f84/start' -d '{"iourc_content": "test"}' -POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/55209a6f-8fe0-49d1-a884-b7cd09547b5c/start HTTP/1.1 -{} +POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/2923f599-894a-4b18-a9eb-17a389298f84/start HTTP/1.1 +{ + "iourc_content": "test" +} HTTP/1.1 204 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 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 index 66a5ebee..3feaf6b0 100644 --- a/docs/api/examples/post_projectsprojectidiouvmsvmidstop.txt +++ b/docs/api/examples/post_projectsprojectidiouvmsvmidstop.txt @@ -1,13 +1,14 @@ -curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/d893bd2c-d84e-4f89-ad84-f3bdfaf460b6/stop' -d '{}' +curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/ff05b41f-cd2b-4b33-90be-9362430b68ae/stop' -d '{}' -POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/d893bd2c-d84e-4f89-ad84-f3bdfaf460b6/stop HTTP/1.1 +POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/ff05b41f-cd2b-4b33-90be-9362430b68ae/stop HTTP/1.1 {} HTTP/1.1 204 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 X-ROUTE: /v1/projects/{project_id}/iou/vms/{vm_id}/stop diff --git a/docs/api/examples/post_projectsprojectidportsudp.txt b/docs/api/examples/post_projectsprojectidportsudp.txt index 6ee01042..b82c6f19 100644 --- a/docs/api/examples/post_projectsprojectidportsudp.txt +++ b/docs/api/examples/post_projectsprojectidportsudp.txt @@ -5,11 +5,12 @@ POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/ports/udp HTTP/1.1 HTTP/1.1 201 +ACCESS-CONTROL-ALLOW-ORIGIN: * 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.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 X-ROUTE: /v1/projects/{project_id}/ports/udp { diff --git a/docs/api/examples/post_projectsprojectidqemuvms.txt b/docs/api/examples/post_projectsprojectidqemuvms.txt index 7a780fa4..7949b11e 100644 --- a/docs/api/examples/post_projectsprojectidqemuvms.txt +++ b/docs/api/examples/post_projectsprojectidqemuvms.txt @@ -1,40 +1,61 @@ -curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms' -d '{"hda_disk_image": "/tmp/hda", "name": "PC TEST 1", "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpyasp9636/qemu_x42", "ram": 1024}' +curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms' -d '{"hda_disk_image": "linux.img", "name": "PC TEST 1", "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmph3pfurip/qemu-system-x86_64", "ram": 1024}' POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms HTTP/1.1 { - "hda_disk_image": "/tmp/hda", + "hda_disk_image": "linux.img", "name": "PC TEST 1", - "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpyasp9636/qemu_x42", + "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmph3pfurip/qemu-system-x86_64", "ram": 1024 } HTTP/1.1 201 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive -CONTENT-LENGTH: 606 +CONTENT-LENGTH: 1363 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 X-ROUTE: /v1/projects/{project_id}/qemu/vms { + "acpi_shutdown": false, "adapter_type": "e1000", "adapters": 1, - "console": 2000, + "boot_priority": "c", + "cdrom_image": "", + "cdrom_image_md5sum": null, + "console": 2001, + "console_type": "telnet", "cpu_throttling": 0, - "hda_disk_image": "/tmp/hda", + "cpus": 1, + "hda_disk_image": "linux.img", + "hda_disk_image_md5sum": null, + "hda_disk_interface": "ide", "hdb_disk_image": "", + "hdb_disk_image_md5sum": null, + "hdb_disk_interface": "ide", "hdc_disk_image": "", + "hdc_disk_image_md5sum": null, + "hdc_disk_interface": "ide", "hdd_disk_image": "", + "hdd_disk_image_md5sum": null, + "hdd_disk_interface": "ide", "initrd": "", + "initrd_md5sum": null, "kernel_command_line": "", "kernel_image": "", + "kernel_image_md5sum": null, "legacy_networking": false, + "mac_address": "00:00:ab:50:58:00", "name": "PC TEST 1", "options": "", + "platform": "x86_64", "process_priority": "low", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpyasp9636/qemu_x42", + "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmph3pfurip/qemu-system-x86_64", "ram": 1024, - "vm_id": "8c325041-39a8-4c31-b921-b66dadadc353" + "usage": "", + "vm_directory": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpqqnauky9/a1e920ca-338a-4e9f-b363-aa607b09dd80/project-files/qemu/b949f2e2-d338-4b51-aea9-270196ed5058", + "vm_id": "b949f2e2-d338-4b51-aea9-270196ed5058" } diff --git a/docs/api/examples/post_projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.txt b/docs/api/examples/post_projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.txt index 59d13e6a..af51745e 100644 --- a/docs/api/examples/post_projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.txt +++ b/docs/api/examples/post_projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -1,21 +1,22 @@ -curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/e791569e-7f95-4a1d-9f8d-b48611afeef3/adapters/1/ports/0/nio' -d '{"ethernet_device": "eth0", "type": "nio_generic_ethernet"}' +curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/82435d91-3cb5-4a44-abff-34a4ae46b24f/adapters/1/ports/0/nio' -d '{"ethernet_device": "eth0", "type": "nio_generic_ethernet"}' -POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/e791569e-7f95-4a1d-9f8d-b48611afeef3/adapters/1/ports/0/nio HTTP/1.1 +POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/82435d91-3cb5-4a44-abff-34a4ae46b24f/adapters/1/ports/0/nio HTTP/1.1 { "ethernet_device": "eth0", "type": "nio_generic_ethernet" } -HTTP/1.1 201 +HTTP/1.1 409 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive -CONTENT-LENGTH: 69 +CONTENT-LENGTH: 89 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 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" + "message": "NIO of type nio_generic_ethernet is not supported", + "status": 409 } diff --git a/docs/api/examples/post_projectsprojectidqemuvmsvmidreload.txt b/docs/api/examples/post_projectsprojectidqemuvmsvmidreload.txt index 5c2a2bf8..0140288a 100644 --- a/docs/api/examples/post_projectsprojectidqemuvmsvmidreload.txt +++ b/docs/api/examples/post_projectsprojectidqemuvmsvmidreload.txt @@ -1,13 +1,14 @@ -curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/e6de45b1-048b-498b-9875-de76762532e9/reload' -d '{}' +curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/3275bdce-9b75-445f-8168-0103cd524182/reload' -d '{}' -POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/e6de45b1-048b-498b-9875-de76762532e9/reload HTTP/1.1 +POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/3275bdce-9b75-445f-8168-0103cd524182/reload HTTP/1.1 {} HTTP/1.1 204 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 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 index 85ba0910..8306c0c2 100644 --- a/docs/api/examples/post_projectsprojectidqemuvmsvmidresume.txt +++ b/docs/api/examples/post_projectsprojectidqemuvmsvmidresume.txt @@ -1,13 +1,14 @@ -curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/d7152d7c-7f23-4d92-9ee1-fae132a50b3b/resume' -d '{}' +curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/2d03502f-6b3f-4551-b924-b74fd5d6cdf5/resume' -d '{}' -POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/d7152d7c-7f23-4d92-9ee1-fae132a50b3b/resume HTTP/1.1 +POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/2d03502f-6b3f-4551-b924-b74fd5d6cdf5/resume HTTP/1.1 {} HTTP/1.1 204 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 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 index 0cd46103..b4699682 100644 --- a/docs/api/examples/post_projectsprojectidqemuvmsvmidstart.txt +++ b/docs/api/examples/post_projectsprojectidqemuvmsvmidstart.txt @@ -1,13 +1,14 @@ -curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/031c1e57-577c-4ff4-91d1-da6fe0816fdd/start' -d '{}' +curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/04afa03a-3d9b-4aa7-945f-8f3077b6790e/start' -d '{}' -POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/031c1e57-577c-4ff4-91d1-da6fe0816fdd/start HTTP/1.1 +POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/04afa03a-3d9b-4aa7-945f-8f3077b6790e/start HTTP/1.1 {} HTTP/1.1 204 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 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 index 31095727..1b0b590e 100644 --- a/docs/api/examples/post_projectsprojectidqemuvmsvmidstop.txt +++ b/docs/api/examples/post_projectsprojectidqemuvmsvmidstop.txt @@ -1,13 +1,14 @@ -curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/79ca17e8-0194-4682-bb2a-0bdd9f7d1e1a/stop' -d '{}' +curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/f5bfbadc-9a09-4090-a624-89be68858746/stop' -d '{}' -POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/79ca17e8-0194-4682-bb2a-0bdd9f7d1e1a/stop HTTP/1.1 +POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/f5bfbadc-9a09-4090-a624-89be68858746/stop HTTP/1.1 {} HTTP/1.1 204 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 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 index 2b8146c9..5fee4174 100644 --- a/docs/api/examples/post_projectsprojectidqemuvmsvmidsuspend.txt +++ b/docs/api/examples/post_projectsprojectidqemuvmsvmidsuspend.txt @@ -1,13 +1,14 @@ -curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/7e24f49b-51ea-410f-bc94-16fc58071493/suspend' -d '{}' +curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/bb00b1e0-f19d-4ce8-b99a-82c1518fa33e/suspend' -d '{}' -POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/7e24f49b-51ea-410f-bc94-16fc58071493/suspend HTTP/1.1 +POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/bb00b1e0-f19d-4ce8-b99a-82c1518fa33e/suspend HTTP/1.1 {} HTTP/1.1 204 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 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 index bae91853..18ea88be 100644 --- a/docs/api/examples/post_projectsprojectidvirtualboxvms.txt +++ b/docs/api/examples/post_projectsprojectidvirtualboxvms.txt @@ -9,23 +9,26 @@ POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms HTTP/1.1 HTTP/1.1 201 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive -CONTENT-LENGTH: 355 +CONTENT-LENGTH: 409 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 X-ROUTE: /v1/projects/{project_id}/virtualbox/vms { + "acpi_shutdown": false, "adapter_type": "Intel PRO/1000 MT Desktop (82540EM)", "adapters": 0, - "console": 2000, + "console": 2001, "enable_remote_console": false, "headless": false, "name": "VM1", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "ram": 0, "use_any_adapter": false, - "vm_id": "fd208626-81e5-449e-b145-fd6993f5097c", + "vm_directory": null, + "vm_id": "39cc5964-d0be-487b-9f0c-8fbea600e452", "vmname": "VM1" } diff --git a/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.txt b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.txt index cf095b1b..2b40e2b9 100644 --- a/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.txt +++ b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -1,6 +1,6 @@ -curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/5463a797-0eb0-41d2-8b32-1efbd7a353cc/adapters/0/ports/0/nio' -d '{"lport": 4242, "rhost": "127.0.0.1", "rport": 4343, "type": "nio_udp"}' +curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/556aba72-c19a-4fc3-a350-9454791687f4/adapters/0/ports/0/nio' -d '{"lport": 4242, "rhost": "127.0.0.1", "rport": 4343, "type": "nio_udp"}' -POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/5463a797-0eb0-41d2-8b32-1efbd7a353cc/adapters/0/ports/0/nio HTTP/1.1 +POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/556aba72-c19a-4fc3-a350-9454791687f4/adapters/0/ports/0/nio HTTP/1.1 { "lport": 4242, "rhost": "127.0.0.1", @@ -10,11 +10,12 @@ POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/5463a797-0 HTTP/1.1 201 +ACCESS-CONTROL-ALLOW-ORIGIN: * 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.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 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/post_projectsprojectidvirtualboxvmsvmidreload.txt b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidreload.txt index 54f0322a..b4eb77d6 100644 --- a/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidreload.txt +++ b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidreload.txt @@ -1,13 +1,14 @@ -curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/f24209c8-671e-428a-9561-db4775f6b8a7/reload' -d '{}' +curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/1a517572-9b86-441a-8347-1d28b01d9a41/reload' -d '{}' -POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/f24209c8-671e-428a-9561-db4775f6b8a7/reload HTTP/1.1 +POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/1a517572-9b86-441a-8347-1d28b01d9a41/reload HTTP/1.1 {} HTTP/1.1 204 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 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 index 039ec17f..7ff55602 100644 --- a/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidresume.txt +++ b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidresume.txt @@ -1,13 +1,14 @@ -curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/70df82f6-e868-4ab3-9be9-d456871f41dc/resume' -d '{}' +curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/7f5648e4-0ce8-49b3-b470-f2b821a5fe3e/resume' -d '{}' -POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/70df82f6-e868-4ab3-9be9-d456871f41dc/resume HTTP/1.1 +POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/7f5648e4-0ce8-49b3-b470-f2b821a5fe3e/resume HTTP/1.1 {} HTTP/1.1 204 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 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 index 1f3ad83e..a37f7d80 100644 --- a/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidstart.txt +++ b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidstart.txt @@ -1,13 +1,14 @@ -curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/354d48fe-cab6-41d8-8cc1-64716e02c3a8/start' -d '{}' +curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/ee1fef8f-cbe1-4590-8c47-fbd12f7f6beb/start' -d '{}' -POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/354d48fe-cab6-41d8-8cc1-64716e02c3a8/start HTTP/1.1 +POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/ee1fef8f-cbe1-4590-8c47-fbd12f7f6beb/start HTTP/1.1 {} HTTP/1.1 204 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 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 index 9c16d5c2..d1b07cbf 100644 --- a/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidstop.txt +++ b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidstop.txt @@ -1,13 +1,14 @@ -curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/90733316-a02a-490b-b8c4-e6ea4a32296a/stop' -d '{}' +curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/5e8543c3-45a7-4668-824f-01eaeb848898/stop' -d '{}' -POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/90733316-a02a-490b-b8c4-e6ea4a32296a/stop HTTP/1.1 +POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/5e8543c3-45a7-4668-824f-01eaeb848898/stop HTTP/1.1 {} HTTP/1.1 204 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 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 index 6cd4c9bc..e7db500d 100644 --- a/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidsuspend.txt +++ b/docs/api/examples/post_projectsprojectidvirtualboxvmsvmidsuspend.txt @@ -1,13 +1,14 @@ -curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/0f257aa5-d61d-4ee9-872a-898462f7ecdf/suspend' -d '{}' +curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/95f620da-c0e5-4404-81fc-10f3e18786cb/suspend' -d '{}' -POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/0f257aa5-d61d-4ee9-872a-898462f7ecdf/suspend HTTP/1.1 +POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/95f620da-c0e5-4404-81fc-10f3e18786cb/suspend HTTP/1.1 {} HTTP/1.1 204 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 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 index 629a420f..584c6d5b 100644 --- a/docs/api/examples/post_projectsprojectidvpcsvms.txt +++ b/docs/api/examples/post_projectsprojectidvpcsvms.txt @@ -7,18 +7,21 @@ POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms HTTP/1.1 HTTP/1.1 201 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive -CONTENT-LENGTH: 220 +CONTENT-LENGTH: 422 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 X-ROUTE: /v1/projects/{project_id}/vpcs/vms { - "console": 2009, + "console": 2010, "name": "PC TEST 1", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "startup_script": null, "startup_script_path": null, - "vm_id": "68c6af80-0a82-406e-b051-24c95bd728f4" + "status": "stopped", + "vm_directory": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpqqnauky9/a1e920ca-338a-4e9f-b363-aa607b09dd80/project-files/vpcs/d1808514-893b-4aa5-86c2-c4dea88ff895", + "vm_id": "d1808514-893b-4aa5-86c2-c4dea88ff895" } diff --git a/docs/api/examples/post_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt b/docs/api/examples/post_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt index 360ffc76..37833633 100644 --- a/docs/api/examples/post_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt +++ b/docs/api/examples/post_projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.txt @@ -1,6 +1,6 @@ -curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/4125e37f-2bf1-435c-a86c-ae1fce4c916a/adapters/0/ports/0/nio' -d '{"lport": 4242, "rhost": "127.0.0.1", "rport": 4343, "type": "nio_udp"}' +curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/fbf2e4d7-d60e-4e03-95bc-1bd53b74689a/adapters/0/ports/0/nio' -d '{"lport": 4242, "rhost": "127.0.0.1", "rport": 4343, "type": "nio_udp"}' -POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/4125e37f-2bf1-435c-a86c-ae1fce4c916a/adapters/0/ports/0/nio HTTP/1.1 +POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/fbf2e4d7-d60e-4e03-95bc-1bd53b74689a/adapters/0/ports/0/nio HTTP/1.1 { "lport": 4242, "rhost": "127.0.0.1", @@ -10,11 +10,12 @@ POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/4125e37f-2bf1-43 HTTP/1.1 201 +ACCESS-CONTROL-ALLOW-ORIGIN: * 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.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 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/post_projectsprojectidvpcsvmsvmidreload.txt b/docs/api/examples/post_projectsprojectidvpcsvmsvmidreload.txt index 7aa26e08..2bb604c9 100644 --- a/docs/api/examples/post_projectsprojectidvpcsvmsvmidreload.txt +++ b/docs/api/examples/post_projectsprojectidvpcsvmsvmidreload.txt @@ -1,13 +1,14 @@ -curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/4c5c5174-07bf-4f2a-93ab-cec244e24852/reload' -d '{}' +curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/1a1b95de-cc91-4a90-870b-5c7c8965eb5d/reload' -d '{}' -POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/4c5c5174-07bf-4f2a-93ab-cec244e24852/reload HTTP/1.1 +POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/1a1b95de-cc91-4a90-870b-5c7c8965eb5d/reload HTTP/1.1 {} HTTP/1.1 204 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 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 index 04eae9b0..ae5a8998 100644 --- a/docs/api/examples/post_projectsprojectidvpcsvmsvmidstart.txt +++ b/docs/api/examples/post_projectsprojectidvpcsvmsvmidstart.txt @@ -1,13 +1,14 @@ -curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/43ac3ea8-78a7-405e-ad5e-653293c48e66/start' -d '{}' +curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/184c6dcc-a8fd-4cc9-b333-86cc24f2e98e/start' -d '{}' -POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/43ac3ea8-78a7-405e-ad5e-653293c48e66/start HTTP/1.1 +POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/184c6dcc-a8fd-4cc9-b333-86cc24f2e98e/start HTTP/1.1 {} HTTP/1.1 204 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 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 index a8a44b4f..4352ce57 100644 --- a/docs/api/examples/post_projectsprojectidvpcsvmsvmidstop.txt +++ b/docs/api/examples/post_projectsprojectidvpcsvmsvmidstop.txt @@ -1,13 +1,14 @@ -curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/59df4b41-6a23-4c99-9370-600b3a2cff23/stop' -d '{}' +curl -i -X POST 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/bc34aac0-6c5e-4b91-b2ea-47b76d4b26f1/stop' -d '{}' -POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/59df4b41-6a23-4c99-9370-600b3a2cff23/stop HTTP/1.1 +POST /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/bc34aac0-6c5e-4b91-b2ea-47b76d4b26f1/stop HTTP/1.1 {} HTTP/1.1 204 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive CONTENT-LENGTH: 0 DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 X-ROUTE: /v1/projects/{project_id}/vpcs/vms/{vm_id}/stop diff --git a/docs/api/examples/post_qemuimg.txt b/docs/api/examples/post_qemuimg.txt new file mode 100644 index 00000000..8fe3d69f --- /dev/null +++ b/docs/api/examples/post_qemuimg.txt @@ -0,0 +1,23 @@ +curl -i -X POST 'http://localhost:8000/v1/qemu/img' -d '{"cluster_size": 64, "format": "qcow2", "lazy_refcounts": "off", "path": "/tmp/hda.qcow2", "preallocation": "metadata", "qemu_img": "/tmp/qemu-img", "refcount_bits": 12, "size": 100}' + +POST /v1/qemu/img HTTP/1.1 +{ + "cluster_size": 64, + "format": "qcow2", + "lazy_refcounts": "off", + "path": "/tmp/hda.qcow2", + "preallocation": "metadata", + "qemu_img": "/tmp/qemu-img", + "refcount_bits": 12, + "size": 100 +} + + +HTTP/1.1 201 +ACCESS-CONTROL-ALLOW-ORIGIN: * +CONNECTION: keep-alive +CONTENT-LENGTH: 0 +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.5 GNS3/1.4.0dev13 +X-ROUTE: /v1/qemu/img + diff --git a/docs/api/examples/post_version.txt b/docs/api/examples/post_version.txt index 1bd3ea12..37a197b0 100644 --- a/docs/api/examples/post_version.txt +++ b/docs/api/examples/post_version.txt @@ -1,19 +1,20 @@ -curl -i -X POST 'http://localhost:8000/v1/version' -d '{"version": "1.3.1.rc2"}' +curl -i -X POST 'http://localhost:8000/v1/version' -d '{"version": "1.4.0dev13"}' POST /v1/version HTTP/1.1 { - "version": "1.3.1.rc2" + "version": "1.4.0dev13" } HTTP/1.1 200 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive -CONTENT-LENGTH: 30 +CONTENT-LENGTH: 31 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 X-ROUTE: /v1/version { - "version": "1.3.1.rc2" + "version": "1.4.0dev13" } diff --git a/docs/api/examples/put_projectsprojectid.txt b/docs/api/examples/put_projectsprojectid.txt index 54cf8429..f524c22d 100644 --- a/docs/api/examples/put_projectsprojectid.txt +++ b/docs/api/examples/put_projectsprojectid.txt @@ -1,18 +1,19 @@ -curl -i -X PUT 'http://localhost:8000/v1/projects/b32aab45-411c-4171-9f20-357eaa00d54c' -d '{"name": "second_name", "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-4450/test_update_path_project_non_l0"}' +curl -i -X PUT 'http://localhost:8000/v1/projects/9d7dccbb-0fc0-40d0-9420-912733dcbf74' -d '{"name": "second_name", "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-of-noplay/pytest-110/test_update_path_project_non_l0"}' -PUT /v1/projects/b32aab45-411c-4171-9f20-357eaa00d54c HTTP/1.1 +PUT /v1/projects/9d7dccbb-0fc0-40d0-9420-912733dcbf74 HTTP/1.1 { "name": "second_name", - "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-4450/test_update_path_project_non_l0" + "path": "/private/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/pytest-of-noplay/pytest-110/test_update_path_project_non_l0" } HTTP/1.1 403 +ACCESS-CONTROL-ALLOW-ORIGIN: * 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.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 X-ROUTE: /v1/projects/{project_id} { diff --git a/docs/api/examples/put_projectsprojectidiouvmsvmid.txt b/docs/api/examples/put_projectsprojectidiouvmsvmid.txt index 80cd3523..55248a93 100644 --- a/docs/api/examples/put_projectsprojectidiouvmsvmid.txt +++ b/docs/api/examples/put_projectsprojectidiouvmsvmid.txt @@ -1,40 +1,44 @@ -curl -i -X PUT 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/3812eced-e777-4f35-8c7e-e6736e34fcfd' -d '{"console": 2001, "ethernet_adapters": 4, "initial_config_content": "hostname test", "iourc_content": "test", "l1_keepalives": true, "name": "test", "nvram": 2048, "ram": 512, "serial_adapters": 0, "use_default_iou_values": true}' +curl -i -X PUT 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/6438493a-152d-4cb0-b8d2-fcfe1f6bf540' -d '{"console": 2002, "ethernet_adapters": 4, "iourc_content": "test", "l1_keepalives": true, "name": "test", "nvram": 2048, "ram": 512, "serial_adapters": 0, "startup_config_content": "hostname test", "use_default_iou_values": true}' -PUT /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/3812eced-e777-4f35-8c7e-e6736e34fcfd HTTP/1.1 +PUT /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/iou/vms/6438493a-152d-4cb0-b8d2-fcfe1f6bf540 HTTP/1.1 { - "console": 2001, + "console": 2002, "ethernet_adapters": 4, - "initial_config_content": "hostname test", "iourc_content": "test", "l1_keepalives": true, "name": "test", "nvram": 2048, "ram": 512, "serial_adapters": 0, + "startup_config_content": "hostname test", "use_default_iou_values": true } HTTP/1.1 200 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive -CONTENT-LENGTH: 461 +CONTENT-LENGTH: 685 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 X-ROUTE: /v1/projects/{project_id}/iou/vms/{vm_id} { - "console": 2001, + "console": 2002, "ethernet_adapters": 4, - "initial_config": "initial-config.cfg", - "iourc_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpws7fdl5e/iourc", + "iourc_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpyrljan4a/iourc", "l1_keepalives": true, + "md5sum": null, "name": "test", "nvram": 2048, "path": "iou.bin", + "private_config": null, "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "ram": 512, "serial_adapters": 0, + "startup_config": "startup-config.cfg", "use_default_iou_values": true, - "vm_id": "3812eced-e777-4f35-8c7e-e6736e34fcfd" + "vm_directory": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmp2yt9sd1l/a1e920ca-338a-4e9f-b363-aa607b09dd80/project-files/iou/6438493a-152d-4cb0-b8d2-fcfe1f6bf540", + "vm_id": "6438493a-152d-4cb0-b8d2-fcfe1f6bf540" } diff --git a/docs/api/examples/put_projectsprojectidqemuvmsvmid.txt b/docs/api/examples/put_projectsprojectidqemuvmsvmid.txt index 288411b1..52872ddf 100644 --- a/docs/api/examples/put_projectsprojectidqemuvmsvmid.txt +++ b/docs/api/examples/put_projectsprojectidqemuvmsvmid.txt @@ -1,40 +1,61 @@ -curl -i -X PUT 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/e5b90e65-6f6a-4d44-b2d8-3a5c3a4a626b' -d '{"console": 2001, "hdb_disk_image": "/tmp/hdb", "name": "test", "ram": 1024}' +curl -i -X PUT 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/578b6391-e461-4f17-86f6-dd556d2f4dde' -d '{"console": 2002, "hdb_disk_image": "linux.img", "name": "test", "ram": 1024}' -PUT /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/e5b90e65-6f6a-4d44-b2d8-3a5c3a4a626b HTTP/1.1 +PUT /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/qemu/vms/578b6391-e461-4f17-86f6-dd556d2f4dde HTTP/1.1 { - "console": 2001, - "hdb_disk_image": "/tmp/hdb", + "console": 2002, + "hdb_disk_image": "linux.img", "name": "test", "ram": 1024 } HTTP/1.1 200 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive -CONTENT-LENGTH: 601 +CONTENT-LENGTH: 1358 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 X-ROUTE: /v1/projects/{project_id}/qemu/vms/{vm_id} { + "acpi_shutdown": false, "adapter_type": "e1000", "adapters": 1, - "console": 2001, + "boot_priority": "c", + "cdrom_image": "", + "cdrom_image_md5sum": null, + "console": 2002, + "console_type": "telnet", "cpu_throttling": 0, + "cpus": 1, "hda_disk_image": "", - "hdb_disk_image": "/tmp/hdb", + "hda_disk_image_md5sum": null, + "hda_disk_interface": "ide", + "hdb_disk_image": "linux.img", + "hdb_disk_image_md5sum": null, + "hdb_disk_interface": "ide", "hdc_disk_image": "", + "hdc_disk_image_md5sum": null, + "hdc_disk_interface": "ide", "hdd_disk_image": "", + "hdd_disk_image_md5sum": null, + "hdd_disk_interface": "ide", "initrd": "", + "initrd_md5sum": null, "kernel_command_line": "", "kernel_image": "", + "kernel_image_md5sum": null, "legacy_networking": false, + "mac_address": "00:00:ab:4d:de:00", "name": "test", "options": "", + "platform": "x86_64", "process_priority": "low", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", - "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpyasp9636/qemu_x42", + "qemu_path": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmph3pfurip/qemu-system-x86_64", "ram": 1024, - "vm_id": "e5b90e65-6f6a-4d44-b2d8-3a5c3a4a626b" + "usage": "", + "vm_directory": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpqqnauky9/a1e920ca-338a-4e9f-b363-aa607b09dd80/project-files/qemu/578b6391-e461-4f17-86f6-dd556d2f4dde", + "vm_id": "578b6391-e461-4f17-86f6-dd556d2f4dde" } diff --git a/docs/api/examples/put_projectsprojectidvirtualboxvmsvmid.txt b/docs/api/examples/put_projectsprojectidvirtualboxvmsvmid.txt index 68ec4c22..b7a858c5 100644 --- a/docs/api/examples/put_projectsprojectidvirtualboxvmsvmid.txt +++ b/docs/api/examples/put_projectsprojectidvirtualboxvmsvmid.txt @@ -1,30 +1,33 @@ -curl -i -X PUT 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/514963f5-93f8-4d18-bcd6-7d50ef7164a6' -d '{"console": 2010, "name": "test"}' +curl -i -X PUT 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/aa45d68c-c68e-4296-9550-641b522a73c2' -d '{"console": 2011, "name": "test"}' -PUT /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/514963f5-93f8-4d18-bcd6-7d50ef7164a6 HTTP/1.1 +PUT /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/virtualbox/vms/aa45d68c-c68e-4296-9550-641b522a73c2 HTTP/1.1 { - "console": 2010, + "console": 2011, "name": "test" } HTTP/1.1 200 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive -CONTENT-LENGTH: 359 +CONTENT-LENGTH: 413 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 X-ROUTE: /v1/projects/{project_id}/virtualbox/vms/{vm_id} { + "acpi_shutdown": false, "adapter_type": "Intel PRO/1000 MT Desktop (82540EM)", "adapters": 0, - "console": 2010, + "console": 2011, "enable_remote_console": false, "headless": false, "name": "test", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "ram": 0, "use_any_adapter": false, - "vm_id": "514963f5-93f8-4d18-bcd6-7d50ef7164a6", + "vm_directory": null, + "vm_id": "aa45d68c-c68e-4296-9550-641b522a73c2", "vmname": "VMTEST" } diff --git a/docs/api/examples/put_projectsprojectidvpcsvmsvmid.txt b/docs/api/examples/put_projectsprojectidvpcsvmsvmid.txt index 70c8462c..0c1c229e 100644 --- a/docs/api/examples/put_projectsprojectidvpcsvmsvmid.txt +++ b/docs/api/examples/put_projectsprojectidvpcsvmsvmid.txt @@ -1,26 +1,29 @@ -curl -i -X PUT 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/4905e649-6e81-446d-a60b-fd8b058a85e8' -d '{"console": 2011, "name": "test", "startup_script": "ip 192.168.1.1"}' +curl -i -X PUT 'http://localhost:8000/v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/608a4cd0-476c-40ba-95db-0c81effc8e9c' -d '{"console": 2012, "name": "test", "startup_script": "ip 192.168.1.1"}' -PUT /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/4905e649-6e81-446d-a60b-fd8b058a85e8 HTTP/1.1 +PUT /v1/projects/a1e920ca-338a-4e9f-b363-aa607b09dd80/vpcs/vms/608a4cd0-476c-40ba-95db-0c81effc8e9c HTTP/1.1 { - "console": 2011, + "console": 2012, "name": "test", "startup_script": "ip 192.168.1.1" } HTTP/1.1 200 +ACCESS-CONTROL-ALLOW-ORIGIN: * CONNECTION: keep-alive -CONTENT-LENGTH: 236 +CONTENT-LENGTH: 438 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT -SERVER: Python/3.4 GNS3/1.3.1.rc2 +SERVER: Python/3.5 GNS3/1.4.0dev13 X-ROUTE: /v1/projects/{project_id}/vpcs/vms/{vm_id} { - "console": 2011, + "console": 2012, "name": "test", "project_id": "a1e920ca-338a-4e9f-b363-aa607b09dd80", "startup_script": "ip 192.168.1.1", "startup_script_path": "startup.vpc", - "vm_id": "4905e649-6e81-446d-a60b-fd8b058a85e8" + "status": "stopped", + "vm_directory": "/var/folders/3s/r2wbv07n7wg4vrsn874lmxxh0000gn/T/tmpqqnauky9/a1e920ca-338a-4e9f-b363-aa607b09dd80/project-files/vpcs/608a4cd0-476c-40ba-95db-0c81effc8e9c", + "vm_id": "608a4cd0-476c-40ba-95db-0c81effc8e9c" } diff --git a/docs/api/v1/docker.rst b/docs/api/v1/docker.rst new file mode 100644 index 00000000..2e700cc8 --- /dev/null +++ b/docs/api/v1/docker.rst @@ -0,0 +1,8 @@ +Docker +--------------------- + +.. toctree:: + :glob: + :maxdepth: 2 + + docker/* diff --git a/docs/api/v1/docker/dockerimages.rst b/docs/api/v1/docker/dockerimages.rst new file mode 100644 index 00000000..d282af92 --- /dev/null +++ b/docs/api/v1/docker/dockerimages.rst @@ -0,0 +1,13 @@ +/v1/docker/images +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +GET /v1/docker/images +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Get all available Docker images + +Response status codes +********************** +- **200**: Success + diff --git a/docs/api/v1/docker/projectsprojectiddockerimages.rst b/docs/api/v1/docker/projectsprojectiddockerimages.rst new file mode 100644 index 00000000..01f41cc4 --- /dev/null +++ b/docs/api/v1/docker/projectsprojectiddockerimages.rst @@ -0,0 +1,49 @@ +/v1/projects/{project_id}/docker/images +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/docker/images +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Create a new Docker container + +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 Docker adapter type
adapters integer number of adapters
console string console name
imagename string Docker image name
name string Docker container name
startcmd string Docker CMD entry
vm_id Docker VM instance identifier
+ +Output +******* +.. raw:: html + + + + + + + + + + +
Name Mandatory Type Description
adapter_type string Docker adapter type
adapters integer number of adapters
cid string Docker container ID
image string Docker image name
name string Docker container name
project_id string Project UUID
vm_id string Docker container instance UUID
+ diff --git a/docs/api/v1/docker/projectsprojectiddockerimagesid.rst b/docs/api/v1/docker/projectsprojectiddockerimagesid.rst new file mode 100644 index 00000000..6a9e006d --- /dev/null +++ b/docs/api/v1/docker/projectsprojectiddockerimagesid.rst @@ -0,0 +1,20 @@ +/v1/projects/{project_id}/docker/images/{id} +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +DELETE /v1/projects/**{project_id}**/docker/images/**{id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Delete a Docker container + +Parameters +********** +- **id**: ID for the container +- **project_id**: UUID for the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance deleted + diff --git a/docs/api/v1/docker/projectsprojectiddockerimagesidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/v1/docker/projectsprojectiddockerimagesidadaptersadapternumberdportsportnumberdnio.rst new file mode 100644 index 00000000..e5724d50 --- /dev/null +++ b/docs/api/v1/docker/projectsprojectiddockerimagesidadaptersadapternumberdportsportnumberdnio.rst @@ -0,0 +1,40 @@ +/v1/projects/{project_id}/docker/images/{id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/docker/images/**{id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Add a NIO to a Docker container + +Parameters +********** +- **id**: ID of the container +- **adapter_number**: Adapter where the nio should be added +- **project_id**: UUID for the project +- **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}**/docker/images/**{id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Remove a NIO from a Docker container + +Parameters +********** +- **id**: ID of the container +- **adapter_number**: Adapter where the nio should be added +- **project_id**: UUID for the project +- **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/docker/projectsprojectiddockerimagesidreload.rst b/docs/api/v1/docker/projectsprojectiddockerimagesidreload.rst new file mode 100644 index 00000000..df8104d0 --- /dev/null +++ b/docs/api/v1/docker/projectsprojectiddockerimagesidreload.rst @@ -0,0 +1,50 @@ +/v1/projects/{project_id}/docker/images/{id}/reload +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/docker/images/**{id}**/reload +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Restart a Docker container + +Parameters +********** +- **id**: ID of the container +- **project_id**: UUID of the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance restarted + +Input +******* +.. raw:: html + + + + + + + + + + +
Name Mandatory Type Description
adapter_type string Docker adapter type
adapters integer number of adapters
console string console name
imagename string Docker image name
name string Docker container name
startcmd string Docker CMD entry
vm_id Docker VM instance identifier
+ +Output +******* +.. raw:: html + + + + + + + + + + +
Name Mandatory Type Description
adapter_type string Docker adapter type
adapters integer number of adapters
cid string Docker container ID
image string Docker image name
name string Docker container name
project_id string Project UUID
vm_id string Docker container instance UUID
+ diff --git a/docs/api/v1/docker/projectsprojectiddockerimagesidstart.rst b/docs/api/v1/docker/projectsprojectiddockerimagesidstart.rst new file mode 100644 index 00000000..49fb5e28 --- /dev/null +++ b/docs/api/v1/docker/projectsprojectiddockerimagesidstart.rst @@ -0,0 +1,50 @@ +/v1/projects/{project_id}/docker/images/{id}/start +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/docker/images/**{id}**/start +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Start a Docker container + +Parameters +********** +- **id**: ID of the container +- **project_id**: UUID of the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance started + +Input +******* +.. raw:: html + + + + + + + + + + +
Name Mandatory Type Description
adapter_type string Docker adapter type
adapters integer number of adapters
console string console name
imagename string Docker image name
name string Docker container name
startcmd string Docker CMD entry
vm_id Docker VM instance identifier
+ +Output +******* +.. raw:: html + + + + + + + + + + +
Name Mandatory Type Description
adapter_type string Docker adapter type
adapters integer number of adapters
cid string Docker container ID
image string Docker image name
name string Docker container name
project_id string Project UUID
vm_id string Docker container instance UUID
+ diff --git a/docs/api/v1/docker/projectsprojectiddockerimagesidstop.rst b/docs/api/v1/docker/projectsprojectiddockerimagesidstop.rst new file mode 100644 index 00000000..797eb66c --- /dev/null +++ b/docs/api/v1/docker/projectsprojectiddockerimagesidstop.rst @@ -0,0 +1,50 @@ +/v1/projects/{project_id}/docker/images/{id}/stop +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/docker/images/**{id}**/stop +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Stop a Docker container + +Parameters +********** +- **id**: ID of the container +- **project_id**: UUID of the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance stopped + +Input +******* +.. raw:: html + + + + + + + + + + +
Name Mandatory Type Description
adapter_type string Docker adapter type
adapters integer number of adapters
console string console name
imagename string Docker image name
name string Docker container name
startcmd string Docker CMD entry
vm_id Docker VM instance identifier
+ +Output +******* +.. raw:: html + + + + + + + + + + +
Name Mandatory Type Description
adapter_type string Docker adapter type
adapters integer number of adapters
cid string Docker container ID
image string Docker image name
name string Docker container name
project_id string Project UUID
vm_id string Docker container instance UUID
+ diff --git a/docs/api/v1/docker/projectsprojectiddockerimagesidsuspend.rst b/docs/api/v1/docker/projectsprojectiddockerimagesidsuspend.rst new file mode 100644 index 00000000..11297147 --- /dev/null +++ b/docs/api/v1/docker/projectsprojectiddockerimagesidsuspend.rst @@ -0,0 +1,50 @@ +/v1/projects/{project_id}/docker/images/{id}/suspend +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/docker/images/**{id}**/suspend +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Pause a Docker container + +Parameters +********** +- **id**: ID of the container +- **project_id**: UUID of the project + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Instance paused + +Input +******* +.. raw:: html + + + + + + + + + + +
Name Mandatory Type Description
adapter_type string Docker adapter type
adapters integer number of adapters
console string console name
imagename string Docker image name
name string Docker container name
startcmd string Docker CMD entry
vm_id Docker VM instance identifier
+ +Output +******* +.. raw:: html + + + + + + + + + + +
Name Mandatory Type Description
adapter_type string Docker adapter type
adapters integer number of adapters
cid string Docker container ID
image string Docker image name
name string Docker container name
project_id string Project UUID
vm_id string Docker container instance UUID
+ diff --git a/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceid.rst b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceid.rst index 7f0e65e4..2ccc6822 100644 --- a/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceid.rst +++ b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceid.rst @@ -9,8 +9,8 @@ Get a Dynamips device instance Parameters ********** -- **project_id**: UUID for the project - **device_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** @@ -38,8 +38,8 @@ Update a Dynamips device instance Parameters ********** -- **project_id**: UUID for the project - **device_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** @@ -60,6 +60,7 @@ Ethernet switch port + @@ -95,8 +96,8 @@ Delete a Dynamips device instance Parameters ********** -- **project_id**: UUID for the project - **device_id**: UUID for the instance +- **project_id**: UUID for the project Response status codes ********************** diff --git a/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst index 0533ad08..4bbd4479 100644 --- a/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst +++ b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdnio.rst @@ -9,9 +9,9 @@ Add a NIO to a Dynamips device instance Parameters ********** +- **device_id**: UUID for the instance - **project_id**: UUID for the project - **port_number**: Port on the device -- **device_id**: UUID for the instance Response status codes ********************** @@ -47,6 +47,17 @@ Linux Ethernet Network Input/Output
Name Mandatory Type Description
ethertype enum Possible values: , 0x8100, 0x88A8, 0x9100, 0x9200
port integer Port number
type enum Possible values: access, dot1q, qinq
vlan integer VLAN number
type enum Possible values: nio_linux_ethernet
+NAT +^^^^^^^^^^^^^^^^^^^^^^ +NAT Network Input/Output + +.. raw:: html + + + + +
Name Mandatory Type Description
type enum Possible values: nio_nat
+ NULL ^^^^^^^^^^^^^^^^^^^^^^ NULL Network Input/Output @@ -117,7 +128,7 @@ Body - +
Name Mandatory Type Description
mappings object
nio UDP, Ethernet, LinuxEthernet, TAP, UNIX, VDE, NULL
nio UDP, Ethernet, LinuxEthernet, NAT, TAP, UNIX, VDE, NULL
port_settings object Ethernet switch
@@ -128,9 +139,9 @@ Remove a NIO from a Dynamips device instance Parameters ********** +- **device_id**: UUID for the instance - **project_id**: UUID for the project - **port_number**: Port on the device -- **device_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst index 117cd928..fc6abef3 100644 --- a/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst +++ b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdstartcapture.rst @@ -9,9 +9,9 @@ Start a packet capture on a Dynamips device instance Parameters ********** +- **device_id**: UUID for the instance - **project_id**: UUID for the project - **port_number**: Port on the device -- **device_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst index 9674ef65..716c458d 100644 --- a/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst +++ b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceidportsportnumberdstopcapture.rst @@ -9,9 +9,9 @@ Stop a packet capture on a Dynamips device instance Parameters ********** +- **device_id**: UUID for the instance - **project_id**: UUID for the project - **port_number**: Port on the device -- **device_id**: UUID for the instance Response status codes ********************** diff --git a/docs/api/v1/dynamips_vm/dynamipsvms.rst b/docs/api/v1/dynamips_vm/dynamipsvms.rst new file mode 100644 index 00000000..476814cd --- /dev/null +++ b/docs/api/v1/dynamips_vm/dynamipsvms.rst @@ -0,0 +1,13 @@ +/v1/dynamips/vms +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +GET /v1/dynamips/vms +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Retrieve the list of Dynamips VMS + +Response status codes +********************** +- **200**: List of Dynamips VM retrieved + diff --git a/docs/api/v1/dynamips_vm/dynamipsvmspath.rst b/docs/api/v1/dynamips_vm/dynamipsvmspath.rst new file mode 100644 index 00000000..375c93c6 --- /dev/null +++ b/docs/api/v1/dynamips_vm/dynamipsvmspath.rst @@ -0,0 +1,13 @@ +/v1/dynamips/vms/{path} +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/dynamips/vms/**{path}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Upload Dynamips image. + +Response status codes +********************** +- **204**: Image uploaded + diff --git a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvms.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvms.rst index a8b15b28..cd11089f 100644 --- a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvms.rst +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvms.rst @@ -23,6 +23,7 @@ Input + @@ -35,6 +36,7 @@ Input + @@ -73,6 +75,7 @@ Output
Name Mandatory Type Description
auto_delete_disks boolean automatically delete nvram and disk files
aux integer auxiliary console TCP port
chassis string router chassis model
clock_divisor integer clock divisor
idlepc string Idle-PC value
idlesleep integer idlesleep value
image string path to the IOS image
image_md5sum ['string', 'null'] checksum of the IOS image
iomem integer I/O memory percentage
mac_addr string base MAC address
midplane enum Possible values: std, vxr
+ @@ -85,6 +88,7 @@ Output + @@ -110,6 +114,7 @@ Output + diff --git a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmid.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmid.rst index 0ec4f747..38bc4e01 100644 --- a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmid.rst +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmid.rst @@ -24,6 +24,7 @@ Output
Name Mandatory Type Description
auto_delete_disks boolean automatically delete nvram and disk files
aux ['integer', 'null'] auxiliary console TCP port
chassis string router chassis model
clock_divisor integer clock divisor
idlepc string Idle-PC value
idlesleep integer idlesleep value
image string path to the IOS image
image_md5sum ['string', 'null'] checksum of the IOS image
iomem integer I/O memory percentage
mac_addr string base MAC address
midplane enum Possible values: std, vxr
startup_config string path to the IOS startup configuration file
startup_config_base64 string startup configuration base64 encoded
system_id string system ID
vm_directory string
vm_id string Dynamips router instance UUID
wic0 Network module WIC slot 0
wic1 Network module WIC slot 0
+ @@ -36,6 +37,7 @@ Output + @@ -61,6 +63,7 @@ Output + @@ -90,6 +93,7 @@ Input
Name Mandatory Type Description
auto_delete_disks boolean automatically delete nvram and disk files
aux ['integer', 'null'] auxiliary console TCP port
chassis string router chassis model
clock_divisor integer clock divisor
idlepc string Idle-PC value
idlesleep integer idlesleep value
image string path to the IOS image
image_md5sum ['string', 'null'] checksum of the IOS image
iomem integer I/O memory percentage
mac_addr string base MAC address
midplane enum Possible values: std, vxr
startup_config string path to the IOS startup configuration file
startup_config_base64 string startup configuration base64 encoded
system_id string system ID
vm_directory string
vm_id string Dynamips router instance UUID
wic0 Network module WIC slot 0
wic1 Network module WIC slot 0
+ @@ -101,6 +105,7 @@ Input + @@ -110,7 +115,6 @@ Input - @@ -123,7 +127,6 @@ Input - @@ -138,6 +141,7 @@ Output
Name Mandatory Type Description
auto_delete_disks boolean automatically delete nvram and disk files
aux integer auxiliary console TCP port
chassis string router chassis model
clock_divisor integer clock divisor
idlepc string Idle-PC value
idlesleep integer idlesleep value
image string path to the IOS image
image_md5sum ['string', 'null'] checksum of the IOS image
iomem integer I/O memory percentage
mac_addr string base MAC address
midplane enum Possible values: std, vxr
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
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
+ @@ -150,6 +154,7 @@ Output + @@ -175,6 +180,7 @@ Output + diff --git a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst index c1c114ec..b7a216fd 100644 --- a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -9,9 +9,9 @@ Add a NIO to a Dynamips VM instance Parameters ********** -- **project_id**: UUID for the project - **adapter_number**: Adapter where the nio should be added - **port_number**: Port on the adapter +- **project_id**: UUID for the project - **vm_id**: UUID for the instance Response status codes @@ -27,9 +27,9 @@ Remove a NIO from a Dynamips VM instance Parameters ********** -- **project_id**: UUID for the project - **adapter_number**: Adapter from where the nio should be removed - **port_number**: Port on the adapter +- **project_id**: UUID for the project - **vm_id**: UUID for the instance Response status codes diff --git a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst index 57271aac..cfebeb84 100644 --- a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst @@ -9,9 +9,9 @@ Start a packet capture on a Dynamips VM instance Parameters ********** -- **project_id**: UUID for the project - **adapter_number**: Adapter to start a packet capture - **port_number**: Port on the adapter +- **project_id**: UUID for the project - **vm_id**: UUID for the instance Response status codes diff --git a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst index c3b44232..960a0d6f 100644 --- a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst @@ -9,9 +9,9 @@ Stop a packet capture on a Dynamips VM instance Parameters ********** -- **project_id**: UUID for the project - **adapter_number**: Adapter to stop a packet capture - **port_number**: Port on the adapter (always 0) +- **project_id**: UUID for the project - **vm_id**: UUID for the instance Response status codes diff --git a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidconfigs.rst b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidconfigs.rst index 547b9e25..d8aae9a1 100644 --- a/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidconfigs.rst +++ b/docs/api/v1/dynamips_vm/projectsprojectiddynamipsvmsvmidconfigs.rst @@ -19,7 +19,7 @@ Output
Name Mandatory Type Description
auto_delete_disks boolean automatically delete nvram and disk files
aux ['integer', 'null'] auxiliary console TCP port
chassis string router chassis model
clock_divisor integer clock divisor
idlepc string Idle-PC value
idlesleep integer idlesleep value
image string path to the IOS image
image_md5sum ['string', 'null'] checksum of the IOS image
iomem integer I/O memory percentage
mac_addr string base MAC address
midplane enum Possible values: std, vxr
startup_config string path to the IOS startup configuration file
startup_config_base64 string startup configuration base64 encoded
system_id string system ID
vm_directory string
vm_id string Dynamips router instance UUID
wic0 Network module WIC slot 0
wic1 Network module WIC slot 0
- - + +
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
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/file.rst b/docs/api/v1/file.rst new file mode 100644 index 00000000..47b74965 --- /dev/null +++ b/docs/api/v1/file.rst @@ -0,0 +1,8 @@ +File +--------------------- + +.. toctree:: + :glob: + :maxdepth: 2 + + file/* diff --git a/docs/api/v1/file/filesstream.rst b/docs/api/v1/file/filesstream.rst new file mode 100644 index 00000000..7d61f7ce --- /dev/null +++ b/docs/api/v1/file/filesstream.rst @@ -0,0 +1,24 @@ +/v1/files/stream +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +GET /v1/files/stream +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Stream a file from the server + +Response status codes +********************** +- **200**: File retrieved +- **409**: Can't access to file +- **404**: File doesn't exist + +Input +******* +.. raw:: html + + + + +
Name Mandatory Type Description
location ['string'] File path
+ diff --git a/docs/api/v1/iou/iouvms.rst b/docs/api/v1/iou/iouvms.rst new file mode 100644 index 00000000..466ae34c --- /dev/null +++ b/docs/api/v1/iou/iouvms.rst @@ -0,0 +1,19 @@ +/v1/iou/vms +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +GET /v1/iou/vms +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Retrieve the list of IOU VMS + +Response status codes +********************** +- **200**: List of IOU VM retrieved + +Sample session +*************** + + +.. literalinclude:: ../../examples/get_iouvms.txt + diff --git a/docs/api/v1/iou/iouvmspath.rst b/docs/api/v1/iou/iouvmspath.rst new file mode 100644 index 00000000..da07a57c --- /dev/null +++ b/docs/api/v1/iou/iouvmspath.rst @@ -0,0 +1,13 @@ +/v1/iou/vms/{path} +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/iou/vms/**{path}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Upload IOU image. + +Response status codes +********************** +- **204**: Image uploaded + diff --git a/docs/api/v1/iou/projectsprojectidiouvms.rst b/docs/api/v1/iou/projectsprojectidiouvms.rst index 6387045e..95620207 100644 --- a/docs/api/v1/iou/projectsprojectidiouvms.rst +++ b/docs/api/v1/iou/projectsprojectidiouvms.rst @@ -25,14 +25,18 @@ Input Name Mandatory Type Description console ['integer', 'null'] console TCP port ethernet_adapters integer How many ethernet adapters are connected to the IOU - initial_config_content ['string', 'null'] Initial configuration of the IOU - iourc_content ['string', 'null'] Content of the iourc file, if a file exist on servers this variable is ignored. It's mostly for compatibility with < 1.3 releases + iourc_content ['string', 'null'] Content of the iourc file. Ignored if Null l1_keepalives ['boolean', 'null'] Always up ethernet interface + md5sum ['string', 'null'] Checksum of iou binary name ✔ string IOU VM name nvram ['integer', 'null'] Allocated NVRAM KB path ✔ string Path of iou binary + private_config ['string', 'null'] Path to the private-config of IOU + private_config_content ['string', 'null'] Private-config of IOU ram ['integer', 'null'] Allocated RAM MB serial_adapters integer How many serial adapters are connected to the IOU + startup_config ['string', 'null'] Path to the startup-config of IOU + startup_config_content ['string', 'null'] Startup-config of IOU use_default_iou_values ['boolean', 'null'] Use default IOU values vm_id IOU VM identifier @@ -45,16 +49,19 @@ Output Name Mandatory Type Description console ✔ integer console TCP port ethernet_adapters ✔ integer How many ethernet adapters are connected to the IOU - initial_config ✔ ['string', 'null'] Path of the initial config content relative to project directory iourc_path ['string', 'null'] Path of the iourc file used by remote servers l1_keepalives ✔ boolean Always up ethernet interface + md5sum ✔ ['string', 'null'] Checksum of iou binary name ✔ string IOU VM name nvram ✔ integer Allocated NVRAM KB path ✔ string Path of iou binary + private_config ✔ ['string', 'null'] Path of the private-config content relative to project directory project_id ✔ string Project UUID ram ✔ integer Allocated RAM MB serial_adapters ✔ integer How many serial adapters are connected to the IOU + startup_config ✔ ['string', 'null'] Path of the startup-config content relative to project directory use_default_iou_values ✔ ['boolean', 'null'] Use default IOU values + vm_directory string vm_id ✔ string IOU VM UUID diff --git a/docs/api/v1/iou/projectsprojectidiouvmsvmid.rst b/docs/api/v1/iou/projectsprojectidiouvmsvmid.rst index 87bba3cf..33b73ae4 100644 --- a/docs/api/v1/iou/projectsprojectidiouvmsvmid.rst +++ b/docs/api/v1/iou/projectsprojectidiouvmsvmid.rst @@ -26,16 +26,19 @@ Output Name Mandatory Type Description console ✔ integer console TCP port ethernet_adapters ✔ integer How many ethernet adapters are connected to the IOU - initial_config ✔ ['string', 'null'] Path of the initial config content relative to project directory iourc_path ['string', 'null'] Path of the iourc file used by remote servers l1_keepalives ✔ boolean Always up ethernet interface + md5sum ✔ ['string', 'null'] Checksum of iou binary name ✔ string IOU VM name nvram ✔ integer Allocated NVRAM KB path ✔ string Path of iou binary + private_config ✔ ['string', 'null'] Path of the private-config content relative to project directory project_id ✔ string Project UUID ram ✔ integer Allocated RAM MB serial_adapters ✔ integer How many serial adapters are connected to the IOU + startup_config ✔ ['string', 'null'] Path of the startup-config content relative to project directory use_default_iou_values ✔ ['boolean', 'null'] Use default IOU values + vm_directory string vm_id ✔ string IOU VM UUID @@ -70,14 +73,16 @@ Input Name Mandatory Type Description console ['integer', 'null'] console TCP port ethernet_adapters ['integer', 'null'] How many ethernet adapters are connected to the IOU - initial_config_content ['string', 'null'] Initial configuration of the IOU - iourc_content ['string', 'null'] Content of the iourc file, if a file exist on servers this variable is ignored. It's mostly for compatibility with < 1.3 releases + iourc_content ['string', 'null'] Content of the iourc file. Ignored if Null l1_keepalives ['boolean', 'null'] Always up ethernet interface + md5sum ['string', 'null'] Checksum of iou binary name ['string', 'null'] IOU VM name nvram ['integer', 'null'] Allocated NVRAM KB path ['string', 'null'] Path of iou binary + private_config_content ['string', 'null'] Private-config of IOU ram ['integer', 'null'] Allocated RAM MB serial_adapters ['integer', 'null'] How many serial adapters are connected to the IOU + startup_config_content ['string', 'null'] Startup-config of IOU use_default_iou_values ['boolean', 'null'] Use default IOU values @@ -89,16 +94,19 @@ Output Name Mandatory Type Description console ✔ integer console TCP port ethernet_adapters ✔ integer How many ethernet adapters are connected to the IOU - initial_config ✔ ['string', 'null'] Path of the initial config content relative to project directory iourc_path ['string', 'null'] Path of the iourc file used by remote servers l1_keepalives ✔ boolean Always up ethernet interface + md5sum ✔ ['string', 'null'] Checksum of iou binary name ✔ string IOU VM name nvram ✔ integer Allocated NVRAM KB path ✔ string Path of iou binary + private_config ✔ ['string', 'null'] Path of the private-config content relative to project directory project_id ✔ string Project UUID ram ✔ integer Allocated RAM MB serial_adapters ✔ integer How many serial adapters are connected to the IOU + startup_config ✔ ['string', 'null'] Path of the startup-config content relative to project directory use_default_iou_values ✔ ['boolean', 'null'] Use default IOU values + vm_directory string vm_id ✔ string IOU VM UUID diff --git a/docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst index 748ed1e2..d7411d32 100644 --- a/docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -9,9 +9,9 @@ Add a NIO to a IOU instance Parameters ********** -- **project_id**: UUID for the project - **adapter_number**: Network adapter where the nio is located - **port_number**: Port where the nio should be added +- **project_id**: UUID for the project - **vm_id**: UUID for the instance Response status codes @@ -33,9 +33,9 @@ Remove a NIO from a IOU instance Parameters ********** -- **project_id**: UUID for the project - **adapter_number**: Network adapter where the nio is located - **port_number**: Port from where the nio should be removed +- **project_id**: UUID for the project - **vm_id**: UUID for the instance Response status codes diff --git a/docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst b/docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst index cc7a521d..e94abb49 100644 --- a/docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst +++ b/docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst @@ -9,9 +9,9 @@ Start a packet capture on a IOU VM instance Parameters ********** -- **project_id**: UUID for the project - **adapter_number**: Adapter to start a packet capture - **port_number**: Port on the adapter +- **project_id**: UUID for the project - **vm_id**: UUID for the instance Response status codes diff --git a/docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst b/docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst index fb66ba89..13b38095 100644 --- a/docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst +++ b/docs/api/v1/iou/projectsprojectidiouvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst @@ -9,9 +9,9 @@ Stop a packet capture on a IOU VM instance Parameters ********** -- **project_id**: UUID for the project - **adapter_number**: Adapter to stop a packet capture - **port_number**: Port on the adapter (always 0) +- **project_id**: UUID for the project - **vm_id**: UUID for the instance Response status codes diff --git a/docs/api/v1/iou/projectsprojectidiouvmsvmidinitialconfig.rst b/docs/api/v1/iou/projectsprojectidiouvmsvmidconfigs.rst similarity index 52% rename from docs/api/v1/iou/projectsprojectidiouvmsvmidinitialconfig.rst rename to docs/api/v1/iou/projectsprojectidiouvmsvmidconfigs.rst index 6159e783..f9955b7e 100644 --- a/docs/api/v1/iou/projectsprojectidiouvmsvmidinitialconfig.rst +++ b/docs/api/v1/iou/projectsprojectidiouvmsvmidconfigs.rst @@ -1,15 +1,15 @@ -/v1/projects/{project_id}/iou/vms/{vm_id}/initial_config +/v1/projects/{project_id}/iou/vms/{vm_id}/configs ---------------------------------------------------------------------------------------------------------------------- .. contents:: -GET /v1/projects/**{project_id}**/iou/vms/**{vm_id}**/initial_config +GET /v1/projects/**{project_id}**/iou/vms/**{vm_id}**/configs ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Retrieve the initial config content +Retrieve the startup and private configs content Response status codes ********************** -- **200**: Initial config retrieved +- **200**: Configs retrieved - **400**: Invalid request - **404**: Instance doesn't exist @@ -19,12 +19,13 @@ Output - + +
Name Mandatory Type Description
content ['string', 'null'] Content of the initial configuration file
private_config_content ['string', 'null'] Content of the private configuration file
startup_config_content ['string', 'null'] Content of the startup configuration file
Sample session *************** -.. literalinclude:: ../../examples/get_projectsprojectidiouvmsvmidinitialconfig.txt +.. literalinclude:: ../../examples/get_projectsprojectidiouvmsvmidconfigs.txt diff --git a/docs/api/v1/iou/projectsprojectidiouvmsvmidconfigssave.rst b/docs/api/v1/iou/projectsprojectidiouvmsvmidconfigssave.rst new file mode 100644 index 00000000..b212fc51 --- /dev/null +++ b/docs/api/v1/iou/projectsprojectidiouvmsvmidconfigssave.rst @@ -0,0 +1,15 @@ +/v1/projects/{project_id}/iou/vms/{vm_id}/configs/save +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/iou/vms/**{vm_id}**/configs/save +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Save the startup and private configs content + +Response status codes +********************** +- **200**: Configs saved +- **400**: Invalid request +- **404**: Instance doesn't exist + diff --git a/docs/api/v1/iou/projectsprojectidiouvmsvmidstart.rst b/docs/api/v1/iou/projectsprojectidiouvmsvmidstart.rst index 25a9e236..14124ac2 100644 --- a/docs/api/v1/iou/projectsprojectidiouvmsvmidstart.rst +++ b/docs/api/v1/iou/projectsprojectidiouvmsvmidstart.rst @@ -18,6 +18,15 @@ Response status codes - **404**: Instance doesn't exist - **204**: Instance started +Input +******* +.. raw:: html + + + + +
Name Mandatory Type Description
iourc_content ['string', 'null'] Content of the iourc file. Ignored if Null
+ Sample session *************** diff --git a/docs/api/v1/project/projects.rst b/docs/api/v1/project/projects.rst index e9299195..13e87bd8 100644 --- a/docs/api/v1/project/projects.rst +++ b/docs/api/v1/project/projects.rst @@ -3,6 +3,21 @@ .. contents:: +GET /v1/projects +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +List projects opened on the server + +Response status codes +********************** +- **200**: Project list + +Sample session +*************** + + +.. literalinclude:: ../../examples/get_projects.txt + + POST /v1/projects ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Create a new project on the server @@ -31,7 +46,7 @@ Output - + diff --git a/docs/api/v1/project/projectsprojectid.rst b/docs/api/v1/project/projectsprojectid.rst index 0772e7d7..ce2e2c8a 100644 --- a/docs/api/v1/project/projectsprojectid.rst +++ b/docs/api/v1/project/projectsprojectid.rst @@ -23,7 +23,7 @@ Output
Name Mandatory Type Description
location string Base directory where the project should be created on remote server
name string Project name
name ['string', 'null'] Project name
path string Directory of the project on the server
project_id string Project UUID
temporary boolean If project is a temporary project
- + @@ -68,7 +68,7 @@ Output
Name Mandatory Type Description
location string Base directory where the project should be created on remote server
name string Project name
name ['string', 'null'] Project name
path string Directory of the project on the server
project_id string Project UUID
temporary boolean If project is a temporary project
- + diff --git a/docs/api/v1/project/projectsprojectidfiles.rst b/docs/api/v1/project/projectsprojectidfiles.rst new file mode 100644 index 00000000..2c877216 --- /dev/null +++ b/docs/api/v1/project/projectsprojectidfiles.rst @@ -0,0 +1,24 @@ +/v1/projects/{project_id}/files +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +GET /v1/projects/**{project_id}**/files +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +List files of a project + +Parameters +********** +- **project_id**: The UUID of the project + +Response status codes +********************** +- **200**: Return list of files +- **404**: The project doesn't exist + +Sample session +*************** + + +.. literalinclude:: ../../examples/get_projectsprojectidfiles.txt + diff --git a/docs/api/v1/project/projectsprojectidfilespath.rst b/docs/api/v1/project/projectsprojectidfilespath.rst new file mode 100644 index 00000000..4ce054ef --- /dev/null +++ b/docs/api/v1/project/projectsprojectidfilespath.rst @@ -0,0 +1,19 @@ +/v1/projects/{project_id}/files/{path:.+} +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +GET /v1/projects/**{project_id}**/files/**{path:.+}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Get a file of a project + +Parameters +********** +- **project_id**: The UUID of the project + +Response status codes +********************** +- **200**: Return the file +- **403**: Permission denied +- **404**: The file doesn't exist + diff --git a/docs/api/v1/project/projectsprojectidnotifications.rst b/docs/api/v1/project/projectsprojectidnotifications.rst new file mode 100644 index 00000000..7967deb0 --- /dev/null +++ b/docs/api/v1/project/projectsprojectidnotifications.rst @@ -0,0 +1,18 @@ +/v1/projects/{project_id}/notifications +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +GET /v1/projects/**{project_id}**/notifications +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Receive notifications about the projects + +Parameters +********** +- **project_id**: The UUID of the project + +Response status codes +********************** +- **200**: End of stream +- **404**: The project doesn't exist + diff --git a/docs/api/v1/qemu/projectsprojectidqemuvms.rst b/docs/api/v1/qemu/projectsprojectidqemuvms.rst index 6c3d8e65..3621e2ca 100644 --- a/docs/api/v1/qemu/projectsprojectidqemuvms.rst +++ b/docs/api/v1/qemu/projectsprojectidqemuvms.rst @@ -5,7 +5,7 @@ POST /v1/projects/**{project_id}**/qemu/vms ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Create a new Qemu.instance +Create a new Qemu VM instance Parameters ********** @@ -23,23 +23,43 @@ Input
Name Mandatory Type Description
location string Base directory where the project should be created on remote server
name string Project name
name ['string', 'null'] Project name
path string Directory of the project on the server
project_id string Project UUID
temporary boolean If project is a temporary project
+ + + + + - - - - - + + + + + + + + + + + + + + + - + + + + + - + +
Name Mandatory Type Description
acpi_shutdown ['boolean', 'null'] ACPI shutdown support
adapter_type ['string', 'null'] QEMU adapter type
adapters ['integer', 'null'] number of adapters
boot_priority enum Possible values: c, d, n, cn, cd
cdrom_image string QEMU cdrom image path
cdrom_image_md5sum ['string', 'null'] QEMU cdrom image checksum
console ['integer', 'null'] console TCP port
console_type enum Possible values: telnet, vnc
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
hdc_disk_image ['string', 'null'] QEMU hdc disk image path
hdd_disk_image ['string', 'null'] QEMU hdd disk image path
initrd ['string', 'null'] QEMU initrd path
cpus ['integer', 'null'] number of vCPUs
hda_disk_image string QEMU hda disk image path
hda_disk_image_md5sum ['string', 'null'] QEMU hda disk image checksum
hda_disk_interface string QEMU hda interface
hdb_disk_image string QEMU hdb disk image path
hdb_disk_image_md5sum ['string', 'null'] QEMU hdb disk image checksum
hdb_disk_interface string QEMU hdb interface
hdc_disk_image string QEMU hdc disk image path
hdc_disk_image_md5sum ['string', 'null'] QEMU hdc disk image checksum
hdc_disk_interface string QEMU hdc interface
hdd_disk_image string QEMU hdd disk image path
hdd_disk_image_md5sum ['string', 'null'] QEMU hdd disk image checksum
hdd_disk_interface string QEMU hdd interface
initrd string QEMU initrd path
initrd_md5sum ['string', 'null'] QEMU initrd path
kernel_command_line ['string', 'null'] QEMU kernel command line
kernel_image ['string', 'null'] QEMU kernel image path
kernel_image string QEMU kernel image path
kernel_image_md5sum ['string', 'null'] QEMU kernel image checksum
legacy_networking ['boolean', 'null'] Use QEMU legagy networking commands (-net syntax)
linked_clone boolean either the VM is a linked clone or not
mac_address ['string', 'null'] QEMU MAC address
name string QEMU VM instance name
options ['string', 'null'] Additional QEMU options
platform enum Possible values: aarch64, alpha, arm, cris, i386, lm32, m68k, microblaze, microblazeel, mips, mips64, mips64el, mipsel, moxie, or32, ppc, ppc64, ppcemb, s390x, sh4, sh4eb, sparc, sparc64, tricore, unicore32, x86_64, xtensa, xtensaeb, null
process_priority enum Possible values: realtime, very high, high, normal, low, very low, null
qemu_path string Path to QEMU
qemu_path ['string', 'null'] Path to QEMU
ram ['integer', 'null'] amount of RAM in MB
usage string How to use the qemu VM
vm_id QEMU VM identifier
@@ -49,24 +69,44 @@ Output + + + + + + + + + + + + + + + + + + + +
Name Mandatory Type Description
acpi_shutdown boolean ACPI shutdown support
adapter_type string QEMU adapter type
adapters integer number of adapters
boot_priority enum Possible values: c, d
cdrom_image string QEMU cdrom image path
cdrom_image_md5sum ['string', 'null'] QEMU cdrom image checksum
console integer console TCP port
console_type enum Possible values: telnet, vnc
cpu_throttling integer Percentage of CPU allowed for QEMU
cpus ['integer', 'null'] number of vCPUs
hda_disk_image string QEMU hda disk image path
hda_disk_image_md5sum ['string', 'null'] QEMU hda disk image checksum
hda_disk_interface string QEMU hda interface
hdb_disk_image string QEMU hdb disk image path
hdb_disk_image_md5sum ['string', 'null'] QEMU hdb disk image checksum
hdb_disk_interface string QEMU hdb interface
hdc_disk_image string QEMU hdc disk image path
hdc_disk_image_md5sum ['string', 'null'] QEMU hdc disk image checksum
hdc_disk_interface string QEMU hdc interface
hdd_disk_image string QEMU hdd disk image path
hdd_disk_image_md5sum ['string', 'null'] QEMU hdd disk image checksum
hdd_disk_interface string QEMU hdd interface
initrd string QEMU initrd path
initrd_md5sum ['string', 'null'] QEMU initrd path
kernel_command_line string QEMU kernel command line
kernel_image string QEMU kernel image path
kernel_image_md5sum ['string', 'null'] QEMU kernel image checksum
legacy_networking boolean Use QEMU legagy networking commands (-net syntax)
mac_address string QEMU MAC address
name string QEMU VM instance name
options string Additional QEMU options
platform enum Possible values: aarch64, alpha, arm, cris, i386, lm32, m68k, microblaze, microblazeel, mips, mips64, mips64el, mipsel, moxie, or32, ppc, ppc64, ppcemb, s390x, sh4, sh4eb, sparc, sparc64, tricore, unicore32, x86_64, xtensa, xtensaeb
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
usage string How to use the qemu VM
vm_directory string
vm_id string QEMU VM uuid
diff --git a/docs/api/v1/qemu/projectsprojectidqemuvmsvmid.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmid.rst index 396d5db1..caf301ed 100644 --- a/docs/api/v1/qemu/projectsprojectidqemuvmsvmid.rst +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmid.rst @@ -5,7 +5,7 @@ GET /v1/projects/**{project_id}**/qemu/vms/**{vm_id}** ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Get a Qemu.instance +Get a Qemu VM instance Parameters ********** @@ -24,24 +24,44 @@ Output + + + + + + + + + + + + + + + + + + + +
Name Mandatory Type Description
acpi_shutdown boolean ACPI shutdown support
adapter_type string QEMU adapter type
adapters integer number of adapters
boot_priority enum Possible values: c, d
cdrom_image string QEMU cdrom image path
cdrom_image_md5sum ['string', 'null'] QEMU cdrom image checksum
console integer console TCP port
console_type enum Possible values: telnet, vnc
cpu_throttling integer Percentage of CPU allowed for QEMU
cpus ['integer', 'null'] number of vCPUs
hda_disk_image string QEMU hda disk image path
hda_disk_image_md5sum ['string', 'null'] QEMU hda disk image checksum
hda_disk_interface string QEMU hda interface
hdb_disk_image string QEMU hdb disk image path
hdb_disk_image_md5sum ['string', 'null'] QEMU hdb disk image checksum
hdb_disk_interface string QEMU hdb interface
hdc_disk_image string QEMU hdc disk image path
hdc_disk_image_md5sum ['string', 'null'] QEMU hdc disk image checksum
hdc_disk_interface string QEMU hdc interface
hdd_disk_image string QEMU hdd disk image path
hdd_disk_image_md5sum ['string', 'null'] QEMU hdd disk image checksum
hdd_disk_interface string QEMU hdd interface
initrd string QEMU initrd path
initrd_md5sum ['string', 'null'] QEMU initrd path
kernel_command_line string QEMU kernel command line
kernel_image string QEMU kernel image path
kernel_image_md5sum ['string', 'null'] QEMU kernel image checksum
legacy_networking boolean Use QEMU legagy networking commands (-net syntax)
mac_address string QEMU MAC address
name string QEMU VM instance name
options string Additional QEMU options
platform enum Possible values: aarch64, alpha, arm, cris, i386, lm32, m68k, microblaze, microblazeel, mips, mips64, mips64el, mipsel, moxie, or32, ppc, ppc64, ppcemb, s390x, sh4, sh4eb, sparc, sparc64, tricore, unicore32, x86_64, xtensa, xtensaeb
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
usage string How to use the qemu VM
vm_directory string
vm_id string QEMU VM uuid
@@ -54,7 +74,7 @@ Sample session PUT /v1/projects/**{project_id}**/qemu/vms/**{vm_id}** ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Update a Qemu.instance +Update a Qemu VM instance Parameters ********** @@ -74,23 +94,42 @@ Input + + + + + - - - - - + + + + + + + + + + + + + + + - + + + + +
Name Mandatory Type Description
acpi_shutdown ['boolean', 'null'] ACPI shutdown support
adapter_type ['string', 'null'] QEMU adapter type
adapters ['integer', 'null'] number of adapters
boot_priority enum Possible values: c, d
cdrom_image string QEMU cdrom image path
cdrom_image_md5sum ['string', 'null'] QEMU cdrom image checksum
console ['integer', 'null'] console TCP port
console_type enum Possible values: telnet, vnc
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
hdc_disk_image ['string', 'null'] QEMU hdc disk image path
hdd_disk_image ['string', 'null'] QEMU hdd disk image path
initrd ['string', 'null'] QEMU initrd path
cpus ['integer', 'null'] number of vCPUs
hda_disk_image string QEMU hda disk image path
hda_disk_image_md5sum ['string', 'null'] QEMU hda disk image checksum
hda_disk_interface string QEMU hda interface
hdb_disk_image string QEMU hdb disk image path
hdb_disk_image_md5sum ['string', 'null'] QEMU hdb disk image checksum
hdb_disk_interface string QEMU hdb interface
hdc_disk_image string QEMU hdc disk image path
hdc_disk_image_md5sum ['string', 'null'] QEMU hdc disk image checksum
hdc_disk_interface string QEMU hdc interface
hdd_disk_image string QEMU hdd disk image path
hdd_disk_image_md5sum ['string', 'null'] QEMU hdd disk image checksum
hdd_disk_interface string QEMU hdd interface
initrd string QEMU initrd path
initrd_md5sum ['string', 'null'] QEMU initrd path
kernel_command_line ['string', 'null'] QEMU kernel command line
kernel_image ['string', 'null'] QEMU kernel image path
kernel_image string QEMU kernel image path
kernel_image_md5sum ['string', 'null'] QEMU kernel image checksum
legacy_networking ['boolean', 'null'] Use QEMU legagy networking commands (-net syntax)
mac_address ['string', 'null'] QEMU MAC address
name ['string', 'null'] QEMU VM instance name
options ['string', 'null'] Additional QEMU options
platform enum Possible values: aarch64, alpha, arm, cris, i386, lm32, m68k, microblaze, microblazeel, mips, mips64, mips64el, mipsel, moxie, or32, ppc, ppc64, ppcemb, s390x, sh4, sh4eb, sparc, sparc64, tricore, unicore32, x86_64, xtensa, xtensaeb, null
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
usage string How to use the qemu VM
Output @@ -99,24 +138,44 @@ Output + + + + + + + + + + + + + + + + + + + +
Name Mandatory Type Description
acpi_shutdown boolean ACPI shutdown support
adapter_type string QEMU adapter type
adapters integer number of adapters
boot_priority enum Possible values: c, d
cdrom_image string QEMU cdrom image path
cdrom_image_md5sum ['string', 'null'] QEMU cdrom image checksum
console integer console TCP port
console_type enum Possible values: telnet, vnc
cpu_throttling integer Percentage of CPU allowed for QEMU
cpus ['integer', 'null'] number of vCPUs
hda_disk_image string QEMU hda disk image path
hda_disk_image_md5sum ['string', 'null'] QEMU hda disk image checksum
hda_disk_interface string QEMU hda interface
hdb_disk_image string QEMU hdb disk image path
hdb_disk_image_md5sum ['string', 'null'] QEMU hdb disk image checksum
hdb_disk_interface string QEMU hdb interface
hdc_disk_image string QEMU hdc disk image path
hdc_disk_image_md5sum ['string', 'null'] QEMU hdc disk image checksum
hdc_disk_interface string QEMU hdc interface
hdd_disk_image string QEMU hdd disk image path
hdd_disk_image_md5sum ['string', 'null'] QEMU hdd disk image checksum
hdd_disk_interface string QEMU hdd interface
initrd string QEMU initrd path
initrd_md5sum ['string', 'null'] QEMU initrd path
kernel_command_line string QEMU kernel command line
kernel_image string QEMU kernel image path
kernel_image_md5sum ['string', 'null'] QEMU kernel image checksum
legacy_networking boolean Use QEMU legagy networking commands (-net syntax)
mac_address string QEMU MAC address
name string QEMU VM instance name
options string Additional QEMU options
platform enum Possible values: aarch64, alpha, arm, cris, i386, lm32, m68k, microblaze, microblazeel, mips, mips64, mips64el, mipsel, moxie, or32, ppc, ppc64, ppcemb, s390x, sh4, sh4eb, sparc, sparc64, tricore, unicore32, x86_64, xtensa, xtensaeb
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
usage string How to use the qemu VM
vm_directory string
vm_id string QEMU VM uuid
@@ -129,7 +188,7 @@ Sample session DELETE /v1/projects/**{project_id}**/qemu/vms/**{vm_id}** ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Delete a Qemu.instance +Delete a Qemu VM instance Parameters ********** diff --git a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst index d9a76999..1da34454 100644 --- a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -5,13 +5,13 @@ 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 +Add a NIO to a Qemu VM instance Parameters ********** -- **project_id**: UUID for the project - **adapter_number**: Network adapter where the nio is located - **port_number**: Port on the adapter (always 0) +- **project_id**: UUID for the project - **vm_id**: UUID for the instance Response status codes @@ -29,13 +29,13 @@ Sample session 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 +Remove a NIO from a Qemu VM instance Parameters ********** -- **project_id**: UUID for the project - **adapter_number**: Network adapter where the nio is located - **port_number**: Port on the adapter (always 0) +- **project_id**: UUID for the project - **vm_id**: UUID for the instance Response status codes diff --git a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidreload.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidreload.rst index 5b29cec3..701115e5 100644 --- a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidreload.rst +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidreload.rst @@ -5,7 +5,7 @@ POST /v1/projects/**{project_id}**/qemu/vms/**{vm_id}**/reload ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Reload a Qemu.instance +Reload a Qemu VM instance Parameters ********** diff --git a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidresume.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidresume.rst index 59b5c1f7..2d5314ff 100644 --- a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidresume.rst +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidresume.rst @@ -5,7 +5,7 @@ POST /v1/projects/**{project_id}**/qemu/vms/**{vm_id}**/resume ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Resume a Qemu.instance +Resume a Qemu VM instance Parameters ********** diff --git a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstart.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstart.rst index f5306892..80a97d69 100644 --- a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstart.rst +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstart.rst @@ -5,7 +5,7 @@ POST /v1/projects/**{project_id}**/qemu/vms/**{vm_id}**/start ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Start a Qemu.instance +Start a Qemu VM instance Parameters ********** diff --git a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstop.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstop.rst index d5c41c96..83ea04c0 100644 --- a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstop.rst +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidstop.rst @@ -5,7 +5,7 @@ POST /v1/projects/**{project_id}**/qemu/vms/**{vm_id}**/stop ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Stop a Qemu.instance +Stop a Qemu VM instance Parameters ********** diff --git a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidsuspend.rst b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidsuspend.rst index 63e8c94a..f09da59d 100644 --- a/docs/api/v1/qemu/projectsprojectidqemuvmsvmidsuspend.rst +++ b/docs/api/v1/qemu/projectsprojectidqemuvmsvmidsuspend.rst @@ -5,7 +5,7 @@ POST /v1/projects/**{project_id}**/qemu/vms/**{vm_id}**/suspend ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Suspend a Qemu.instance +Suspend a Qemu VM instance Parameters ********** diff --git a/docs/api/v1/qemu/qemubinaries.rst b/docs/api/v1/qemu/qemubinaries.rst index 6f851195..b04a906c 100644 --- a/docs/api/v1/qemu/qemubinaries.rst +++ b/docs/api/v1/qemu/qemubinaries.rst @@ -13,6 +13,15 @@ Response status codes - **400**: Invalid request - **404**: Instance doesn't exist +Input +******* +.. raw:: html + + + + +
Name Mandatory Type Description
archs array Architectures to filter binaries by
+ Sample session *************** diff --git a/docs/api/v1/qemu/qemucapabilities.rst b/docs/api/v1/qemu/qemucapabilities.rst new file mode 100644 index 00000000..390525df --- /dev/null +++ b/docs/api/v1/qemu/qemucapabilities.rst @@ -0,0 +1,28 @@ +/v1/qemu/capabilities +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +GET /v1/qemu/capabilities +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Get a list of Qemu capabilities on this server + +Response status codes +********************** +- **200**: Success + +Output +******* +.. raw:: html + + + + +
Name Mandatory Type Description
kvm array Architectures that KVM is enabled for
+ +Sample session +*************** + + +.. literalinclude:: ../../examples/get_qemucapabilities.txt + diff --git a/docs/api/v1/qemu/qemuimg.rst b/docs/api/v1/qemu/qemuimg.rst new file mode 100644 index 00000000..b94c4da8 --- /dev/null +++ b/docs/api/v1/qemu/qemuimg.rst @@ -0,0 +1,39 @@ +/v1/qemu/img +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/qemu/img +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Create a Qemu image + +Response status codes +********************** +- **201**: Image created + +Input +******* +.. raw:: html + + + + + + + + + + + + + + + +
Name Mandatory Type Description
adapter_type enum Possible values: ide, lsilogic, buslogic, legacyESX
cluster_size integer
format enum Possible values: qcow2, qcow, vpc, vdi, vmdk, raw
lazy_refcounts enum Possible values: on, off
path string Absolute or relative path of the image
preallocation enum Possible values: off, metadata, falloc, full
qemu_img string Path to the qemu-img binary
refcount_bits integer
size integer Image size in M
static enum Possible values: on, off
subformat enum Possible values: dynamic, fixed, streamOptimized, twoGbMaxExtentSparse, twoGbMaxExtentFlat, monolithicSparse, monolithicFlat
zeroed_grain enum Possible values: on, off
+ +Sample session +*************** + + +.. literalinclude:: ../../examples/post_qemuimg.txt + diff --git a/docs/api/v1/qemu/qemuimgbinaries.rst b/docs/api/v1/qemu/qemuimgbinaries.rst new file mode 100644 index 00000000..596e0b69 --- /dev/null +++ b/docs/api/v1/qemu/qemuimgbinaries.rst @@ -0,0 +1,15 @@ +/v1/qemu/img-binaries +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +GET /v1/qemu/img-binaries +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Get a list of available Qemu-img binaries + +Response status codes +********************** +- **200**: Success +- **400**: Invalid request +- **404**: Instance doesn't exist + diff --git a/docs/api/v1/qemu/qemuvms.rst b/docs/api/v1/qemu/qemuvms.rst new file mode 100644 index 00000000..fa8208c9 --- /dev/null +++ b/docs/api/v1/qemu/qemuvms.rst @@ -0,0 +1,13 @@ +/v1/qemu/vms +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +GET /v1/qemu/vms +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Retrieve the list of Qemu images + +Response status codes +********************** +- **200**: List of Qemu images retrieved + diff --git a/docs/api/v1/qemu/qemuvmspath.rst b/docs/api/v1/qemu/qemuvmspath.rst new file mode 100644 index 00000000..625ed024 --- /dev/null +++ b/docs/api/v1/qemu/qemuvmspath.rst @@ -0,0 +1,13 @@ +/v1/qemu/vms/{path:.+} +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/qemu/vms/**{path:.+}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Upload Qemu image. + +Response status codes +********************** +- **204**: Image uploaded + diff --git a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvms.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvms.rst index a6a77682..879c24ea 100644 --- a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvms.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvms.rst @@ -23,6 +23,7 @@ Input + @@ -42,6 +43,7 @@ Output
Name Mandatory Type Description
acpi_shutdown boolean ACPI shutdown
adapter_type string VirtualBox adapter type
adapters integer number of adapters
console integer console TCP port
+ @@ -51,6 +53,7 @@ Output +
Name Mandatory Type Description
acpi_shutdown boolean ACPI shutdown
adapter_type string VirtualBox adapter type
adapters integer number of adapters
console integer console TCP port
project_id string Project UUID
ram integer Amount of RAM
use_any_adapter boolean allow GNS3 to use any VirtualBox adapter
vm_directory ['string', 'null']
vm_id string VirtualBox VM instance UUID
vmname string VirtualBox VM name (in VirtualBox itself)
diff --git a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmid.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmid.rst index 6c974e12..7028c292 100644 --- a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmid.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmid.rst @@ -24,6 +24,7 @@ Output + @@ -33,6 +34,7 @@ Output +
Name Mandatory Type Description
acpi_shutdown boolean ACPI shutdown
adapter_type string VirtualBox adapter type
adapters integer number of adapters
console integer console TCP port
project_id string Project UUID
ram integer Amount of RAM
use_any_adapter boolean allow GNS3 to use any VirtualBox adapter
vm_directory ['string', 'null']
vm_id string VirtualBox VM instance UUID
vmname string VirtualBox VM name (in VirtualBox itself)
@@ -66,6 +68,7 @@ Input + @@ -83,6 +86,7 @@ Output
Name Mandatory Type Description
acpi_shutdown boolean ACPI shutdown
adapter_type string VirtualBox adapter type
adapters integer number of adapters
console integer console TCP port
+ @@ -92,6 +96,7 @@ Output +
Name Mandatory Type Description
acpi_shutdown boolean ACPI shutdown
adapter_type string VirtualBox adapter type
adapters integer number of adapters
console integer console TCP port
project_id string Project UUID
ram integer Amount of RAM
use_any_adapter boolean allow GNS3 to use any VirtualBox adapter
vm_directory ['string', 'null']
vm_id string VirtualBox VM instance UUID
vmname string VirtualBox VM name (in VirtualBox itself)
diff --git a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst index 19b76253..bb37508f 100644 --- a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -9,9 +9,9 @@ Add a NIO to a VirtualBox VM instance Parameters ********** -- **project_id**: UUID for the project - **adapter_number**: Adapter where the nio should be added - **port_number**: Port on the adapter (always 0) +- **project_id**: UUID for the project - **vm_id**: UUID for the instance Response status codes @@ -33,9 +33,9 @@ Remove a NIO from a VirtualBox VM instance Parameters ********** -- **project_id**: UUID for the project - **adapter_number**: Adapter from where the nio should be removed - **port_number**: Port on the adapter (always 0) +- **project_id**: UUID for the project - **vm_id**: UUID for the instance Response status codes diff --git a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst index 47b4fdca..188bc6d5 100644 --- a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst @@ -9,9 +9,9 @@ Start a packet capture on a VirtualBox VM instance Parameters ********** -- **project_id**: UUID for the project - **adapter_number**: Adapter to start a packet capture - **port_number**: Port on the adapter (always 0) +- **project_id**: UUID for the project - **vm_id**: UUID for the instance Response status codes diff --git a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst index 057170b0..370f02da 100644 --- a/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst +++ b/docs/api/v1/virtualbox/projectsprojectidvirtualboxvmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst @@ -9,9 +9,9 @@ Stop a packet capture on a VirtualBox VM instance Parameters ********** -- **project_id**: UUID for the project - **adapter_number**: Adapter to stop a packet capture - **port_number**: Port on the adapter (always 0) +- **project_id**: UUID for the project - **vm_id**: UUID for the instance Response status codes diff --git a/docs/api/v1/vmware.rst b/docs/api/v1/vmware.rst new file mode 100644 index 00000000..6a3cf3a0 --- /dev/null +++ b/docs/api/v1/vmware.rst @@ -0,0 +1,8 @@ +Vmware +--------------------- + +.. toctree:: + :glob: + :maxdepth: 2 + + vmware/* diff --git a/docs/api/v1/vmware/projectsprojectidvmwarevms.rst b/docs/api/v1/vmware/projectsprojectidvmwarevms.rst new file mode 100644 index 00000000..cf422e4c --- /dev/null +++ b/docs/api/v1/vmware/projectsprojectidvmwarevms.rst @@ -0,0 +1,60 @@ +/v1/projects/{project_id}/vmware/vms +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/vmware/vms +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Create a new VMware 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
acpi_shutdown boolean ACPI shutdown
adapter_type string VMware 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 VMware VM instance name
use_any_adapter boolean allow GNS3 to use any VMware adapter
use_ubridge boolean use uBridge for network connections
vm_id string VMware VM instance identifier
vmx_path string path to the vmx file
+ +Output +******* +.. raw:: html + + + + + + + + + + + + + + + + +
Name Mandatory Type Description
acpi_shutdown boolean ACPI shutdown
adapter_type string VMware 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 VMware VM instance name
project_id string Project UUID
use_any_adapter boolean allow GNS3 to use any VMware adapter
use_ubridge boolean use uBridge for network connections
vm_directory ['string', 'null']
vm_id string VMware VM instance UUID
vmx_path string path to the vmx file
+ diff --git a/docs/api/v1/vmware/projectsprojectidvmwarevmsvmid.rst b/docs/api/v1/vmware/projectsprojectidvmwarevmsvmid.rst new file mode 100644 index 00000000..f3359fb0 --- /dev/null +++ b/docs/api/v1/vmware/projectsprojectidvmwarevmsvmid.rst @@ -0,0 +1,113 @@ +/v1/projects/{project_id}/vmware/vms/{vm_id} +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +GET /v1/projects/**{project_id}**/vmware/vms/**{vm_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Get a VMware 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
acpi_shutdown boolean ACPI shutdown
adapter_type string VMware 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 VMware VM instance name
project_id string Project UUID
use_any_adapter boolean allow GNS3 to use any VMware adapter
use_ubridge boolean use uBridge for network connections
vm_directory ['string', 'null']
vm_id string VMware VM instance UUID
vmx_path string path to the vmx file
+ + +PUT /v1/projects/**{project_id}**/vmware/vms/**{vm_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Update a VMware 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
acpi_shutdown boolean ACPI shutdown
adapter_type string VMware 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 VMware VM instance name
use_any_adapter boolean allow GNS3 to use any VMware adapter
use_ubridge boolean use uBridge for network connections
vmx_path string path to the vmx file
+ +Output +******* +.. raw:: html + + + + + + + + + + + + + + + + +
Name Mandatory Type Description
acpi_shutdown boolean ACPI shutdown
adapter_type string VMware 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 VMware VM instance name
project_id string Project UUID
use_any_adapter boolean allow GNS3 to use any VMware adapter
use_ubridge boolean use uBridge for network connections
vm_directory ['string', 'null']
vm_id string VMware VM instance UUID
vmx_path string path to the vmx file
+ + +DELETE /v1/projects/**{project_id}**/vmware/vms/**{vm_id}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Delete a VMware 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/vmware/projectsprojectidvmwarevmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/v1/vmware/projectsprojectidvmwarevmsvmidadaptersadapternumberdportsportnumberdnio.rst new file mode 100644 index 00000000..3e026251 --- /dev/null +++ b/docs/api/v1/vmware/projectsprojectidvmwarevmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -0,0 +1,40 @@ +/v1/projects/{project_id}/vmware/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/vmware/vms/**{vm_id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Add a NIO to a VMware VM instance + +Parameters +********** +- **adapter_number**: Adapter where the nio should be added +- **port_number**: Port on the adapter (always 0) +- **project_id**: UUID for the project +- **vm_id**: UUID for the instance + +Response status codes +********************** +- **400**: Invalid request +- **201**: NIO created +- **404**: Instance doesn't exist + + +DELETE /v1/projects/**{project_id}**/vmware/vms/**{vm_id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Remove a NIO from a VMware VM instance + +Parameters +********** +- **adapter_number**: Adapter from where the nio should be removed +- **port_number**: Port on the adapter (always 0) +- **project_id**: UUID for the project +- **vm_id**: UUID for the instance + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: NIO deleted + diff --git a/docs/api/v1/vmware/projectsprojectidvmwarevmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst b/docs/api/v1/vmware/projectsprojectidvmwarevmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst new file mode 100644 index 00000000..f988f5cd --- /dev/null +++ b/docs/api/v1/vmware/projectsprojectidvmwarevmsvmidadaptersadapternumberdportsportnumberdstartcapture.rst @@ -0,0 +1,31 @@ +/v1/projects/{project_id}/vmware/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/start_capture +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/vmware/vms/**{vm_id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/start_capture +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Start a packet capture on a VMware VM instance + +Parameters +********** +- **adapter_number**: Adapter to start a packet capture +- **port_number**: Port on the adapter (always 0) +- **project_id**: UUID for the project +- **vm_id**: UUID for the instance + +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/vmware/projectsprojectidvmwarevmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst b/docs/api/v1/vmware/projectsprojectidvmwarevmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst new file mode 100644 index 00000000..23a1980e --- /dev/null +++ b/docs/api/v1/vmware/projectsprojectidvmwarevmsvmidadaptersadapternumberdportsportnumberdstopcapture.rst @@ -0,0 +1,22 @@ +/v1/projects/{project_id}/vmware/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/stop_capture +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/vmware/vms/**{vm_id}**/adapters/**{adapter_number:\d+}**/ports/**{port_number:\d+}**/stop_capture +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Stop a packet capture on a VMware VM instance + +Parameters +********** +- **adapter_number**: Adapter to stop a packet capture +- **port_number**: Port on the adapter (always 0) +- **project_id**: UUID for the project +- **vm_id**: UUID for the instance + +Response status codes +********************** +- **400**: Invalid request +- **404**: Instance doesn't exist +- **204**: Capture stopped + diff --git a/docs/api/v1/vmware/projectsprojectidvmwarevmsvmidinterfacesvmnet.rst b/docs/api/v1/vmware/projectsprojectidvmwarevmsvmidinterfacesvmnet.rst new file mode 100644 index 00000000..0b30ef4b --- /dev/null +++ b/docs/api/v1/vmware/projectsprojectidvmwarevmsvmidinterfacesvmnet.rst @@ -0,0 +1,18 @@ +/v1/projects/{project_id}/vmware/vms/{vm_id}/interfaces/vmnet +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/vmware/vms/**{vm_id}**/interfaces/vmnet +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Allocate a VMware VMnet interface on the server + +Parameters +********** +- **project_id**: The UUID of the project +- **vm_id**: UUID for the instance + +Response status codes +********************** +- **201**: VMnet interface allocated + diff --git a/docs/api/v1/vmware/projectsprojectidvmwarevmsvmidreload.rst b/docs/api/v1/vmware/projectsprojectidvmwarevmsvmidreload.rst new file mode 100644 index 00000000..f3363785 --- /dev/null +++ b/docs/api/v1/vmware/projectsprojectidvmwarevmsvmidreload.rst @@ -0,0 +1,20 @@ +/v1/projects/{project_id}/vmware/vms/{vm_id}/reload +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/vmware/vms/**{vm_id}**/reload +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Reload a VMware 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/vmware/projectsprojectidvmwarevmsvmidresume.rst b/docs/api/v1/vmware/projectsprojectidvmwarevmsvmidresume.rst new file mode 100644 index 00000000..66267bd8 --- /dev/null +++ b/docs/api/v1/vmware/projectsprojectidvmwarevmsvmidresume.rst @@ -0,0 +1,20 @@ +/v1/projects/{project_id}/vmware/vms/{vm_id}/resume +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/vmware/vms/**{vm_id}**/resume +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Resume a suspended VMware 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/vmware/projectsprojectidvmwarevmsvmidstart.rst b/docs/api/v1/vmware/projectsprojectidvmwarevmsvmidstart.rst new file mode 100644 index 00000000..57fdeab7 --- /dev/null +++ b/docs/api/v1/vmware/projectsprojectidvmwarevmsvmidstart.rst @@ -0,0 +1,20 @@ +/v1/projects/{project_id}/vmware/vms/{vm_id}/start +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/vmware/vms/**{vm_id}**/start +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Start a VMware 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/vmware/projectsprojectidvmwarevmsvmidstop.rst b/docs/api/v1/vmware/projectsprojectidvmwarevmsvmidstop.rst new file mode 100644 index 00000000..95a3f6fe --- /dev/null +++ b/docs/api/v1/vmware/projectsprojectidvmwarevmsvmidstop.rst @@ -0,0 +1,20 @@ +/v1/projects/{project_id}/vmware/vms/{vm_id}/stop +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/vmware/vms/**{vm_id}**/stop +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Stop a VMware 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/vmware/projectsprojectidvmwarevmsvmidsuspend.rst b/docs/api/v1/vmware/projectsprojectidvmwarevmsvmidsuspend.rst new file mode 100644 index 00000000..5655b3ab --- /dev/null +++ b/docs/api/v1/vmware/projectsprojectidvmwarevmsvmidsuspend.rst @@ -0,0 +1,20 @@ +/v1/projects/{project_id}/vmware/vms/{vm_id}/suspend +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +POST /v1/projects/**{project_id}**/vmware/vms/**{vm_id}**/suspend +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Suspend a VMware 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/vmware/vmwarevms.rst b/docs/api/v1/vmware/vmwarevms.rst new file mode 100644 index 00000000..af366959 --- /dev/null +++ b/docs/api/v1/vmware/vmwarevms.rst @@ -0,0 +1,13 @@ +/v1/vmware/vms +---------------------------------------------------------------------------------------------------------------------- + +.. contents:: + +GET /v1/vmware/vms +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Get all VMware VMs available + +Response status codes +********************** +- **200**: Success + diff --git a/docs/api/v1/vpcs/projectsprojectidvpcsvms.rst b/docs/api/v1/vpcs/projectsprojectidvpcsvms.rst index 3bc1e779..5f576e24 100644 --- a/docs/api/v1/vpcs/projectsprojectidvpcsvms.rst +++ b/docs/api/v1/vpcs/projectsprojectidvpcsvms.rst @@ -40,6 +40,8 @@ Output 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 + status ✔ enum Possible values: started, stopped + vm_directory string vm_id ✔ string VPCS VM UUID diff --git a/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmid.rst b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmid.rst index 08762d16..38eb6f6a 100644 --- a/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmid.rst +++ b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmid.rst @@ -29,6 +29,8 @@ Output 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 + status ✔ enum Possible values: started, stopped + vm_directory string vm_id ✔ string VPCS VM UUID @@ -77,6 +79,8 @@ Output 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 + status ✔ enum Possible values: started, stopped + vm_directory string vm_id ✔ string VPCS VM UUID diff --git a/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst index 7592c97c..c56e69c7 100644 --- a/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst +++ b/docs/api/v1/vpcs/projectsprojectidvpcsvmsvmidadaptersadapternumberdportsportnumberdnio.rst @@ -9,9 +9,9 @@ Add a NIO to a VPCS instance Parameters ********** -- **project_id**: UUID for the project - **adapter_number**: Network adapter where the nio is located - **port_number**: Port where the nio should be added +- **project_id**: UUID for the project - **vm_id**: UUID for the instance Response status codes @@ -33,9 +33,9 @@ Remove a NIO from a VPCS instance Parameters ********** -- **project_id**: UUID for the project - **adapter_number**: Network adapter where the nio is located - **port_number**: Port from where the nio should be removed +- **project_id**: UUID for the project - **vm_id**: UUID for the instance Response status codes diff --git a/docs/general.rst b/docs/general.rst index 9f4bc91c..97fef935 100644 --- a/docs/general.rst +++ b/docs/general.rst @@ -199,3 +199,15 @@ to the nature of the multiple supported VM it's easy for an user to upload and run code on your machine. +Notifications +============= + +You can receive notification from the server if you listen the HTTP stream /notifications. + +The available notification are: +* ping +* vm.created +* vm.started +* vm.stopped +* log.error + diff --git a/gns3server.bat b/gns3server.bat index ff132074..b5448a41 100644 --- a/gns3server.bat +++ b/gns3server.bat @@ -1,2 +1,2 @@ -SET PYTHONPATH=. -python.exe gns3server/main.py --debug --local +SET PYTHONPATH=%~dp0 +python.exe %~dp0/gns3server/main.py --debug --local diff --git a/gns3server/__main__.py b/gns3server/__main__.py new file mode 100644 index 00000000..7a1f1b64 --- /dev/null +++ b/gns3server/__main__.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +# +# 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 .main import main + +main() diff --git a/gns3server/cert_utils/create_cert.sh b/gns3server/cert_utils/create_cert.sh index 92f6edfb..4828e110 100755 --- a/gns3server/cert_utils/create_cert.sh +++ b/gns3server/cert_utils/create_cert.sh @@ -17,28 +17,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -# Bash shell script for generating self-signed certs. Run this in a folder, as it -# generates a few files. Large portions of this script were taken from the -# following artcile: -# -# http://usrportage.de/archives/919-Batch-generating-SSL-certificates.html -# -# Additional alterations by: Brad Landers -# Date: 2012-01-27 -# https://gist.github.com/bradland/1690807 - -# Script accepts a single argument, the fqdn for the cert +# Bash shell script for generating self-signed certs. +# The certicate is automaticaly put in your GNS3 config -DST_DIR="$HOME/.config/GNS3Certs/" +DST_DIR="$HOME/.config/GNS3/ssl" OLD_DIR=`pwd` -#GNS3 Server expects to find certs with the default FQDN below. If you create -#different certs you will need to update server.py -DOMAIN="$1" -if [ -z "$DOMAIN" ]; then - DOMAIN="gns3server.localdomain.com" -fi - fail_if_error() { [ $1 != 0 ] && { unset PASSPHRASE @@ -52,48 +36,6 @@ mkdir -p $DST_DIR fail_if_error $? cd $DST_DIR +SUBJ="/C=CA/ST=Alberta/O=GNS3SELF/localityName=Calgary/commonName=localhost/organizationalUnitName=GNS3Server/emailAddress=gns3cert@gns3.com" -# Generate a passphrase -export PASSPHRASE=$(head -c 500 /dev/urandom | tr -dc a-z0-9A-Z | head -c 128; echo) - -# Certificate details; replace items in angle brackets with your own info -subj=" -C=CA -ST=Alberta -O=GNS3 -localityName=Calgary -commonName=$DOMAIN -organizationalUnitName=GNS3Server -emailAddress=gns3cert@gns3.com -" - -# Generate the server private key -openssl genrsa -aes256 -out $DOMAIN.key -passout env:PASSPHRASE 2048 -fail_if_error $? - -#openssl rsa -outform der -in $DOMAIN.pem -out $DOMAIN.key -passin env:PASSPHRASE - -# Generate the CSR -openssl req \ - -new \ - -batch \ - -subj "$(echo -n "$subj" | tr "\n" "/")" \ - -key $DOMAIN.key \ - -out $DOMAIN.csr \ - -passin env:PASSPHRASE -fail_if_error $? -cp $DOMAIN.key $DOMAIN.key.org -fail_if_error $? - -# Strip the password so we don't have to type it every time we restart Apache -openssl rsa -in $DOMAIN.key.org -out $DOMAIN.key -passin env:PASSPHRASE -fail_if_error $? - -# Generate the cert (good for 10 years) -openssl x509 -req -days 3650 -in $DOMAIN.csr -signkey $DOMAIN.key -out $DOMAIN.crt -fail_if_error $? - -echo "${DST_DIR}${DOMAIN}.key" -echo "${DST_DIR}${DOMAIN}.crt" - -cd $OLD_DIR \ No newline at end of file +openssl req -nodes -new -x509 -keyout server.key -out server.cert -subj "$SUBJ" diff --git a/gns3server/config.py b/gns3server/config.py index e0086b5a..44336e71 100644 --- a/gns3server/config.py +++ b/gns3server/config.py @@ -36,16 +36,19 @@ class Config(object): Configuration file management using configparser. :params files: Array of configuration files (optional) + :params config_directory: Path of the configuration directory. If None default OS directory """ - def __init__(self, files=None): + def __init__(self, files=None, config_directory=None): self._files = files # Monitor configuration files for changes self._watched_files = {} - if sys.platform.startswith("win"): + if hasattr(sys, "_called_from_test"): + self._files = files + elif sys.platform.startswith("win"): appname = "GNS3" @@ -60,11 +63,11 @@ class Config(object): common_appdata = os.path.expandvars("%COMMON_APPDATA%") filename = "gns3_server.ini" if self._files is None: - self._files = [os.path.join(appdata, appname, filename), + self._files = [os.path.join(os.getcwd(), filename), + 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] + os.path.join(common_appdata, appname + ".ini")] else: # On UNIX-like platforms, the configuration file location can be one of the following: @@ -74,19 +77,18 @@ class Config(object): # 4: /etc/xdg/GNS3.conf # 5: server.conf in the current working directory - if sys.platform.startswith("darwin"): - appname = "gns3.net" - else: - appname = "GNS3" + appname = "GNS3" home = os.path.expanduser("~") filename = "gns3_server.conf" if self._files is None: - self._files = [os.path.join(home, ".config", appname, filename), + self._files = [os.path.join(os.getcwd(), filename), + 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] + os.path.join("/etc/xdg", appname + ".conf")] + if self._files is None: + self._files = [] self.clear() self._watch_config_file() diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py index 489042b5..7d4b1f0d 100644 --- a/gns3server/crash_report.py +++ b/gns3server/crash_report.py @@ -28,15 +28,16 @@ except ImportError: # raven is not installed with deb package in order to simplify packaging RAVEN_AVAILABLE = False -from .version import __version__ +from .version import __version__, __version_info__ from .config import Config +from .utils.get_resource import get_resource import logging log = logging.getLogger(__name__) # Dev build -if __version__[4] != 0: +if __version_info__[3] != 0: import faulthandler # Display a traceback in case of segfault crash. Usefull when frozen @@ -51,10 +52,10 @@ class CrashReport: Report crash to a third party service """ - DSN = "sync+https://e47fab7019fe4fb3b699609bb9f4fefc:d1d3008a6c8a443ba34f336434c2da34@app.getsentry.com/38482" + DSN = "sync+https://119ddececccd43b69951ac87d4859870:2a982a50bbbb49ddb33c87ef3720026e@app.getsentry.com/38482" if hasattr(sys, "frozen"): - cacert = os.path.join(os.getcwd(), "cacert.pem") - if os.path.isfile(cacert): + cacert = get_resource("cacert.pem") + if cacert is not None and os.path.isfile(cacert): DSN += "?ca_certs={}".format(cacert) else: log.warning("The SSL certificate bundle file '{}' could not be found".format(cacert)) @@ -63,6 +64,13 @@ class CrashReport: def __init__(self): self._client = None + # We don't want sentry making noise if an error is catched when you don't have internet + sentry_errors = logging.getLogger('sentry.errors') + sentry_errors.disabled = True + + sentry_uncaught = logging.getLogger('sentry.errors.uncaught') + sentry_uncaught.disabled = True + def capture_exception(self, request=None): if not RAVEN_AVAILABLE: return diff --git a/gns3server/handlers/__init__.py b/gns3server/handlers/__init__.py index 53d65e69..d2fb9b5f 100644 --- a/gns3server/handlers/__init__.py +++ b/gns3server/handlers/__init__.py @@ -24,9 +24,12 @@ from gns3server.handlers.api.dynamips_device_handler import DynamipsDeviceHandle from gns3server.handlers.api.dynamips_vm_handler import DynamipsVMHandler from gns3server.handlers.api.qemu_handler import QEMUHandler from gns3server.handlers.api.virtualbox_handler import VirtualBoxHandler +from gns3server.handlers.api.docker_handler import DockerHandler from gns3server.handlers.api.vpcs_handler import VPCSHandler +from gns3server.handlers.api.vmware_handler import VMwareHandler from gns3server.handlers.api.config_handler import ConfigHandler from gns3server.handlers.api.server_handler import ServerHandler +from gns3server.handlers.api.file_handler import FileHandler from gns3server.handlers.upload_handler import UploadHandler from gns3server.handlers.index_handler import IndexHandler diff --git a/gns3server/handlers/api/docker_handler.py b/gns3server/handlers/api/docker_handler.py new file mode 100644 index 00000000..0f015054 --- /dev/null +++ b/gns3server/handlers/api/docker_handler.py @@ -0,0 +1,247 @@ +# -*- 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 aiohttp.web import HTTPConflict + +from ...web.route import Route +from ...modules.docker import Docker + +from ...schemas.docker import ( + DOCKER_CREATE_SCHEMA, DOCKER_UPDATE_SCHEMA, DOCKER_CAPTURE_SCHEMA, + DOCKER_OBJECT_SCHEMA +) +from ...schemas.nio import NIO_SCHEMA + + +class DockerHandler: + """API entry points for Docker.""" + + @classmethod + @Route.get( + r"/docker/images", + status_codes={ + 200: "Success", + }, + description="Get all available Docker images") + def show(request, response): + docker_manager = Docker.instance() + images = yield from docker_manager.list_images() + response.json(images) + + @classmethod + @Route.post( + r"/projects/{project_id}/docker/images", + parameters={ + "project_id": "UUID for the project" + }, + status_codes={ + 201: "Instance created", + 400: "Invalid request", + 409: "Conflict" + }, + description="Create a new Docker container", + input=DOCKER_CREATE_SCHEMA, + output=DOCKER_OBJECT_SCHEMA) + def create(request, response): + docker_manager = Docker.instance() + container = yield from docker_manager.create_vm( + request.json.pop("name"), + request.match_info["project_id"], + request.json.get("id"), + image=request.json.pop("imagename"), + startcmd=request.json.get("startcmd") + ) + # FIXME: DO WE NEED THIS? + for name, value in request.json.items(): + if name != "_vm_id": + if hasattr(container, name) and getattr(container, name) != value: + setattr(container, name, value) + + response.set_status(201) + response.json(container) + + @classmethod + @Route.post( + r"/projects/{project_id}/docker/images/{id}/start", + parameters={ + "project_id": "UUID of the project", + "id": "ID of the container" + }, + status_codes={ + 204: "Instance started", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Start a Docker container", + input=DOCKER_CREATE_SCHEMA, + output=DOCKER_OBJECT_SCHEMA) + def start(request, response): + docker_manager = Docker.instance() + container = docker_manager.get_container( + request.match_info["id"], + project_id=request.match_info["project_id"]) + yield from container.start() + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/docker/images/{id}/stop", + parameters={ + "project_id": "UUID of the project", + "id": "ID of the container" + }, + status_codes={ + 204: "Instance stopped", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Stop a Docker container", + input=DOCKER_CREATE_SCHEMA, + output=DOCKER_OBJECT_SCHEMA) + def stop(request, response): + docker_manager = Docker.instance() + container = docker_manager.get_container( + request.match_info["id"], + project_id=request.match_info["project_id"]) + yield from container.stop() + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/docker/images/{id}/reload", + parameters={ + "project_id": "UUID of the project", + "id": "ID of the container" + }, + status_codes={ + 204: "Instance restarted", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Restart a Docker container", + input=DOCKER_CREATE_SCHEMA, + output=DOCKER_OBJECT_SCHEMA) + def reload(request, response): + docker_manager = Docker.instance() + container = docker_manager.get_container( + request.match_info["id"], + project_id=request.match_info["project_id"]) + yield from container.restart() + response.set_status(204) + + @classmethod + @Route.delete( + r"/projects/{project_id}/docker/images/{id}", + parameters={ + "id": "ID for the container", + "project_id": "UUID for the project" + }, + status_codes={ + 204: "Instance deleted", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Delete a Docker container") + def delete(request, response): + docker_manager = Docker.instance() + container = docker_manager.get_container( + request.match_info["id"], + project_id=request.match_info["project_id"]) + yield from container.remove() + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/docker/images/{id}/suspend", + parameters={ + "project_id": "UUID of the project", + "id": "ID of the container" + }, + status_codes={ + 204: "Instance paused", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Pause a Docker container", + input=DOCKER_CREATE_SCHEMA, + output=DOCKER_OBJECT_SCHEMA) + def suspend(request, response): + docker_manager = Docker.instance() + container = docker_manager.get_container( + request.match_info["id"], + project_id=request.match_info["project_id"]) + yield from container.pause() + response.set_status(204) + + @Route.post( + r"/projects/{project_id}/docker/images/{id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio", + parameters={ + "project_id": "UUID for the project", + "id": "ID of the container", + "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 Docker container", + input=NIO_SCHEMA, + output=NIO_SCHEMA) + def create_nio(request, response): + docker_manager = Docker.instance() + container = docker_manager.get_container( + request.match_info["id"], + project_id=request.match_info["project_id"]) + nio_type = request.json["type"] + if nio_type not in ("nio_udp"): + raise HTTPConflict( + text="NIO of type {} is not supported".format(nio_type)) + nio = docker_manager.create_nio( + int(request.match_info["adapter_number"]), request.json) + adapter = container._ethernet_adapters[ + int(request.match_info["adapter_number"]) + ] + container.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}/docker/images/{id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio", + parameters={ + "project_id": "UUID for the project", + "id": "ID of the container", + "adapter_number": "Adapter where the nio should be added", + "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 Docker container") + def delete_nio(request, response): + docker_manager = Docker.instance() + container = docker_manager.get_container( + request.match_info["id"], + project_id=request.match_info["project_id"]) + yield from container.adapter_remove_nio_binding( + int(request.match_info["adapter_number"])) + response.set_status(204) diff --git a/gns3server/handlers/api/dynamips_vm_handler.py b/gns3server/handlers/api/dynamips_vm_handler.py index 8bb32bf5..f3a96eed 100644 --- a/gns3server/handlers/api/dynamips_vm_handler.py +++ b/gns3server/handlers/api/dynamips_vm_handler.py @@ -21,6 +21,7 @@ import base64 from ...web.route import Route from ...schemas.nio import NIO_SCHEMA +from ...schemas.vm import VM_LIST_IMAGES_SCHEMA from ...schemas.dynamips_vm import VM_CREATE_SCHEMA from ...schemas.dynamips_vm import VM_UPDATE_SCHEMA from ...schemas.dynamips_vm import VM_CAPTURE_SCHEMA @@ -74,7 +75,6 @@ class DynamipsVMHandler: chassis=request.json.pop("chassis", default_chassis)) 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) @@ -120,7 +120,6 @@ class DynamipsVMHandler: vm = dynamips_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) yield from dynamips_manager.update_vm_settings(vm, request.json) - yield from dynamips_manager.ghost_ios_support(vm) response.json(vm) @classmethod @@ -161,6 +160,10 @@ class DynamipsVMHandler: dynamips_manager = Dynamips.instance() vm = dynamips_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + try: + yield from dynamips_manager.ghost_ios_support(vm) + except GeneratorExit: + pass yield from vm.start() response.set_status(204) @@ -320,7 +323,7 @@ class DynamipsVMHandler: pcap_file_path = os.path.join(vm.project.capture_working_directory(), request.json["capture_file_name"]) if sys.platform.startswith('win'): - #FIXME: Dynamips (Cygwin actually) doesn't like non ascii paths on Windows + # FIXME: Dynamips (Cygwin actually) doesn't like non ascii paths on Windows try: pcap_file_path.encode('ascii') except UnicodeEncodeError: @@ -459,3 +462,30 @@ class DynamipsVMHandler: idlepc = yield from dynamips_manager.auto_idlepc(vm) response.set_status(200) response.json({"idlepc": idlepc}) + + @Route.get( + r"/dynamips/vms", + status_codes={ + 200: "List of Dynamips VM retrieved", + }, + description="Retrieve the list of Dynamips VMS", + output=VM_LIST_IMAGES_SCHEMA) + def list_vms(request, response): + + dynamips_manager = Dynamips.instance() + vms = yield from dynamips_manager.list_images() + response.set_status(200) + response.json(vms) + + @Route.post( + r"/dynamips/vms/{path}", + status_codes={ + 204: "Image uploaded", + }, + raw=True, + description="Upload Dynamips image.") + def upload_vm(request, response): + + dynamips_manager = Dynamips.instance() + yield from dynamips_manager.write_image(request.match_info["path"], request.content) + response.set_status(204) diff --git a/gns3server/handlers/api/file_handler.py b/gns3server/handlers/api/file_handler.py new file mode 100644 index 00000000..4342f6c6 --- /dev/null +++ b/gns3server/handlers/api/file_handler.py @@ -0,0 +1,59 @@ +# +# Copyright (C) 2015 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import asyncio +import aiohttp + +from ...web.route import Route +from ...schemas.file import FILE_STREAM_SCHEMA + + +class FileHandler: + + @classmethod + @Route.get( + r"/files/stream", + description="Stream a file from the server", + status_codes={ + 200: "File retrieved", + 404: "File doesn't exist", + 409: "Can't access to file" + }, + input=FILE_STREAM_SCHEMA + ) + def read(request, response): + response.enable_chunked_encoding() + + try: + with open(request.json.get("location"), "rb") as f: + loop = asyncio.get_event_loop() + response.content_type = "application/octet-stream" + response.set_status(200) + # Very important: do not send a content lenght otherwise QT close the connection but curl can consume the Feed + response.content_length = None + + response.start(request) + + while True: + data = yield from loop.run_in_executor(None, f.read, 16) + if len(data) == 0: + yield from asyncio.sleep(0.1) + else: + response.write(data) + except FileNotFoundError: + raise aiohttp.web.HTTPNotFound() + except OSError as e: + raise aiohttp.web.HTTPConflict(text=str(e)) diff --git a/gns3server/handlers/api/iou_handler.py b/gns3server/handlers/api/iou_handler.py index 73c4183f..5606cd8b 100644 --- a/gns3server/handlers/api/iou_handler.py +++ b/gns3server/handlers/api/iou_handler.py @@ -21,10 +21,12 @@ from aiohttp.web import HTTPConflict from ...web.route import Route from ...schemas.nio import NIO_SCHEMA from ...schemas.iou import IOU_CREATE_SCHEMA +from ...schemas.iou import IOU_START_SCHEMA from ...schemas.iou import IOU_UPDATE_SCHEMA from ...schemas.iou import IOU_OBJECT_SCHEMA from ...schemas.iou import IOU_CAPTURE_SCHEMA -from ...schemas.iou import IOU_INITIAL_CONFIG_SCHEMA +from ...schemas.iou import IOU_CONFIGS_SCHEMA +from ...schemas.vm import VM_LIST_IMAGES_SCHEMA from ...modules.iou import IOU @@ -58,9 +60,15 @@ class IOUHandler: for name, value in request.json.items(): if hasattr(vm, name) and getattr(vm, name) != value: - if name == "initial_config_content" and (vm.initial_config_content and len(vm.initial_config_content) > 0): + if name == "startup_config_content" and (vm.startup_config_content and len(vm.startup_config_content) > 0): + continue + if name == "private_config_content" and (vm.private_config_content and len(vm.private_config_content) > 0): continue setattr(vm, name, value) + if "startup_config_content" in request.json: + vm.startup_config = request.json.get("startup_config_content") + if "private_config_content" in request.json: + vm.private_config = request.json.get("private_config_content") response.set_status(201) response.json(vm) @@ -108,6 +116,10 @@ class IOUHandler: for name, value in request.json.items(): if hasattr(vm, name) and getattr(vm, name) != value: setattr(vm, name, value) + if "startup_config_content" in request.json: + vm.startup_config = request.json.get("startup_config_content") + if "private_config_content" in request.json: + vm.private_config = request.json.get("private_config_content") response.json(vm) @classmethod @@ -140,11 +152,19 @@ class IOUHandler: 400: "Invalid request", 404: "Instance doesn't exist" }, + input=IOU_START_SCHEMA, 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"]) + + for name, value in request.json.items(): + if hasattr(vm, name) and getattr(vm, name) != value: + setattr(vm, name, value) + print(name) + print(vm.iourc_path) + yield from vm.start() response.set_status(204) @@ -296,18 +316,80 @@ class IOUHandler: response.set_status(204) @Route.get( - r"/projects/{project_id}/iou/vms/{vm_id}/initial_config", + r"/projects/{project_id}/iou/vms/{vm_id}/configs", status_codes={ - 200: "Initial config retrieved", + 200: "Configs 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): + output=IOU_CONFIGS_SCHEMA, + description="Retrieve the startup and private configs content") + def get_configs(request, response): iou_manager = IOU.instance() - vm = iou_manager.get_vm(request.match_info["vm_id"], - project_id=request.match_info["project_id"]) + vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + + startup_config_content, private_config_content = vm.extract_configs() + result = {} + if startup_config_content: + result["startup_config_content"] = startup_config_content.decode("utf-8", errors='replace') + else: + # nvram doesn't exists if the VM has not been started at least once + # in this case just use the startup-config file + startup_config_content = vm.startup_config_content + if startup_config_content: + result["startup_config_content"] = startup_config_content + + if private_config_content: + result["private_config_content"] = private_config_content.decode("utf-8", errors='replace') + else: + # nvram doesn't exists if the VM has not been started at least once + # in this case just use the private-config file + private_config_content = vm.private_config_content + if private_config_content: + result["private_config_content"] = private_config_content + response.set_status(200) - response.json({"content": vm.initial_config_content}) + response.json(result) + + @Route.post( + r"/projects/{project_id}/iou/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): + + iou_manager = IOU.instance() + vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + vm.save_configs() + response.set_status(200) + + @Route.get( + r"/iou/vms", + status_codes={ + 200: "List of IOU VM retrieved", + }, + description="Retrieve the list of IOU VMS", + output=VM_LIST_IMAGES_SCHEMA) + def list_vms(request, response): + + iou_manager = IOU.instance() + vms = yield from iou_manager.list_images() + response.set_status(200) + response.json(vms) + + @Route.post( + r"/iou/vms/{path}", + status_codes={ + 204: "Image uploaded", + }, + raw=True, + description="Upload IOU image.") + def upload_vm(request, response): + + iou_manager = IOU.instance() + yield from iou_manager.write_image(request.match_info["path"], request.content) + response.set_status(204) diff --git a/gns3server/handlers/api/project_handler.py b/gns3server/handlers/api/project_handler.py index 85257423..49e795cf 100644 --- a/gns3server/handlers/api/project_handler.py +++ b/gns3server/handlers/api/project_handler.py @@ -15,14 +15,40 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import aiohttp +import asyncio +import json +import os + from ...web.route import Route -from ...schemas.project import PROJECT_OBJECT_SCHEMA, PROJECT_CREATE_SCHEMA, PROJECT_UPDATE_SCHEMA +from ...schemas.project import PROJECT_OBJECT_SCHEMA, PROJECT_CREATE_SCHEMA, PROJECT_UPDATE_SCHEMA, PROJECT_FILE_LIST_SCHEMA, PROJECT_LIST_SCHEMA from ...modules.project_manager import ProjectManager from ...modules import MODULES +import logging +log = logging.getLogger() + class ProjectHandler: + # How many clients has subcribe to notifications + _notifications_listening = {} + + @classmethod + @Route.get( + r"/projects", + description="List projects opened on the server", + status_codes={ + 200: "Project list", + }, + output=PROJECT_LIST_SCHEMA + ) + def list_projects(request, response): + + pm = ProjectManager.instance() + response.set_status(200) + response.json(list(pm.projects)) + @classmethod @Route.post( r"/projects", @@ -126,8 +152,12 @@ class ProjectHandler: pm = ProjectManager.instance() project = pm.get_project(request.match_info["project_id"]) - yield from project.close() - pm.remove_project(project.id) + if ProjectHandler._notifications_listening.setdefault(project.id, 0) <= 1: + yield from project.close() + pm.remove_project(project.id) + del ProjectHandler._notifications_listening[project.id] + else: + log.warning("Skip project closing, another client is listening for project informations") response.set_status(204) @classmethod @@ -148,3 +178,111 @@ class ProjectHandler: yield from project.delete() pm.remove_project(project.id) response.set_status(204) + + @classmethod + @Route.get( + r"/projects/{project_id}/notifications", + description="Receive notifications about the projects", + parameters={ + "project_id": "The UUID of the project", + }, + status_codes={ + 200: "End of stream", + 404: "The project doesn't exist" + }) + def notification(request, response): + + pm = ProjectManager.instance() + project = pm.get_project(request.match_info["project_id"]) + + response.content_type = "application/json" + response.set_status(200) + response.enable_chunked_encoding() + # Very important: do not send a content lenght otherwise QT close the connection but curl can consume the Feed + response.content_length = None + + response.start(request) + queue = project.get_listen_queue() + ProjectHandler._notifications_listening.setdefault(project.id, 0) + ProjectHandler._notifications_listening[project.id] += 1 + response.write("{\"action\": \"ping\"}\n".encode("utf-8")) + while True: + try: + (action, msg) = yield from asyncio.wait_for(queue.get(), 5) + if hasattr(msg, "__json__"): + msg = json.dumps({"action": action, "event": msg.__json__()}, sort_keys=True) + else: + msg = json.dumps({"action": action, "event": msg}, sort_keys=True) + log.debug("Send notification: %s", msg) + response.write(("{}\n".format(msg)).encode("utf-8")) + except asyncio.futures.CancelledError as e: + break + except asyncio.futures.TimeoutError: + response.write("{\"action\": \"ping\"}\n".encode("utf-8")) + project.stop_listen_queue(queue) + if project.id in ProjectHandler._notifications_listening: + ProjectHandler._notifications_listening[project.id] -= 1 + + @classmethod + @Route.get( + r"/projects/{project_id}/files", + description="List files of a project", + parameters={ + "project_id": "The UUID of the project", + }, + status_codes={ + 200: "Return list of files", + 404: "The project doesn't exist" + }, + output=PROJECT_FILE_LIST_SCHEMA) + def list_files(request, response): + + pm = ProjectManager.instance() + project = pm.get_project(request.match_info["project_id"]) + files = yield from project.list_files() + response.json(files) + response.set_status(200) + + @classmethod + @Route.get( + r"/projects/{project_id}/files/{path:.+}", + description="Get a file of a project", + parameters={ + "project_id": "The UUID of the project", + }, + status_codes={ + 200: "Return the file", + 403: "Permission denied", + 404: "The file doesn't exist" + }) + def get_file(request, response): + + pm = ProjectManager.instance() + project = pm.get_project(request.match_info["project_id"]) + path = request.match_info["path"] + path = os.path.normpath(path) + + # Raise error if user try to escape + if path[0] == ".": + raise aiohttp.web.HTTPForbidden + path = os.path.join(project.path, path) + + response.content_type = "application/octet-stream" + response.set_status(200) + response.enable_chunked_encoding() + # Very important: do not send a content length otherwise QT close the connection but curl can consume the Feed + response.content_length = None + + try: + with open(path, "rb") as f: + response.start(request) + while True: + data = f.read(4096) + if not data: + break + yield from response.write(data) + + except FileNotFoundError: + raise aiohttp.web.HTTPNotFound() + except PermissionError: + raise aiohttp.web.HTTPForbidden diff --git a/gns3server/handlers/api/qemu_handler.py b/gns3server/handlers/api/qemu_handler.py index 1bc79976..a02f8693 100644 --- a/gns3server/handlers/api/qemu_handler.py +++ b/gns3server/handlers/api/qemu_handler.py @@ -15,14 +15,23 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import sys +import os.path + from aiohttp.web import HTTPConflict from ...web.route import Route +from ...modules.project_manager import ProjectManager from ...schemas.nio import NIO_SCHEMA 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_BINARY_FILTER_SCHEMA from ...schemas.qemu import QEMU_BINARY_LIST_SCHEMA +from ...schemas.qemu import QEMU_CAPABILITY_LIST_SCHEMA +from ...schemas.qemu import QEMU_IMAGE_CREATE_SCHEMA +from ...schemas.vm import VM_LIST_IMAGES_SCHEMA from ...modules.qemu import Qemu +from ...config import Config class QEMUHandler: @@ -50,9 +59,12 @@ class QEMUHandler: qemu = Qemu.instance() vm = yield from qemu.create_vm(request.json.pop("name"), request.match_info["project_id"], - request.json.get("vm_id"), - qemu_path=request.json.get("qemu_path"), - console=request.json.get("console")) + request.json.pop("vm_id", None), + linked_clone=request.json.get("linked_clone", True), + qemu_path=request.json.pop("qemu_path", None), + console=request.json.pop("console", None), + console_type=request.json.pop("console_type", "telnet"), + platform=request.json.pop("platform", None)) for name, value in request.json.items(): if hasattr(vm, name) and getattr(vm, name) != value: @@ -143,6 +155,11 @@ class QEMUHandler: qemu_manager = Qemu.instance() vm = qemu_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + if sys.platform.startswith("linux") and qemu_manager.config.get_section_config("Qemu").getboolean("enable_kvm", True) \ + and "-no-kvm" not in vm.options: + pm = ProjectManager.instance() + if pm.check_hardware_virtualization(vm) is False: + raise HTTPConflict(text="Cannot start VM with KVM enabled because hardware virtualization (VT-x/AMD-V) is already used by another software like VMware or VirtualBox") yield from vm.start() response.set_status(204) @@ -285,8 +302,88 @@ class QEMUHandler: 404: "Instance doesn't exist" }, description="Get a list of available Qemu binaries", + input=QEMU_BINARY_FILTER_SCHEMA, output=QEMU_BINARY_LIST_SCHEMA) def list_binaries(request, response): - binaries = yield from Qemu.binary_list() + binaries = yield from Qemu.binary_list(request.json.get("archs", None)) + response.json(binaries) + + @classmethod + @Route.get( + r"/qemu/img-binaries", + status_codes={ + 200: "Success", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Get a list of available Qemu-img binaries", + output=QEMU_BINARY_LIST_SCHEMA) + def list_img_binaries(request, response): + + binaries = yield from Qemu.img_binary_list() response.json(binaries) + + @Route.get( + r"/qemu/capabilities", + status_codes={ + 200: "Success" + }, + description="Get a list of Qemu capabilities on this server", + output=QEMU_CAPABILITY_LIST_SCHEMA + ) + def get_capabilities(request, response): + capabilities = {"kvm": []} + kvms = yield from Qemu.get_kvm_archs() + if kvms: + capabilities["kvm"] = kvms + response.json(capabilities) + + @classmethod + @Route.post( + r"/qemu/img", + status_codes={ + 201: "Image created", + }, + description="Create a Qemu image", + input=QEMU_IMAGE_CREATE_SCHEMA + ) + def create_img(request, response): + + qemu_img = request.json.pop("qemu_img") + path = request.json.pop("path") + if os.path.isabs(path): + config = Config.instance() + if config.get_section_config("Server").getboolean("local", False) is False: + response.set_status(403) + return + + yield from Qemu.instance().create_disk(qemu_img, path, request.json) + response.set_status(201) + + @Route.get( + r"/qemu/vms", + status_codes={ + 200: "List of Qemu images retrieved", + }, + description="Retrieve the list of Qemu images", + output=VM_LIST_IMAGES_SCHEMA) + def list_vms(request, response): + + qemu_manager = Qemu.instance() + vms = yield from qemu_manager.list_images() + response.set_status(200) + response.json(vms) + + @Route.post( + r"/qemu/vms/{path:.+}", + status_codes={ + 204: "Image uploaded", + }, + raw=True, + description="Upload Qemu image.") + def upload_vm(request, response): + + qemu_manager = Qemu.instance() + yield from qemu_manager.write_image(request.match_info["path"], request.content) + response.set_status(204) diff --git a/gns3server/handlers/api/virtualbox_handler.py b/gns3server/handlers/api/virtualbox_handler.py index 38274a3a..f93d4db3 100644 --- a/gns3server/handlers/api/virtualbox_handler.py +++ b/gns3server/handlers/api/virtualbox_handler.py @@ -16,6 +16,7 @@ # along with this program. If not, see . import os + from aiohttp.web import HTTPConflict from ...web.route import Route from ...schemas.nio import NIO_SCHEMA @@ -40,10 +41,10 @@ class VirtualBoxHandler: 200: "Success", }, description="Get all VirtualBox VMs available") - def show(request, response): + def index(request, response): vbox_manager = VirtualBox.instance() - vms = yield from vbox_manager.get_list() + vms = yield from vbox_manager.list_images() response.json(vms) @classmethod @@ -190,6 +191,10 @@ class VirtualBoxHandler: vbox_manager = VirtualBox.instance() vm = vbox_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + if (yield from vm.check_hw_virtualization()): + pm = ProjectManager.instance() + if pm.check_hardware_virtualization(vm) is False: + raise HTTPConflict(text="Cannot start VM because hardware virtualization (VT-x/AMD-V) is already used by another software like VMware or KVM (on Linux)") yield from vm.start() response.set_status(204) @@ -246,7 +251,7 @@ class VirtualBoxHandler: 404: "Instance doesn't exist" }, description="Resume a suspended VirtualBox VM instance") - def suspend(request, response): + def resume(request, response): vbox_manager = VirtualBox.instance() vm = vbox_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) diff --git a/gns3server/handlers/api/vmware_handler.py b/gns3server/handlers/api/vmware_handler.py new file mode 100644 index 00000000..8512d53d --- /dev/null +++ b/gns3server/handlers/api/vmware_handler.py @@ -0,0 +1,368 @@ +# -*- 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 ...schemas.vmware import VMWARE_CREATE_SCHEMA +from ...schemas.vmware import VMWARE_UPDATE_SCHEMA +from ...schemas.vmware import VMWARE_OBJECT_SCHEMA +from ...schemas.vmware import VMWARE_CAPTURE_SCHEMA +from ...schemas.nio import NIO_SCHEMA +from ...modules.vmware import VMware +from ...modules.project_manager import ProjectManager + + +class VMwareHandler: + + """ + API entry points for VMware. + """ + + @classmethod + @Route.get( + r"/vmware/vms", + status_codes={ + 200: "Success", + }, + description="Get all VMware VMs available") + def index(request, response): + + vmware_manager = VMware.instance() + vms = yield from vmware_manager.list_vms() + response.json(vms) + + @classmethod + @Route.post( + r"/projects/{project_id}/vmware/vms", + parameters={ + "project_id": "UUID for the project" + }, + status_codes={ + 201: "Instance created", + 400: "Invalid request", + 409: "Conflict" + }, + description="Create a new VMware VM instance", + input=VMWARE_CREATE_SCHEMA, + output=VMWARE_OBJECT_SCHEMA) + def create(request, response): + + vmware_manager = VMware.instance() + vm = yield from vmware_manager.create_vm(request.json.pop("name"), + request.match_info["project_id"], + request.json.get("vm_id"), + request.json.pop("vmx_path"), + request.json.pop("linked_clone"), + console=request.json.get("console", None)) + + for name, value in request.json.items(): + if name != "vm_id": + 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}/vmware/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 VMware VM instance", + output=VMWARE_OBJECT_SCHEMA) + def show(request, response): + + vmware_manager = VMware.instance() + vm = vmware_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}/vmware/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 VMware VM instance", + input=VMWARE_UPDATE_SCHEMA, + output=VMWARE_OBJECT_SCHEMA) + def update(request, response): + + vmware_manager = VMware.instance() + vm = vmware_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + + for name, value in request.json.items(): + if hasattr(vm, name) and getattr(vm, name) != value: + setattr(vm, name, value) + + response.json(vm) + + @classmethod + @Route.delete( + r"/projects/{project_id}/vmware/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 VMware VM instance") + def delete(request, response): + + # check the project_id exists + ProjectManager.instance().get_project(request.match_info["project_id"]) + yield from VMware.instance().delete_vm(request.match_info["vm_id"]) + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/vmware/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 VMware VM instance") + def start(request, response): + + vmware_manager = VMware.instance() + vm = vmware_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + if vm.check_hw_virtualization(): + pm = ProjectManager.instance() + if pm.check_hardware_virtualization(vm) is False: + raise HTTPConflict(text="Cannot start VM because hardware virtualization (VT-x/AMD-V) is already used by another software like VirtualBox or KVM (on Linux)") + yield from vm.start() + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/vmware/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 VMware VM instance") + def stop(request, response): + + vmware_manager = VMware.instance() + vm = vmware_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}/vmware/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 VMware VM instance") + def suspend(request, response): + + vmware_manager = VMware.instance() + vm = vmware_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}/vmware/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 VMware VM instance") + def resume(request, response): + + vmware_manager = VMware.instance() + vm = vmware_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}/vmware/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 VMware VM instance") + def reload(request, response): + + vmware_manager = VMware.instance() + vm = vmware_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}/vmware/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 VMware VM instance", + input=NIO_SCHEMA, + output=NIO_SCHEMA) + def create_nio(request, response): + + vmware_manager = VMware.instance() + vm = vmware_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + nio_type = request.json["type"] + if nio_type not in ("nio_udp", "nio_vmnet", "nio_nat"): + raise HTTPConflict(text="NIO of type {} is not supported".format(nio_type)) + nio = vmware_manager.create_nio(None, 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}/vmware/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 VMware VM instance") + def delete_nio(request, response): + + vmware_manager = VMware.instance() + vm = vmware_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}/vmware/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 VMware VM instance", + input=VMWARE_CAPTURE_SCHEMA) + def start_capture(request, response): + + vmware_manager = VMware.instance() + vm = vmware_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"]) + yield from vm.start_capture(adapter_number, pcap_file_path) + response.json({"pcap_file_path": pcap_file_path}) + + @Route.post( + r"/projects/{project_id}/vmware/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 VMware VM instance") + def stop_capture(request, response): + + vmware_manager = VMware.instance() + vm = vmware_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + adapter_number = int(request.match_info["adapter_number"]) + yield from vm.stop_capture(adapter_number) + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/vmware/vms/{vm_id}/interfaces/vmnet", + parameters={ + "project_id": "The UUID of the project", + "vm_id": "UUID for the instance", + }, + status_codes={ + 201: "VMnet interface allocated", + }, + description="Allocate a VMware VMnet interface on the server") + def allocate_vmnet(request, response): + + vmware_manager = VMware.instance() + vm = vmware_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + vmware_manager.refresh_vmnet_list(ubridge=False) + vmnet = vmware_manager.allocate_vmnet() + vm.vmnets.append(vmnet) + response.set_status(201) + response.json({"vmnet": vmnet}) diff --git a/gns3server/handlers/index_handler.py b/gns3server/handlers/index_handler.py index 1ed5d696..854ed556 100644 --- a/gns3server/handlers/index_handler.py +++ b/gns3server/handlers/index_handler.py @@ -15,8 +15,12 @@ # along with this program. If not, see . from ..web.route import Route +from ..modules.port_manager import PortManager +from ..modules.project_manager import ProjectManager + class IndexHandler: + @classmethod @Route.get( r"/", @@ -25,3 +29,15 @@ class IndexHandler: ) def index(request, response): response.template("index.html") + + @classmethod + @Route.get( + r"/status", + description="Ressources used by GNS3Server", + api_version=None + ) + def ports(request, response): + response.template("status.html", + port_manager=PortManager.instance(), + project_manager=ProjectManager.instance() + ) diff --git a/gns3server/handlers/upload_handler.py b/gns3server/handlers/upload_handler.py index add34b40..a0526950 100644 --- a/gns3server/handlers/upload_handler.py +++ b/gns3server/handlers/upload_handler.py @@ -24,6 +24,7 @@ import asyncio from ..config import Config from ..web.route import Route +from ..utils.images import remove_checksum, md5sum class UploadHandler: @@ -39,7 +40,7 @@ class UploadHandler: try: for root, _, files in os.walk(UploadHandler.image_directory()): for filename in files: - if not filename.startswith("."): + if not filename.startswith(".") and not filename.endswith(".md5sum"): image_file = os.path.join(root, filename) uploaded_files.append(image_file) except OSError: @@ -78,9 +79,14 @@ class UploadHandler: destination_dir = os.path.join(UploadHandler.image_directory(), data["type"]) destination_path = os.path.join(destination_dir, data["file"].filename) os.makedirs(destination_dir, exist_ok=True) + remove_checksum(destination_path) with open(destination_path, "wb+") as f: - chunk = data["file"].file.read() - f.write(chunk) + while True: + chunk = data["file"].file.read(512) + if not chunk: + break + f.write(chunk) + md5sum(destination_path) st = os.stat(destination_path) os.chmod(destination_path, st.st_mode | stat.S_IXUSR) except OSError as e: @@ -104,7 +110,7 @@ class UploadHandler: description="Backup GNS3 projects", api_version=None ) - def backup_images(request, response): + def backup_projects(request, response): yield from UploadHandler._backup_directory(request, response, UploadHandler.project_directory()) @staticmethod diff --git a/gns3server/main.py b/gns3server/main.py index 2d5b27cd..fc8e4d3f 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -16,6 +16,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +""" +Entry point of the server. It's support daemonize the process +""" # WARNING # Due to buggy user machines we choose to put this as the first loading modules @@ -25,122 +28,36 @@ # filesystem permissions, but it's a common mistake. import gns3server.utils.get_resource - import os -import datetime import sys -import locale -import argparse - -from gns3server.server import Server -from gns3server.web.logger import init_logger -from gns3server.version import __version__ -from gns3server.config import Config -from gns3server.modules.project import Project -from gns3server.crash_report import CrashReport - -import logging -log = logging.getLogger(__name__) -def locale_check(): +def daemonize(): """ - Checks if this application runs with a correct locale (i.e. supports UTF-8 encoding) and attempt to fix - if this is not the case. - - This is to prevent UnicodeEncodeError with unicode paths when using standard library I/O operation - methods (e.g. os.stat() or os.path.*) which rely on the system or user locale. - - More information can be found there: http://seasonofcode.com/posts/unicode-i-o-and-locales-in-python.html - or there: http://robjwells.com/post/61198832297/get-your-us-ascii-out-of-my-face + Do the UNIX double-fork magic for properly detaching process """ - - # no need to check on Windows or when this application is frozen - if sys.platform.startswith("win") or hasattr(sys, "frozen"): - return - - language = encoding = None try: - language, encoding = locale.getlocale() - except ValueError as 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...") - 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)) - raise SystemExit - elif encoding != "UTF-8": - 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)) - raise SystemExit - else: - log.info("Current locale is {}.{}".format(language, encoding)) - - -def parse_arguments(argv, config): - """ - Parse command line arguments and override local configuration - - :params args: Array of command line arguments - :params config: ConfigParser with default variable from configuration - """ - - defaults = { - "host": config.get("host", "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), - "logfile": config.getboolean("logfile", ""), - } - - 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)") - parser.add_argument("--log", help="send output to logfile instead of console") - - return parser.parse_args(argv) - + pid = os.fork() + if pid > 0: + # Exit first parent + sys.exit(0) + except OSError as e: + print("First fork failed: %d (%s)\n" % (e.errno, e.strerror), file=sys.stderr) + sys.exit(1) -def set_config(args): + # Decouple from parent environment + os.setsid() + os.umask(0o007) - 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) + # Do second fork + try: + pid = os.fork() + if pid > 0: + # Exit from second parent + sys.exit(0) + except OSError as e: + print("Second fork failed: %d (%s)\n" % (e.errno, e.strerror), file=sys.stderr) + sys.exit(1) def main(): @@ -148,63 +65,12 @@ def main(): Entry point for GNS3 server """ - 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, logfile=args.log, 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") - - user_log.info("Running with Python {major}.{minor}.{micro} and has PID {pid}".format( - 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) - locale_check() + if not sys.platform.startswith("win"): + if "--daemon" in sys.argv: + daemonize() + from gns3server.run import run + run() - try: - os.getcwd() - except FileNotFoundError: - log.critical("The current working directory doesn't exist") - return - - Project.clean_project_directory() - - CrashReport.instance() - - try: - host = server_config["host"].encode("idna").decode() - except UnicodeError: - log.critical("Invalid hostname %s", server_config["host"]) - return - - port = int(server_config["port"]) - server = Server.instance(host, port) - try: - server.run() - except OSError as e: - # This is to ignore OSError: [WinError 0] The operation completed successfully exception on Windows. - if not sys.platform.startswith("win") and not e.winerror == 0: - raise - 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/modules/__init__.py b/gns3server/modules/__init__.py index 928bb1a9..7c23503c 100644 --- a/gns3server/modules/__init__.py +++ b/gns3server/modules/__init__.py @@ -21,8 +21,9 @@ from .vpcs import VPCS from .virtualbox import VirtualBox from .dynamips import Dynamips from .qemu import Qemu +from .vmware import VMware -MODULES = [VPCS, VirtualBox, Dynamips, Qemu] +MODULES = [VPCS, VirtualBox, Dynamips, Qemu, VMware] if sys.platform.startswith("linux") or hasattr(sys, "_called_from_test") or os.environ.get("PYTEST_BUILD_DOCUMENTATION") == "1": diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index ad12a784..b8e2512a 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -23,19 +23,24 @@ import asyncio import aiohttp import socket import shutil +import re import logging log = logging.getLogger(__name__) from uuid import UUID, uuid4 +from gns3server.utils.interfaces import is_interface_up from ..config import Config from ..utils.asyncio import wait_run_in_executor +from ..utils import force_unix_path from .project_manager import ProjectManager from .nios.nio_udp import NIOUDP from .nios.nio_tap import NIOTAP from .nios.nio_nat import NIONAT from .nios.nio_generic_ethernet import NIOGenericEthernet +from ..utils.images import md5sum, remove_checksum +from .vm_error import VMError class BaseManager: @@ -310,7 +315,7 @@ class BaseManager: return vm @staticmethod - def _has_privileged_access(executable): + def has_privileged_access(executable): """ Check if an executable can access Ethernet and TAP devices in RAW mode. @@ -327,19 +332,20 @@ class BaseManager: 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: + + if os.stat(executable).st_uid == 0 and (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: + try: + if sys.platform.startswith("linux") and "security.capability" in os.listxattr(executable): caps = os.getxattr(executable, "security.capability") # test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set if struct.unpack("= 0.9.2, detected version is {}".format(self._ubridge_hypervisor.version)) + + @property + def hw_virtualization(self): + """ + Returns either the VM is using hardware virtualization or not. + + :return: boolean + """ + + return self._hw_virtualization + + def check_available_ram(self, requested_ram): + """ + Sends a warning notification if there is not enough RAM on the system to allocate requested RAM. + + :param requested_ram: requested amount of RAM in MB + """ + + available_ram = int(psutil.virtual_memory().available / (1024 * 1024)) + percentage_left = psutil.virtual_memory().percent + if requested_ram > available_ram: + message = '"{}" requires {}MB of RAM to run but there is only {}MB - {}% of RAM left on "{}"'.format(self.name, + requested_ram, + available_ram, + percentage_left, + platform.node()) + self.project.emit("log.warning", {"message": message}) diff --git a/gns3server/modules/docker/__init__.py b/gns3server/modules/docker/__init__.py new file mode 100644 index 00000000..c20a4efb --- /dev/null +++ b/gns3server/modules/docker/__init__.py @@ -0,0 +1,119 @@ +# -*- 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 . + +""" +Docker server module. +""" + +import asyncio +import logging +import aiohttp +import docker +from requests.exceptions import ConnectionError + +log = logging.getLogger(__name__) + +from ..base_manager import BaseManager +from ..project_manager import ProjectManager +from .docker_vm import Container +from .docker_error import DockerError + + +class Docker(BaseManager): + + _VM_CLASS = Container + + def __init__(self): + super().__init__() + # FIXME: make configurable and start docker before trying + self._server_url = 'unix://var/run/docker.sock' + self._client = docker.Client(base_url=self._server_url) + self._execute_lock = asyncio.Lock() + + @property + def server_url(self): + """Returns the Docker server url. + + :returns: url + :rtype: string + """ + return self._server_url + + @server_url.setter + def server_url(self, value): + self._server_url = value + self._client = docker.Client(base_url=value) + + @asyncio.coroutine + def execute(self, command, kwargs, timeout=60): + command = getattr(self._client, command) + log.debug("Executing Docker with command: {}".format(command)) + try: + result = command(**kwargs) + except Exception as error: + raise DockerError("Docker has returned an error: {}".format(error)) + return result + + @asyncio.coroutine + def list_images(self): + """Gets Docker image list. + + :returns: list of dicts + :rtype: list + """ + images = [] + try: + for image in self._client.images(): + for tag in image['RepoTags']: + images.append({'imagename': tag}) + return images + except ConnectionError as error: + raise DockerError( + """Docker couldn't list images and returned an error: {} +Is the Docker service running?""".format(error)) + + @asyncio.coroutine + def list_containers(self): + """Gets Docker container list. + + :returns: list of dicts + :rtype: list + """ + return self._client.containers() + + def get_container(self, cid, project_id=None): + """Returns a Docker container. + + :param id: Docker container identifier + :param project_id: Project identifier + + :returns: Docker container + """ + if project_id: + project = ProjectManager.instance().get_project(project_id) + + if cid not in self._vms: + raise aiohttp.web.HTTPNotFound( + text="Docker container with ID {} doesn't exist".format(cid)) + + container = self._vms[cid] + if project_id: + if container.project.id != project.id: + raise aiohttp.web.HTTPNotFound( + text="Project ID {} doesn't belong to container {}".format( + project_id, container.name)) + return container diff --git a/gns3server/modules/docker/docker_error.py b/gns3server/modules/docker/docker_error.py new file mode 100644 index 00000000..2e03ace5 --- /dev/null +++ b/gns3server/modules/docker/docker_error.py @@ -0,0 +1,26 @@ +# -*- 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 . + +""" +Custom exceptions for the Docker module. +""" + +from ..vm_error import VMError + + +class DockerError(VMError): + pass diff --git a/gns3server/modules/docker/docker_vm.py b/gns3server/modules/docker/docker_vm.py new file mode 100644 index 00000000..517e8126 --- /dev/null +++ b/gns3server/modules/docker/docker_vm.py @@ -0,0 +1,403 @@ +# -*- 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 . + +""" +Docker container instance. +""" + +import asyncio +import shutil +import psutil + +from docker.utils import create_host_config +from gns3server.ubridge.hypervisor import Hypervisor +from pkg_resources import parse_version +from .docker_error import DockerError +from ..base_vm import BaseVM +from ..adapters.ethernet_adapter import EthernetAdapter +from ..nios.nio_udp import NIOUDP + +import logging +log = logging.getLogger(__name__) + + +class Container(BaseVM): + """Docker container implementation. + + :param name: Docker container name + :param vm_id: Docker VM identifier + :param project: Project instance + :param manager: Manager instance + :param image: Docker image + """ + + def __init__(self, name, vm_id, project, manager, image, startcmd=None): + self._name = name + self._id = vm_id + self._project = project + self._manager = manager + self._image = image + self._startcmd = startcmd + self._veths = [] + self._ethernet_adapters = [] + self._ubridge_hypervisor = None + self._temporary_directory = None + self._hw_virtualization = False + + log.debug( + "{module}: {name} [{image}] initialized.".format( + module=self.manager.module_name, + name=self.name, + image=self._image)) + + def __json__(self): + return { + "name": self._name, + "vm_id": self._id, + "cid": self._cid, + "project_id": self._project.id, + "image": self._image, + } + + @property + def veths(self): + """Returns Docker host veth interfaces.""" + return self._veths + + @asyncio.coroutine + def _get_container_state(self): + """Returns the container state (e.g. running, paused etc.) + + :returns: state + :rtype: str + """ + try: + result = yield from self.manager.execute( + "inspect_container", {"container": self._cid}) + result_dict = {state.lower(): value for state, value in result["State"].items()} + for state, value in result_dict.items(): + if value is True: + # a container can be both paused and running + if state == "paused": + return "paused" + if state == "running": + if "paused" in result_dict and result_dict["paused"] is True: + return "paused" + return state.lower() + return 'exited' + except Exception as err: + raise DockerError("Could not get container state for {0}: ".format( + self._name), str(err)) + + @asyncio.coroutine + def create(self): + """Creates the Docker container.""" + params = { + "name": self._name, + "image": self._image, + "network_disabled": True, + "host_config": create_host_config( + privileged=True, cap_add=['ALL']) + } + if self._startcmd: + params.update({'command': self._startcmd}) + + result = yield from self.manager.execute("create_container", params) + self._cid = result['Id'] + log.info("Docker container '{name}' [{id}] created".format( + name=self._name, id=self._id)) + return True + + @property + def ubridge_path(self): + """Returns the uBridge executable path. + + :returns: path to uBridge + """ + path = self._manager.config.get_section_config("Server").get( + "ubridge_path", "ubridge") + if path == "ubridge": + path = shutil.which("ubridge") + return path + + @asyncio.coroutine + def _start_ubridge(self): + """Starts uBridge (handles connections to and from this Docker VM).""" + server_config = self._manager.config.get_section_config("Server") + server_host = server_config.get("host") + self._ubridge_hypervisor = Hypervisor( + self._project, self.ubridge_path, self.working_dir, server_host) + + log.info("Starting new uBridge hypervisor {}:{}".format( + self._ubridge_hypervisor.host, self._ubridge_hypervisor.port)) + yield from self._ubridge_hypervisor.start() + log.info("Hypervisor {}:{} has successfully started".format( + self._ubridge_hypervisor.host, self._ubridge_hypervisor.port)) + yield from self._ubridge_hypervisor.connect() + if parse_version( + self._ubridge_hypervisor.version) < parse_version('0.9.1'): + raise DockerError( + "uBridge version must be >= 0.9.1, detected version is {}".format( + self._ubridge_hypervisor.version)) + + @asyncio.coroutine + def start(self): + """Starts this Docker container.""" + + state = yield from self._get_container_state() + if state == "paused": + yield from self.unpause() + else: + result = yield from self.manager.execute( + "start", {"container": self._cid}) + + yield from self._start_ubridge() + for adapter_number in range(0, self.adapters): + nio = self._ethernet_adapters[adapter_number].get_nio(0) + if nio: + yield from self._add_ubridge_connection(nio, adapter_number) + + log.info("Docker container '{name}' [{image}] started".format( + name=self._name, image=self._image)) + + def is_running(self): + """Checks if the container is running. + + :returns: True or False + :rtype: bool + """ + state = yield from self._get_container_state() + if state == "running": + return True + return False + + @asyncio.coroutine + def restart(self): + """Restarts this Docker container.""" + result = yield from self.manager.execute( + "restart", {"container": self._cid}) + log.info("Docker container '{name}' [{image}] restarted".format( + name=self._name, image=self._image)) + + @asyncio.coroutine + def stop(self): + """Stops this Docker container.""" + + if self._ubridge_hypervisor and self._ubridge_hypervisor.is_running(): + yield from self._ubridge_hypervisor.stop() + + state = yield from self._get_container_state() + if state == "paused": + yield from self.unpause() + result = yield from self.manager.execute( + "kill", {"container": self._cid}) + log.info("Docker container '{name}' [{image}] stopped".format( + name=self._name, image=self._image)) + + @asyncio.coroutine + def pause(self): + """Pauses this Docker container.""" + result = yield from self.manager.execute( + "pause", {"container": self._cid}) + log.info("Docker container '{name}' [{image}] paused".format( + name=self._name, image=self._image)) + + @asyncio.coroutine + def unpause(self): + """Unpauses this Docker container.""" + result = yield from self.manager.execute( + "unpause", {"container": self._cid}) + state = yield from self._get_container_state() + log.info("Docker container '{name}' [{image}] unpaused".format( + name=self._name, image=self._image)) + + @asyncio.coroutine + def remove(self): + """Removes this Docker container.""" + state = yield from self._get_container_state() + if state == "paused": + yield from self.unpause() + if state == "running": + yield from self.stop() + result = yield from self.manager.execute( + "remove_container", {"container": self._cid, "force": True}) + log.info("Docker container '{name}' [{image}] removed".format( + name=self._name, image=self._image)) + + @asyncio.coroutine + def close(self): + """Closes this Docker container.""" + log.debug("Docker container '{name}' [{id}] is closing".format( + name=self.name, id=self._cid)) + for adapter in self._ethernet_adapters.values(): + 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.remove() + + log.info("Docker container '{name}' [{id}] closed".format( + name=self.name, id=self._cid)) + self._closed = True + + def _add_ubridge_connection(self, nio, adapter_number): + """ + Creates a connection in uBridge. + + :param nio: NIO instance + :param adapter_number: adapter number + """ + try: + adapter = self._ethernet_adapters[adapter_number] + except IndexError: + raise DockerError( + "Adapter {adapter_number} doesn't exist on Docker container '{name}'".format( + name=self.name, adapter_number=adapter_number)) + + if nio and isinstance(nio, NIOUDP): + for index in range(128): + if "gns3-veth{}ext".format(index) not in psutil.net_if_addrs(): + adapter.ifc = "eth{}".format(str(index)) + adapter.host_ifc = "gns3-veth{}ext".format(str(index)) + adapter.guest_ifc = "gns3-veth{}int".format(str(index)) + break + if not hasattr(adapter, "ifc"): + raise DockerError( + "Adapter {adapter_number} couldn't allocate interface on Docker container '{name}'".format( + name=self.name, adapter_number=adapter_number)) + + yield from self._ubridge_hypervisor.send( + 'docker create_veth {hostif} {guestif}'.format( + guestif=adapter.guest_ifc, hostif=adapter.host_ifc)) + self._veths.append(adapter.host_ifc) + + namespace = yield from self.get_namespace() + yield from self._ubridge_hypervisor.send( + 'docker move_to_ns {ifc} {ns}'.format( + ifc=adapter.guest_ifc, ns=namespace)) + + yield from self._ubridge_hypervisor.send( + 'bridge create bridge{}'.format(adapter_number)) + yield from self._ubridge_hypervisor.send( + 'bridge add_nio_linux_raw bridge{adapter} {ifc}'.format( + ifc=adapter.host_ifc, adapter=adapter_number)) + + if isinstance(nio, NIOUDP): + yield from self._ubridge_hypervisor.send( + 'bridge add_nio_udp bridge{adapter} {lport} {rhost} {rport}'.format( + adapter=adapter_number, lport=nio.lport, rhost=nio.rhost, + rport=nio.rport)) + + if nio.capturing: + yield from self._ubridge_hypervisor.send( + 'bridge start_capture bridge{adapter} "{pcap_file}"'.format( + adapter=adapter_number, pcap_file=nio.pcap_output_file)) + + yield from self._ubridge_hypervisor.send( + 'bridge start bridge{adapter}'.format(adapter=adapter_number)) + + def _delete_ubridge_connection(self, adapter_number): + """Deletes a connection in uBridge. + + :param adapter_number: adapter number + """ + yield from self._ubridge_hypervisor.send("bridge delete bridge{name}".format( + name=adapter_number)) + + adapter = self._ethernet_adapters[adapter_number] + yield from self._ubridge_hypervisor.send("docker delete_veth {name}".format( + name=adapter.host_ifc)) + + def adapter_add_nio_binding(self, adapter_number, nio): + """Adds an adapter NIO binding. + + :param adapter_number: adapter number + :param nio: NIO instance to add to the slot/port + """ + try: + adapter = self._ethernet_adapters[adapter_number] + except IndexError: + raise DockerError( + "Adapter {adapter_number} doesn't exist on Docker container '{name}'".format( + name=self.name, adapter_number=adapter_number)) + + adapter.add_nio(0, nio) + log.info( + "Docker container '{name}' [{id}]: {nio} added to adapter {adapter_number}".format( + name=self.name, + id=self._id, + nio=nio, + adapter_number=adapter_number)) + + def adapter_remove_nio_binding(self, adapter_number): + """ + Removes an adapter NIO binding. + + :param adapter_number: adapter number + + :returns: NIO instance + """ + try: + adapter = self._ethernet_adapters[adapter_number] + except IndexError: + raise DockerError( + "Adapter {adapter_number} doesn't exist on Docker VM '{name}'".format( + name=self.name, adapter_number=adapter_number)) + + adapter.remove_nio(0) + try: + yield from self._delete_ubridge_connection(adapter_number) + except: + pass + + log.info( + "Docker VM '{name}' [{id}]: {nio} removed from adapter {adapter_number}".format( + name=self.name, id=self.id, nio=adapter.host_ifc, + adapter_number=adapter_number)) + + @property + def adapters(self): + """Returns the number of Ethernet adapters for this Docker VM. + + :returns: number of adapters + :rtype: int + """ + return len(self._ethernet_adapters) + + @adapters.setter + def adapters(self, adapters): + """Sets the number of Ethernet adapters for this Docker container. + + :param adapters: number of adapters + """ + + self._ethernet_adapters.clear() + for adapter_number in range(0, adapters): + self._ethernet_adapters.append(EthernetAdapter()) + + log.info( + 'Docker container "{name}" [{id}]: number of Ethernet adapters changed to {adapters}'.format( + name=self._name, + id=self._id, + adapters=adapters)) + + def get_namespace(self): + result = yield from self.manager.execute( + "inspect_container", {"container": self._cid}) + return int(result['State']['Pid']) diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index f69d6641..50ed4522 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -32,9 +32,8 @@ import glob log = logging.getLogger(__name__) -from gns3server.utils.interfaces import get_windows_interfaces +from gns3server.utils.interfaces import get_windows_interfaces, is_interface_up from gns3server.utils.asyncio import wait_run_in_executor -from gns3server.utils.glob import glob_escape from pkg_resources import parse_version from uuid import UUID, uuid4 from ..base_manager import BaseManager @@ -53,8 +52,6 @@ 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 @@ -205,11 +202,12 @@ class Dynamips(BaseManager): 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(glob_escape(project_dir), "*.ghost")) - files += glob.glob(os.path.join(glob_escape(project_dir), "*_lock")) - files += glob.glob(os.path.join(glob_escape(project_dir), "ilt_*")) - files += glob.glob(os.path.join(glob_escape(project_dir), "c[0-9][0-9][0-9][0-9]_i[0-9]*_rommon_vars")) - files += glob.glob(os.path.join(glob_escape(project_dir), "c[0-9][0-9][0-9][0-9]_i[0-9]*_log.txt")) + + files = glob.glob(os.path.join(glob.escape(project_dir), "*.ghost")) + files += glob.glob(os.path.join(glob.escape(project_dir), "*_lock")) + files += glob.glob(os.path.join(glob.escape(project_dir), "ilt_*")) + files += glob.glob(os.path.join(glob.escape(project_dir), "c[0-9][0-9][0-9][0-9]_i[0-9]*_rommon_vars")) + files += glob.glob(os.path.join(glob.escape(project_dir), "c[0-9][0-9][0-9][0-9]_i[0-9]*_log.txt")) for file in files: try: log.debug("Deleting file {}".format(file)) @@ -374,10 +372,16 @@ class Dynamips(BaseManager): server_host = server_config.get("host") try: - # let the OS find an unused port for the Dynamips hypervisor - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: - sock.bind((server_host, 0)) - port = sock.getsockname()[1] + info = socket.getaddrinfo(server_host, 0, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE) + if not info: + raise DynamipsError("getaddrinfo returns an empty list on {}".format(server_host)) + for res in info: + af, socktype, proto, _, sa = res + # let the OS find an unused port for the Dynamips hypervisor + with socket.socket(af, socktype, proto) as sock: + sock.bind(sa) + port = sock.getsockname()[1] + break except OSError as e: raise DynamipsError("Could not find free port for the Dynamips hypervisor: {}".format(e)) @@ -421,9 +425,13 @@ class Dynamips(BaseManager): rhost = nio_settings["rhost"] rport = nio_settings["rport"] try: - # TODO: handle IPv6 - with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: - sock.connect((rhost, rport)) + info = socket.getaddrinfo(rhost, rport, socket.AF_UNSPEC, socket.SOCK_DGRAM, 0, socket.AI_PASSIVE) + if not info: + raise DynamipsError("getaddrinfo returns an empty list on {}:{}".format(rhost, rport)) + for res in info: + af, socktype, proto, _, sa = res + with socket.socket(af, socktype, proto) as sock: + sock.connect(sa) except OSError as e: raise DynamipsError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e)) nio = NIOUDP(node.hypervisor, lport, rhost, rport) @@ -440,6 +448,8 @@ class Dynamips(BaseManager): raise DynamipsError("Could not find interface {} on this host".format(ethernet_device)) else: ethernet_device = npf_interface + if not is_interface_up(ethernet_device): + raise aiohttp.web.HTTPConflict(text="Ethernet interface {} is down".format(ethernet_device)) nio = NIOGenericEthernet(node.hypervisor, ethernet_device) elif nio_settings["type"] == "nio_linux_ethernet": if sys.platform.startswith("win"): @@ -448,6 +458,8 @@ class Dynamips(BaseManager): nio = NIOLinuxEthernet(node.hypervisor, ethernet_device) elif nio_settings["type"] == "nio_tap": tap_device = nio_settings["tap_device"] + if not is_interface_up(tap_device): + raise aiohttp.web.HTTPConflict(text="TAP interface {} is down".format(tap_device)) nio = NIOTAP(node.hypervisor, tap_device) elif nio_settings["type"] == "nio_unix": local_file = nio_settings["local_file"] @@ -683,3 +695,34 @@ class Dynamips(BaseManager): Return the full path of the images directory on disk """ return os.path.join(os.path.expanduser(self.config.get_section_config("Server").get("images_path", "~/GNS3/images")), "IOS") + + @asyncio.coroutine + def list_images(self): + """ + Return the list of available IOS images. + + :returns: Array of hash + """ + + image_dir = self.get_images_directory() + try: + files = os.listdir(image_dir) + except FileNotFoundError: + return [] + files.sort() + images = [] + for filename in files: + if filename[0] != "." and not filename.endswith(".md5sum"): + try: + path = os.path.join(image_dir, filename) + with open(path, "rb") as f: + # read the first 7 bytes of the file. + elf_header_start = f.read(7) + except OSError as e: + print(e) + continue + # valid 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': + path = os.path.relpath(path, image_dir) + images.append({"filename": filename, "path": path}) + return images diff --git a/gns3server/modules/dynamips/dynamips_hypervisor.py b/gns3server/modules/dynamips/dynamips_hypervisor.py index cc01cf8d..ba3afb2c 100644 --- a/gns3server/modules/dynamips/dynamips_hypervisor.py +++ b/gns3server/modules/dynamips/dynamips_hypervisor.py @@ -81,8 +81,8 @@ class DynamipsHypervisor: while time.time() - begin < timeout: yield from asyncio.sleep(0.01) try: - self._reader, self._writer = yield from asyncio.open_connection(host, self._port) - except OSError as e: + self._reader, self._writer = yield from asyncio.wait_for(asyncio.open_connection(host, self._port), timeout=1) + except (asyncio.TimeoutError, OSError) as e: last_exception = e continue connection_success = True @@ -276,7 +276,7 @@ class DynamipsHypervisor: while True: try: try: - #line = yield from self._reader.readline() # this can lead to ValueError: Line is too long + # line = yield from self._reader.readline() # this can lead to ValueError: Line is too long chunk = yield from self._reader.read(1024) # match to Dynamips' buffer size except asyncio.CancelledError: # task has been canceled but continue to read diff --git a/gns3server/modules/dynamips/hypervisor.py b/gns3server/modules/dynamips/hypervisor.py index caac0148..127e9239 100644 --- a/gns3server/modules/dynamips/hypervisor.py +++ b/gns3server/modules/dynamips/hypervisor.py @@ -24,6 +24,7 @@ import subprocess import tempfile import asyncio +from gns3server.utils.asyncio import wait_for_process_termination from .dynamips_hypervisor import DynamipsHypervisor from .dynamips_error import DynamipsError @@ -70,6 +71,16 @@ class Hypervisor(DynamipsHypervisor): return self._id + @property + def process(self): + """ + Returns the subprocess of the Hypervisor + + :returns: subprocess + """ + + return self._process + @property def started(self): """ @@ -136,7 +147,7 @@ class Hypervisor(DynamipsHypervisor): # time to delete UNIX NIOs for instance. yield from asyncio.sleep(0.01) try: - yield from asyncio.wait_for(self._process.wait(), timeout=3) + yield from wait_for_process_termination(self._process, timeout=3) except asyncio.TimeoutError: if self._process.returncode is None: log.warn("Dynamips process {} is still running... killing it".format(self._process.pid)) diff --git a/gns3server/modules/dynamips/nios/nio_fifo.py b/gns3server/modules/dynamips/nios/nio_fifo.py deleted file mode 100644 index 9e7695a2..00000000 --- a/gns3server/modules/dynamips/nios/nio_fifo.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 . - -""" -Interface for FIFO NIOs. -""" - -import asyncio -import uuid -from .nio import NIO - -import logging -log = logging.getLogger(__name__) - - -class NIOFIFO(NIO): - - """ - Dynamips FIFO NIO. - - :param hypervisor: Dynamips hypervisor instance - """ - - def __init__(self, hypervisor): - - # create an unique name - name = 'fifo-{}'.format(uuid.uuid4()) - super().__init__(name, hypervisor) - - @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. - - :param nio: FIFO NIO instance - """ - - 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_mcast.py b/gns3server/modules/dynamips/nios/nio_mcast.py deleted file mode 100644 index 4ea47dc2..00000000 --- a/gns3server/modules/dynamips/nios/nio_mcast.py +++ /dev/null @@ -1,105 +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 multicast NIOs. -""" - -import asyncio -import uuid -from .nio import NIO - -import logging -log = logging.getLogger(__name__) - - -class NIOMcast(NIO): - - """ - Dynamips Linux Ethernet NIO. - - :param hypervisor: Dynamips hypervisor instance - :param group: multicast group to bind - :param port: port for binding - """ - - def __init__(self, hypervisor, group, port): - - # create an unique name - name = 'mcast-{}'.format(uuid.uuid4()) - self._group = group - self._port = port - self._ttl = 1 # default TTL - super().__init__(name, hypervisor) - - @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): - """ - Returns the multicast group - - :returns: multicast group address - """ - - return self._group - - @property - def port(self): - """ - Returns the port - - :returns: port number - """ - - return self._port - - @property - def ttl(self): - """ - Returns the TTL associated with the multicast address. - - :returns: TTL value - """ - - return self._ttl - - def set_ttl(self, ttl): - """ - Sets the TTL for the multicast address - - :param ttl: TTL value - """ - - 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/nodes/ethernet_switch.py b/gns3server/modules/dynamips/nodes/ethernet_switch.py index 2f8f0d87..71ab9f80 100644 --- a/gns3server/modules/dynamips/nodes/ethernet_switch.py +++ b/gns3server/modules/dynamips/nodes/ethernet_switch.py @@ -21,12 +21,12 @@ http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L558 """ import asyncio +from pkg_resources import parse_version from .device import Device from ..nios.nio_udp import NIOUDP from ..dynamips_error import DynamipsError - import logging log = logging.getLogger(__name__) @@ -59,7 +59,8 @@ class EthernetSwitch(Device): for port_number, settings in self._mappings.items(): ports.append({"port": port_number, "type": settings[0], - "vlan": settings[1]}) + "vlan": settings[1], + "ethertype": settings[2] if len(settings) > 2 else ""}) ethernet_switch_info["ports"] = ports return ethernet_switch_info @@ -192,7 +193,7 @@ class EthernetSwitch(Device): 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"]) + yield from self.set_qinq_port(port_number, settings["vlan"], settings["ethertype"]) @asyncio.coroutine def set_access_port(self, port_number, vlan_id): @@ -242,7 +243,7 @@ class EthernetSwitch(Device): self._mappings[port_number] = ("dot1q", native_vlan) @asyncio.coroutine - def set_qinq_port(self, port_number, outer_vlan): + def set_qinq_port(self, port_number, outer_vlan, ethertype): """ Sets the specified port as a trunk (QinQ) port. @@ -254,15 +255,20 @@ class EthernetSwitch(Device): raise DynamipsError("Port {} is not allocated".format(port_number)) nio = self._nios[port_number] - yield from self._hypervisor.send('ethsw set_qinq_port "{name}" {nio} {outer_vlan}'.format(name=self._name, - nio=nio, - outer_vlan=outer_vlan)) - - log.info('Ethernet switch "{name}" [{id}]: port {port} set as a QinQ port with outer VLAN {vlan_id}'.format(name=self._name, - id=self._id, - port=port_number, - vlan_id=outer_vlan)) - self._mappings[port_number] = ("qinq", outer_vlan) + if ethertype != "0x8100" and parse_version(self.hypervisor.version) < parse_version('0.2.16'): + raise DynamipsError("Dynamips version required is >= 0.2.16 to change the default QinQ Ethernet type, detected version is {}".format(self.hypervisor.version)) + + yield from self._hypervisor.send('ethsw set_qinq_port "{name}" {nio} {outer_vlan} {ethertype}'.format(name=self._name, + nio=nio, + outer_vlan=outer_vlan, + ethertype=ethertype if ethertype != "0x8100" else "")) + + log.info('Ethernet switch "{name}" [{id}]: port {port} set as a QinQ ({ethertype}) port with outer VLAN {vlan_id}'.format(name=self._name, + id=self._id, + port=port_number, + vlan_id=outer_vlan, + ethertype=ethertype)) + self._mappings[port_number] = ("qinq", outer_vlan, ethertype) @asyncio.coroutine def get_mac_addr_table(self): diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index a8e61a6b..59e40b8e 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -34,9 +34,9 @@ log = logging.getLogger(__name__) from ...base_vm import BaseVM from ..dynamips_error import DynamipsError from ..nios.nio_udp import NIOUDP -from ....utils.glob import glob_escape -from gns3server.utils.asyncio import wait_run_in_executor +from gns3server.utils.asyncio import wait_run_in_executor, monitor_process +from gns3server.utils.images import md5sum class Router(BaseVM): @@ -120,10 +120,12 @@ class Router(BaseVM): router_info = {"name": self.name, "vm_id": self.id, + "vm_directory": os.path.join(self.project.module_working_directory(self.manager.module_name.lower())), "project_id": self.project.id, "dynamips_id": self._dynamips_id, "platform": self._platform, "image": self._image, + "image_md5sum": md5sum(self._image), "startup_config": self._startup_config, "private_config": self._private_config, "ram": self._ram, @@ -155,7 +157,7 @@ class Router(BaseVM): slot_number += 1 # add the wics - if self._slots[0] and self._slots[0].wics: + if len(self._slots) > 0 and self._slots[0] and self._slots[0].wics: for wic_slot_number in range(0, len(self._slots[0].wics)): if self._slots[0].wics[wic_slot_number]: router_info["wic" + str(wic_slot_number)] = str(self._slots[0].wics[wic_slot_number]) @@ -245,8 +247,28 @@ class Router(BaseVM): if elf_header_start != b'\x7fELF\x01\x02\x01': raise DynamipsError('"{}" is not a valid IOS image'.format(self._image)) + # check if there is enough RAM to run + if not self._ghost_flag: + self.check_available_ram(self.ram) + yield from self._hypervisor.send('vm start "{name}"'.format(name=self._name)) + self.status = "started" log.info('router "{name}" [{id}] has been started'.format(name=self._name, id=self._id)) + monitor_process(self._hypervisor.process, self._termination_callback) + + @asyncio.coroutine + def _termination_callback(self, returncode): + """ + Called when the process has stopped. + + :param returncode: Process returncode + """ + + if self.status == "started": + self.status = "stopped" + log.info("Dynamips hypervisor process has stopped, return code: %d", returncode) + if returncode != 0: + self.project.emit("log.error", {"message": "Dynamips hypervisor process has stopped, return code: {}\n{}".format(returncode, self._hypervisor.read_stdout())}) @asyncio.coroutine def stop(self): @@ -257,6 +279,7 @@ class Router(BaseVM): status = yield from self.get_status() if status != "inactive": yield from self._hypervisor.send('vm stop "{name}"'.format(name=self._name)) + self.status = "stopped" log.info('Router "{name}" [{id}] has been stopped'.format(name=self._name, id=self._id)) @asyncio.coroutine @@ -338,13 +361,13 @@ class Router(BaseVM): if self._auto_delete_disks: # delete nvram and disk files project_dir = os.path.join(self.project.module_working_directory(self.manager.module_name.lower())) - files = glob.glob(os.path.join(glob_escape(project_dir), "{}_i{}_disk[0-1]".format(self.platform, self.dynamips_id))) - files += glob.glob(os.path.join(glob_escape(project_dir), "{}_i{}_slot[0-1]".format(self.platform, self.dynamips_id))) - files += glob.glob(os.path.join(glob_escape(project_dir), "{}_i{}_nvram".format(self.platform, self.dynamips_id))) - files += glob.glob(os.path.join(glob_escape(project_dir), "{}_i{}_flash[0-1]".format(self.platform, self.dynamips_id))) - files += glob.glob(os.path.join(glob_escape(project_dir), "{}_i{}_rom".format(self.platform, self.dynamips_id))) - files += glob.glob(os.path.join(glob_escape(project_dir), "{}_i{}_bootflash".format(self.platform, self.dynamips_id))) - files += glob.glob(os.path.join(glob_escape(project_dir), "{}_i{}_ssa").format(self.platform, self.dynamips_id)) + files = glob.glob(os.path.join(glob.escape(project_dir), "{}_i{}_disk[0-1]".format(self.platform, self.dynamips_id))) + files += glob.glob(os.path.join(glob.escape(project_dir), "{}_i{}_slot[0-1]".format(self.platform, self.dynamips_id))) + files += glob.glob(os.path.join(glob.escape(project_dir), "{}_i{}_nvram".format(self.platform, self.dynamips_id))) + files += glob.glob(os.path.join(glob.escape(project_dir), "{}_i{}_flash[0-1]".format(self.platform, self.dynamips_id))) + files += glob.glob(os.path.join(glob.escape(project_dir), "{}_i{}_rom".format(self.platform, self.dynamips_id))) + files += glob.glob(os.path.join(glob.escape(project_dir), "{}_i{}_bootflash".format(self.platform, self.dynamips_id))) + files += glob.glob(os.path.join(glob.escape(project_dir), "{}_i{}_ssa").format(self.platform, self.dynamips_id)) for file in files: try: log.debug("Deleting file {}".format(file)) @@ -428,9 +451,6 @@ class Router(BaseVM): image = self.manager.get_abs_image_path(image) - if not os.path.isfile(image): - raise DynamipsError("IOS image '{}' is not accessible".format(image)) - yield from self._hypervisor.send('vm set_ios "{name}" "{image}"'.format(name=self._name, image=image)) log.info('Router "{name}" [{id}]: has a new IOS image set: "{image}"'.format(name=self._name, @@ -1212,6 +1232,7 @@ class Router(BaseVM): 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)) + try: yield from self._hypervisor.send('vm slot_add_nio_binding "{name}" {slot_number} {port_number} {nio}'.format(name=self._name, slot_number=slot_number, diff --git a/gns3server/modules/iou/__init__.py b/gns3server/modules/iou/__init__.py index df6ad3c7..92ffb526 100644 --- a/gns3server/modules/iou/__init__.py +++ b/gns3server/modules/iou/__init__.py @@ -26,6 +26,9 @@ from ..base_manager import BaseManager from .iou_error import IOUError from .iou_vm import IOUVM +import logging +log = logging.getLogger(__name__) + class IOU(BaseManager): @@ -68,6 +71,23 @@ class IOU(BaseManager): yield from super().close_vm(vm_id, *args, **kwargs) return vm + @asyncio.coroutine + def project_committed(self, project): + """ + Called when a project has been committed. + + :param project: Project instance + """ + + # save the configs when the project is committed + for vm in self._vms.copy().values(): + if vm.project.id == project.id: + try: + vm.save_configs() + except IOUError as e: + log.warning(e) + continue + def get_application_id(self, vm_id): """ Get an unique application identifier for IOU. diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 98a75b5a..1fe6ace1 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -33,6 +33,8 @@ import configparser import struct import hashlib import glob +import binascii +import functools from .iou_error import IOUError from ..adapters.ethernet_adapter import EthernetAdapter @@ -41,9 +43,11 @@ 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 ...utils.glob import glob_escape +from .utils.iou_import import nvram_import +from .utils.iou_export import nvram_export from .ioucon import start_ioucon import gns3server.utils.asyncio +import gns3server.utils.images import logging @@ -83,7 +87,8 @@ class IOUVM(BaseVM): self.serial_adapters = 2 # one adapter = 4 interfaces self._use_default_iou_values = True # for RAM & NVRAM values self._nvram = 128 # Kilobytes - self._initial_config = "" + self._startup_config = "" + self._private_config = "" self._ram = 256 # Megabytes self._l1_keepalives = False # used to overcome the always-up Ethernet interfaces (not supported by all IOSes). @@ -107,6 +112,7 @@ class IOUVM(BaseVM): self.manager.port_manager.release_udp_port(nio.lport, self._project) yield from self.stop() + self.save_configs() @property def path(self): @@ -137,27 +143,6 @@ class IOUVM(BaseVM): if os.path.isfile(fix_path): self._path = fix_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): """ @@ -184,9 +169,30 @@ class IOUVM(BaseVM): def _check_requirements(self): """ - Checks if IOUYAP executable is available. + Checks if IOUYAP executable is available and if image is accessible. """ + 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)) + path = self.iouyap_path if not path: raise IOUError("No path to iouyap program has been set") @@ -201,15 +207,18 @@ class IOUVM(BaseVM): iou_vm_info = {"name": self.name, "vm_id": self.id, + "vm_directory": self.working_dir, "console": self._console, "project_id": self.project.id, "path": self.path, + "md5sum": gns3server.utils.images.md5sum(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, + "startup_config": self.relative_startup_config_file, + "private_config": self.relative_private_config_file, "iourc_path": self.iourc_path, "use_default_iou_values": self._use_default_iou_values} @@ -317,10 +326,10 @@ class IOUVM(BaseVM): :param new_name: name """ - if self.initial_config_file: - content = self.initial_config_content + if self.startup_config_file: + content = self.startup_config_content content = content.replace(self._name, new_name) - self.initial_config_content = content + self.startup_config_content = content super(IOUVM, IOUVM).name.__set__(self, new_name) @@ -423,6 +432,38 @@ class IOUVM(BaseVM): self.iourc_path, hostname)) + def _push_configs_to_nvram(self): + """ + Push the startup-config and private-config content to the NVRAM. + """ + + startup_config_content = self.startup_config_content + if startup_config_content: + nvram_file = os.path.join(self.working_dir, "nvram_{:05d}".format(self.application_id)) + try: + if not os.path.exists(nvram_file): + open(nvram_file, "a").close() + nvram_content = None + else: + with open(nvram_file, "rb") as file: + nvram_content = file.read() + except OSError as e: + raise IOUError("Cannot read nvram file {}: {}".format(nvram_file, e)) + + startup_config_content = startup_config_content.encode("utf-8") + private_config_content = self.private_config_content + if private_config_content is not None: + private_config_content = private_config_content.encode("utf-8") + try: + nvram_content = nvram_import(nvram_content, startup_config_content, private_config_content, self.nvram) + except ValueError as e: + raise IOUError("Cannot push configs to nvram {}: {}".format(nvram_file, e)) + try: + with open(nvram_file, "wb") as file: + file.write(nvram_content) + except OSError as e: + raise IOUError("Cannot write nvram file {}: {}".format(nvram_file, e)) + @asyncio.coroutine def start(self): """ @@ -451,6 +492,11 @@ class IOUVM(BaseVM): raise IOUError("iouyap is necessary to start IOU") self._create_netmap_config() + self._push_configs_to_nvram() + + # check if there is enough RAM to run + self.check_available_ram(self.ram) + # created a environment variable pointing to the iourc file. env = os.environ.copy() @@ -469,6 +515,9 @@ class IOUVM(BaseVM): env=env) log.info("IOU instance {} started PID={}".format(self._id, self._iou_process.pid)) self._started = True + self.status = "started" + callback = functools.partial(self._termination_callback, "IOU") + gns3server.utils.asyncio.monitor_process(self._iou_process, callback) 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: @@ -481,16 +530,32 @@ class IOUVM(BaseVM): # connections support yield from self._start_iouyap() + def _termination_callback(self, process_name, returncode): + """ + Called when the process has stopped. + + :param returncode: Process returncode + """ + + log.info("{} process has stopped, return code: {}".format(process_name, returncode)) + self._terminate_process_iou() + self._terminate_process_iouyap() + self._ioucon_thread_stop_event.set() + if returncode != 0: + self.project.emit("log.error", {"message": "{} process has stopped, return code: {}\n{}".format(process_name, + returncode, + self.read_iou_stdout())}) + def _rename_nvram_file(self): """ Before starting the VM, rename the nvram and vlan.dat files with the correct IOU application identifier. """ destination = os.path.join(self.working_dir, "nvram_{:05d}".format(self.application_id)) - for file_path in glob.glob(os.path.join(glob_escape(self.working_dir), "nvram_*")): + for file_path in glob.glob(os.path.join(glob.escape(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(glob_escape(self.working_dir), "vlan.dat-*")): + for file_path in glob.glob(os.path.join(glob.escape(self.working_dir), "vlan.dat-*")): shutil.move(file_path, destination) @asyncio.coroutine @@ -511,6 +576,8 @@ class IOUVM(BaseVM): stderr=subprocess.STDOUT, cwd=self.working_dir) + callback = functools.partial(self._termination_callback, "iouyap") + gns3server.utils.asyncio.monitor_process(self._iouyap_process, callback) log.info("iouyap started PID={}".format(self._iouyap_process.pid)) except (OSError, subprocess.SubprocessError) as e: iouyap_stdout = self.read_iouyap_stdout() @@ -609,7 +676,10 @@ class IOUVM(BaseVM): 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() + try: + self._iouyap_process.kill() + except ProcessLookupError: + pass self._iouyap_process = None self._started = False @@ -619,24 +689,28 @@ class IOUVM(BaseVM): Terminate the IOUYAP process if running. """ - log.info('Stopping IOUYAP process for IOU VM "{}" PID={}'.format(self.name, self._iouyap_process.pid)) - try: - self._iouyap_process.terminate() - # Sometime the process may already be dead when we garbage collect - except ProcessLookupError: - pass + if self._iouyap_process: + log.info('Stopping IOUYAP process for IOU VM "{}" 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 IOU process if running """ - log.info('Stopping IOU process for IOU VM "{}" PID={}'.format(self.name, self._iou_process.pid)) - try: - self._iou_process.terminate() - # Sometime the process may already be dead when we garbage collect - except ProcessLookupError: - pass + if self._iou_process: + log.info('Stopping IOU process for IOU VM "{}" 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 + self._started = False + self.status = "stopped" @asyncio.coroutine def reload(self): @@ -729,9 +803,11 @@ class IOUVM(BaseVM): 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)]) + # do not let IOU create the NVRAM anymore + #startup_config_file = self.startup_config_file + # if startup_config_file: + # command.extend(["-c", os.path.basename(startup_config_file)]) + if self._l1_keepalives: yield from self._enable_l1_keepalives(command) command.extend([str(self.application_id)]) @@ -949,12 +1025,12 @@ class IOUVM(BaseVM): log.warn("could not determine if layer 1 keepalive messages are supported by {}: {}".format(os.path.basename(self._path), e)) @property - def initial_config_content(self): + def startup_config_content(self): """ - Returns the content of the current initial-config file. + Returns the content of the current startup-config file. """ - config_file = self.initial_config_file + config_file = self.startup_config_file if config_file is None: return None @@ -962,64 +1038,190 @@ class IOUVM(BaseVM): with open(config_file, "rb") as f: return f.read().decode("utf-8", errors="replace") except OSError as e: - raise IOUError("Can't read configuration file '{}': {}".format(config_file, e)) + raise IOUError("Can't read startup-config file '{}': {}".format(config_file, e)) - @initial_config_content.setter - def initial_config_content(self, initial_config): + @startup_config_content.setter + def startup_config_content(self, startup_config): """ - Update the initial config + Update the startup config - :param initial_config: content of the initial configuration file + :param startup_config: content of the startup configuration file """ try: - initial_config_path = os.path.join(self.working_dir, "initial-config.cfg") + startup_config_path = os.path.join(self.working_dir, "startup-config.cfg") - if initial_config is None: - initial_config = '' + if startup_config is None: + startup_config = '' - # We disallow erasing the initial config file - if len(initial_config) == 0 and os.path.exists(initial_config_path): + # We disallow erasing the startup config file + if len(startup_config) == 0 and os.path.exists(startup_config_path): return - with open(initial_config_path, 'w+', encoding='utf-8') as f: - if len(initial_config) == 0: + with open(startup_config_path, 'w+', encoding='utf-8') as f: + if len(startup_config) == 0: f.write('') else: - initial_config = initial_config.replace("%h", self._name) - f.write(initial_config) + startup_config = startup_config.replace("%h", self._name) + f.write(startup_config) except OSError as e: - raise IOUError("Can't write initial configuration file '{}': {}".format(initial_config_path, e)) + raise IOUError("Can't write startup-config file '{}': {}".format(startup_config_path, e)) @property - def initial_config_file(self): + def private_config_content(self): + """ + Returns the content of the current private-config file. """ - Returns the initial config file for this IOU VM. + + config_file = self.private_config_file + if config_file is None: + return None + + try: + with open(config_file, "rb") as f: + return f.read().decode("utf-8", errors="replace") + except OSError as e: + raise IOUError("Can't read private-config file '{}': {}".format(config_file, e)) + + @private_config_content.setter + def private_config_content(self, private_config): + """ + Update the private config + + :param private_config: content of the private configuration file + """ + + try: + private_config_path = os.path.join(self.working_dir, "private-config.cfg") + + if private_config is None: + private_config = '' + + # We disallow erasing the startup config file + if len(private_config) == 0 and os.path.exists(private_config_path): + return + + with open(private_config_path, 'w+', encoding='utf-8') as f: + if len(private_config) == 0: + f.write('') + else: + private_config = private_config.replace("%h", self._name) + f.write(private_config) + except OSError as e: + raise IOUError("Can't write private-config file '{}': {}".format(private_config_path, e)) + + @property + def startup_config_file(self): + """ + Returns the startup-config file for this IOU VM. :returns: path to config file. None if the file doesn't exist """ - path = os.path.join(self.working_dir, 'initial-config.cfg') + path = os.path.join(self.working_dir, 'startup-config.cfg') if os.path.exists(path): return path else: return None @property - def relative_initial_config_file(self): + def private_config_file(self): """ - Returns the initial config file relative to the project directory. - It's compatible with pre 1.3 projects. + Returns the private-config file for this IOU VM. :returns: path to config file. None if the file doesn't exist """ - path = os.path.join(self.working_dir, 'initial-config.cfg') + path = os.path.join(self.working_dir, 'private-config.cfg') if os.path.exists(path): - return 'initial-config.cfg' + return path else: return None + @property + def relative_startup_config_file(self): + """ + Returns the startup-config file relative to the project directory. + It's compatible with pre 1.3 projects. + + :returns: path to startup-config file. None if the file doesn't exist + """ + + path = os.path.join(self.working_dir, 'startup-config.cfg') + if os.path.exists(path): + return 'startup-config.cfg' + else: + return None + + @property + def relative_private_config_file(self): + """ + Returns the private-config file relative to the project directory. + + :returns: path to private-config file. None if the file doesn't exist + """ + + path = os.path.join(self.working_dir, 'private-config.cfg') + if os.path.exists(path): + return 'private-config.cfg' + else: + return None + + def extract_configs(self): + """ + Gets the contents of the config files + startup-config and private-config from NVRAM. + + :returns: tuple (startup-config, private-config) + """ + + nvram_file = os.path.join(self.working_dir, "nvram_{:05d}".format(self.application_id)) + if not os.path.exists(nvram_file): + return None, None + try: + with open(nvram_file, "rb") as file: + nvram_content = file.read() + except OSError as e: + log.warning("Cannot read nvram file {}: {}".format(nvram_file, e)) + return None, None + + try: + startup_config_content, private_config_content = nvram_export(nvram_content) + except ValueError as e: + log.warning("Could not export configs from nvram file".format(nvram_file, e)) + return None, None + + return startup_config_content, private_config_content + + def save_configs(self): + """ + Saves the startup-config and private-config to files. + """ + + if self.startup_config_content or self.private_config_content: + startup_config_content, private_config_content = self.extract_configs() + if startup_config_content: + config_path = os.path.join(self.working_dir, "startup-config.cfg") + try: + config = startup_config_content.decode("utf-8", errors="replace") + config = "!\n" + config.replace("\r", "") + with open(config_path, "wb") as f: + log.info("saving startup-config to {}".format(config_path)) + f.write(config.encode("utf-8")) + except (binascii.Error, OSError) as e: + raise IOUError("Could not save the startup configuration {}: {}".format(config_path, e)) + + if private_config_content: + config_path = os.path.join(self.working_dir, "private-config.cfg") + try: + config = private_config_content.decode("utf-8", errors="replace") + config = "!\n" + config.replace("\r", "") + with open(config_path, "wb") as f: + log.info("saving private-config to {}".format(config_path)) + f.write(config.encode("utf-8")) + except (binascii.Error, OSError) as e: + raise IOUError("Could not save the private configuration {}: {}".format(config_path, e)) + @asyncio.coroutine def start_capture(self, adapter_number, port_number, output_file, data_link_type="DLT_EN10MB"): """ @@ -1051,10 +1253,11 @@ class IOUVM(BaseVM): port_number=port_number)) nio.startPacketCapture(output_file, data_link_type) - log.info('IOU "{name}" [{id}]: starting packet capture on {adapter_number}/{port_number}'.format(name=self._name, + log.info('IOU "{name}" [{id}]: starting packet capture on {adapter_number}/{port_number} to {output_file}'.format(name=self._name, id=self._id, adapter_number=adapter_number, - port_number=port_number)) + port_number=port_number, + output_file=output_file)) if self.is_iouyap_running(): self._update_iouyap_config() diff --git a/gns3server/modules/iou/ioucon.py b/gns3server/modules/iou/ioucon.py index 764e81e8..7aeb0287 100644 --- a/gns3server/modules/iou/ioucon.py +++ b/gns3server/modules/iou/ioucon.py @@ -21,10 +21,14 @@ import socket import sys import os import select -import fcntl +try: + import fcntl + import termios + import tty +except ImportError: + # On windows it's not available but this module can be included by the test suite + pass import struct -import termios -import tty import time import argparse import traceback @@ -351,13 +355,19 @@ class TelnetServer(Console): def __enter__(self): # Open a socket and start listening - sock_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock_fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - try: - sock_fd.bind((self.addr, self.port)) - except OSError: - raise TelnetServerError("Cannot bind to {}:{}" - .format(self.addr, self.port)) + + info = socket.getaddrinfo(self.addr, self.port, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE) + if not info: + raise TelnetServerError("getaddrinfo returns an empty list on {}:{}".format(self.addr, self.port)) + for res in info: + af, socktype, proto, _, sa = res + sock_fd = socket.socket(af, socktype, proto) + sock_fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + try: + sock_fd.bind(sa) + except OSError: + raise TelnetServerError("Cannot bind to {}:{}" + .format(self.addr, self.port)) sock_fd.listen(socket.SOMAXCONN) self.sock_fd = sock_fd @@ -541,7 +551,6 @@ def send_recv_loop(epoll, console, router, esc_char, stop_event): else: router.write(buf) finally: - log.debug("Finally") router.unregister(epoll) console.unregister(epoll) diff --git a/gns3server/modules/iou/utils/__init__.py b/gns3server/modules/iou/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gns3server/modules/iou/utils/iou_export.py b/gns3server/modules/iou/utils/iou_export.py new file mode 100644 index 00000000..33cdc016 --- /dev/null +++ b/gns3server/modules/iou/utils/iou_export.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# To use python v2.7 change the first line to: +#!/usr/bin/env python + +# Copyright (C) 2015 Bernhard Ehlers +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General 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 utility is a stripped down version of dynamips' nvram_export, +# ported from C to Python, see https://github.com/GNS3/dynamips +# nvram_export is (c) 2013 Flávio J. Saraiva + +""" +iou_export exports startup/private configuration from IOU NVRAM file. + +usage: iou_export [-h] NVRAM startup-config [private-config] + +positional arguments: + NVRAM NVRAM file + startup-config startup configuration + private-config private configuration + +optional arguments: + -h, --help show this help message and exit +""" + +import argparse +import sys + + +# Uncompress data in .Z file format. +# Ported from dynamips' fs_nvram.c to python +# Adapted from 7zip's ZDecoder.cpp, which is licensed under LGPL 2.1. +def uncompress_LZC(data): + LZC_NUM_BITS_MIN = 9 + LZC_NUM_BITS_MAX = 16 + + in_data = bytearray(data) + in_len = len(in_data) + out_data = bytearray() + + if in_len == 0: + return out_data + if in_len < 3: + raise ValueError('invalid length') + if in_data[0] != 0x1F or in_data[1] != 0x9D: + raise ValueError('invalid header') + + maxbits = in_data[2] & 0x1F + numItems = 1 << maxbits + blockMode = (in_data[2] & 0x80) != 0 + if maxbits < LZC_NUM_BITS_MIN or maxbits > LZC_NUM_BITS_MAX: + raise ValueError('not supported') + + parents = [0] * numItems + suffixes = [0] * numItems + + in_pos = 3 + numBits = LZC_NUM_BITS_MIN + head = 256 + if blockMode: + head += 1 + + needPrev = 0 + bitPos = 0 + numBufBits = 0 + + parents[256] = 0 + suffixes[256] = 0 + + buf_extend = bytearray([0] * 3) + + while True: + # fill buffer, when empty + if numBufBits == bitPos: + buf_len = min(in_len - in_pos, numBits) + buf = in_data[in_pos:in_pos + buf_len] + buf_extend + numBufBits = buf_len << 3 + bitPos = 0 + in_pos += buf_len + + # extract next symbol + bytePos = bitPos >> 3 + symbol = buf[bytePos] | buf[bytePos + 1] << 8 | buf[bytePos + 2] << 16 + symbol >>= bitPos & 7 + symbol &= (1 << numBits) - 1 + bitPos += numBits + + # check for special conditions: end, bad data, re-initialize dictionary + if bitPos > numBufBits: + break + if symbol >= head: + raise ValueError('invalid data') + if blockMode and symbol == 256: + numBufBits = bitPos = 0 + numBits = LZC_NUM_BITS_MIN + head = 257 + needPrev = 0 + continue + + # convert symbol to string + stack = [] + cur = symbol + while cur >= 256: + stack.append(suffixes[cur]) + cur = parents[cur] + stack.append(cur) + if needPrev: + suffixes[head - 1] = cur + if symbol == head - 1: + stack[0] = cur + stack.reverse() + out_data.extend(stack) + + # update parents, check for numBits change + if head < numItems: + needPrev = 1 + parents[head] = symbol + head += 1 + if head > (1 << numBits): + if numBits < maxbits: + numBufBits = bitPos = 0 + numBits += 1 + else: + needPrev = 0 + + return out_data + + +# extract 16 bit unsigned int from data +def get_uint16(data, off): + return data[off] << 8 | data[off + 1] + + +# extract 32 bit unsigned int from data +def get_uint32(data, off): + return data[off] << 24 | data[off + 1] << 16 | data[off + 2] << 8 | data[off + 3] + + +# export IOU NVRAM +def nvram_export(nvram): + nvram = bytearray(nvram) + + # extract startup config + offset = 0 + if len(nvram) < offset + 36: + raise ValueError('invalid length') + if get_uint16(nvram, offset + 0) != 0xABCD: + raise ValueError('no startup config') + format = get_uint16(nvram, offset + 2) + length = get_uint32(nvram, offset + 16) + offset += 36 + if len(nvram) < offset + length: + raise ValueError('invalid length') + startup = nvram[offset:offset + length] + + # compressed startup config + if format == 2: + try: + startup = uncompress_LZC(startup) + except ValueError as err: + raise ValueError('uncompress startup: ' + str(err)) + + offset += length + # alignment to multiple of 4 + offset = (offset + 3) & ~3 + # check for additonal offset of 4 + if len(nvram) >= offset + 8 and \ + get_uint16(nvram, offset + 4) == 0xFEDC and \ + get_uint16(nvram, offset + 6) == 1: + offset += 4 + + # extract private config + private = None + if len(nvram) >= offset + 16 and get_uint16(nvram, offset + 0) == 0xFEDC: + length = get_uint32(nvram, offset + 12) + offset += 16 + if len(nvram) >= offset + length: + private = nvram[offset:offset + length] + + return (startup, private) + + +if __name__ == '__main__': + # Main program + + parser = argparse.ArgumentParser(description='%(prog)s exports startup/private configuration from IOU NVRAM file.') + parser.add_argument('nvram', metavar='NVRAM', + help='NVRAM file') + parser.add_argument('startup', metavar='startup-config', + help='startup configuration') + parser.add_argument('private', metavar='private-config', nargs='?', + help='private configuration') + args = parser.parse_args() + + try: + fd = open(args.nvram, 'rb') + nvram = fd.read() + fd.close() + except (IOError, OSError) as err: + sys.stderr.write("Error reading file: {}\n".format(err)) + sys.exit(1) + + try: + startup, private = nvram_export(nvram) + except ValueError as err: + sys.stderr.write("nvram_export: {}\n".format(err)) + sys.exit(3) + + try: + fd = open(args.startup, 'wb') + fd.write(startup) + fd.close() + if args.private is not None: + if private is None: + sys.stderr.write("Warning: No private config\n") + else: + fd = open(args.private, 'wb') + fd.write(private) + fd.close() + except (IOError, OSError) as err: + sys.stderr.write("Error writing file: {}\n".format(err)) + sys.exit(1) diff --git a/gns3server/modules/iou/utils/iou_import.py b/gns3server/modules/iou/utils/iou_import.py new file mode 100644 index 00000000..6ebe5258 --- /dev/null +++ b/gns3server/modules/iou/utils/iou_import.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# To use python v2.7 change the first line to: +#!/usr/bin/env python + +# Copyright (C) 2015 Bernhard Ehlers +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General 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_import imports startup/private configuration into IOU NVRAM file. + +usage: iou_import [-h] [-c size] NVRAM startup-config [private-config] + +positional arguments: + NVRAM NVRAM file + startup-config startup configuration + private-config private configuration + +optional arguments: + -h, --help show this help message and exit + -c size, --create size + create NVRAM file, size in kByte +""" + +import argparse +import sys + + +# extract 16 bit unsigned int from data +def get_uint16(data, off): + return data[off] << 8 | data[off + 1] + + +# extract 32 bit unsigned int from data +def get_uint32(data, off): + return data[off] << 24 | data[off + 1] << 16 | data[off + 2] << 8 | data[off + 3] + + +# insert 16 bit unsigned int into data +def put_uint16(data, off, value): + data[off] = (value >> 8) & 0xff + data[off + 1] = value & 0xff + + +# insert 32 bit unsigned int into data +def put_uint32(data, off, value): + data[off] = (value >> 24) & 0xff + data[off + 1] = (value >> 16) & 0xff + data[off + 2] = (value >> 8) & 0xff + data[off + 3] = value & 0xff + + +# calculate padding +def padding(off, ios): + pad = (4 - off % 4) % 4 # padding to alignment of 4 + if ios <= 0x0F00 and pad != 0: # add 4 if IOS <= 15.0 + pad += 4 + return pad + + +# update checksum +def checksum(data, start, end): + put_uint16(data, start + 4, 0) # set checksum to 0 + + chk = 0 + idx = start + while idx < end - 1: + chk += get_uint16(data, idx) + idx += 2 + if idx < end: + chk += data[idx] << 8 + + while chk >> 16: + chk = (chk & 0xffff) + (chk >> 16) + + chk = chk ^ 0xffff + put_uint16(data, start + 4, chk) # set checksum + + +# import IOU NVRAM +def nvram_import(nvram, startup, private, size): + BASE_ADDRESS = 0x10000000 + DEFAULT_IOS = 0x0F04 # IOS 15.4 + + # check size parameter + if size is not None and (size < 8 or size > 1024): + raise ValueError('invalid size') + + # create new nvram if nvram is empty or has wrong size + if nvram is None or (size is not None and len(nvram) != size * 1024): + nvram = bytearray([0] * (size * 1024)) + else: + nvram = bytearray(nvram) + + # check nvram size + nvram_len = len(nvram) + if nvram_len < 8 * 1024 or nvram_len > 1024 * 1024 or nvram_len % 1024 != 0: + raise ValueError('invalid NVRAM length') + nvram_len = nvram_len // 2 + + # get size of current config + config_len = 0 + ios = None + try: + if get_uint16(nvram, 0) == 0xABCD: + ios = get_uint16(nvram, 6) + config_len = 36 + get_uint32(nvram, 16) + config_len += padding(config_len, ios) + if get_uint16(nvram, config_len) == 0xFEDC: + config_len += 16 + get_uint32(nvram, config_len + 12) + except IndexError: + raise ValueError('unknown nvram format') + if config_len > nvram_len: + raise ValueError('unknown nvram format') + + # calculate max. config size + max_config = nvram_len - 2 * 1024 # reserve 2k for files + idx = max_config + empty_sector = bytearray([0] * 1024) + while True: + idx -= 1024 + if idx < config_len: + break + # if valid file header: + if get_uint16(nvram, idx + 0) == 0xDCBA and \ + get_uint16(nvram, idx + 4) < 8 and \ + get_uint16(nvram, idx + 6) <= 992: + max_config = idx + elif nvram[idx:idx + 1024] != empty_sector: + break + + # import startup config + startup = bytearray(startup) + if ios is None: + # Target IOS version is unknown. As some IOU don't work nicely with + # the padding of a different version, the startup config is padded + # with '\n' to the alignment of 4. + ios = DEFAULT_IOS + startup.extend([ord('\n')] * ((4 - len(startup) % 4) % 4)) + new_nvram = bytearray([0] * 36) # startup hdr + put_uint16(new_nvram, 0, 0xABCD) # magic + put_uint16(new_nvram, 2, 1) # raw data + put_uint16(new_nvram, 6, ios) # IOS version + put_uint32(new_nvram, 8, BASE_ADDRESS + 36) # start address + put_uint32(new_nvram, 12, BASE_ADDRESS + 36 + len(startup)) # end address + put_uint32(new_nvram, 16, len(startup)) # length + new_nvram.extend(startup) + new_nvram.extend([0] * padding(len(new_nvram), ios)) + + # import private config + if private is None: + private = bytearray() + else: + private = bytearray(private) + offset = len(new_nvram) + new_nvram.extend([0] * 16) # private hdr + put_uint16(new_nvram, 0 + offset, 0xFEDC) # magic + put_uint16(new_nvram, 2 + offset, 1) # raw data + put_uint32(new_nvram, 4 + offset, + BASE_ADDRESS + offset + 16) # start address + put_uint32(new_nvram, 8 + offset, + BASE_ADDRESS + offset + 16 + len(private)) # end address + put_uint32(new_nvram, 12 + offset, len(private)) # length + new_nvram.extend(private) + + # add rest + if len(new_nvram) > max_config: + raise ValueError('NVRAM size too small') + new_nvram.extend([0] * (max_config - len(new_nvram))) + new_nvram.extend(nvram[max_config:]) + + checksum(new_nvram, 0, nvram_len) + + return new_nvram + + +if __name__ == '__main__': + # Main program + + def check_size(string): + try: + value = int(string) + except ValueError: + raise argparse.ArgumentTypeError('invalid int value: ' + string) + if value < 8 or value > 1024: + raise argparse.ArgumentTypeError('size must be 8..1024') + return value + + parser = argparse.ArgumentParser(description='%(prog)s imports startup/private configuration into IOU NVRAM file.') + parser.add_argument('-c', '--create', metavar='size', type=check_size, + help='create NVRAM file, size in kByte') + parser.add_argument('nvram', metavar='NVRAM', + help='NVRAM file') + parser.add_argument('startup', metavar='startup-config', + help='startup configuration') + parser.add_argument('private', metavar='private-config', nargs='?', + help='private configuration') + args = parser.parse_args() + + try: + if args.create is None: + fd = open(args.nvram, 'rb') + nvram = fd.read() + fd.close() + else: + nvram = None + fd = open(args.startup, 'rb') + startup = fd.read() + fd.close() + if args.private is None: + private = None + else: + fd = open(args.private, 'rb') + private = fd.read() + fd.close() + except (IOError, OSError) as err: + sys.stderr.write("Error reading file: {}\n".format(err)) + sys.exit(1) + + try: + nvram = nvram_import(nvram, startup, private, args.create) + except ValueError as err: + sys.stderr.write("nvram_import: {}\n".format(err)) + sys.exit(3) + + try: + fd = open(args.nvram, 'wb') + fd.write(nvram) + fd.close() + except (IOError, OSError) as err: + sys.stderr.write("Error writing file: {}\n".format(err)) + sys.exit(1) diff --git a/gns3server/modules/port_manager.py b/gns3server/modules/port_manager.py index c74dbbc9..e6aab094 100644 --- a/gns3server/modules/port_manager.py +++ b/gns3server/modules/port_manager.py @@ -16,7 +16,6 @@ # along with this program. If not, see . import socket -import sys import ipaddress from aiohttp.web import HTTPConflict from gns3server.config import Config @@ -43,8 +42,8 @@ class PortManager: server_config = Config.instance().get_section_config("Server") remote_console_connections = server_config.getboolean("allow_remote_console") - console_start_port_range = server_config.getint("console_start_port_range", 2000) - console_end_port_range = server_config.getint("console_end_port_range", 5000) + console_start_port_range = server_config.getint("console_start_port_range", 2001) + console_end_port_range = server_config.getint("console_end_port_range", 7000) 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)) @@ -143,26 +142,16 @@ class PortManager: if end_port < start_port: raise HTTPConflict(text="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 + + last_exception 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 + PortManager._check_port(host, port, socket_type) + return port except OSError as e: last_exception = e if port + 1 == end_port: @@ -174,16 +163,40 @@ class PortManager: end_port, host, last_exception)) + @staticmethod + def _check_port(host, port, socket_type): + """ + Check if an a port is available and raise an OSError if port is not available + + :returns: boolean + """ + if socket_type == "UDP": + socket_type = socket.SOCK_DGRAM + else: + socket_type = socket.SOCK_STREAM + + for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket_type, 0, socket.AI_PASSIVE): + af, socktype, proto, _, sa = res + with socket.socket(af, socktype, proto) as s: + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(sa) # the port is available if bind is a success + return True - def get_free_tcp_port(self, project): + + def get_free_tcp_port(self, project, port_range_start=None, port_range_end=None): """ 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], + # use the default range is not specific one is given + if port_range_start is None and port_range_end is None: + port_range_start = self._console_port_range[0] + port_range_end = self._console_port_range[1] + + port = self.find_unused_port(port_range_start, + port_range_end, host=self._console_host, socket_type="TCP", ignore_ports=self._used_tcp_ports) @@ -193,18 +206,47 @@ class PortManager: log.debug("TCP port {} has been allocated".format(port)) return port - def reserve_tcp_port(self, port, project): + def reserve_tcp_port(self, port, project, port_range_start=None, port_range_end=None): """ - Reserve a specific TCP port number + Reserve a specific TCP port number. If not available replace it + by another. :param port: TCP port number :param project: Project instance + :param port_range_start: Port range to use + :param port_range_end: Port range to use + :returns: The TCP port """ + # use the default range is not specific one is given + if port_range_start is None and port_range_end is None: + port_range_start = self._console_port_range[0] + port_range_end = self._console_port_range[1] + if port in self._used_tcp_ports: - raise HTTPConflict(text="TCP port {} already in use on host".format(port, self._console_host)) + old_port = port + port = self.get_free_tcp_port(project, port_range_start=port_range_start, port_range_end=port_range_end) + msg = "TCP port {} already in use on host {}. Port has been replaced by {}".format(old_port, self._console_host, port) + log.warning(msg) + project.emit("log.warning", {"message": msg}) + return port if port < self._console_port_range[0] or port > self._console_port_range[1]: - raise HTTPConflict(text="TCP port {} is outside the range {}-{}".format(port, self._console_port_range[0], self._console_port_range[1])) + old_port = port + port = self.get_free_tcp_port(project, port_range_start=port_range_start, port_range_end=port_range_end) + msg = "TCP port {} is outside the range {}-{} on host {}. Port has been replaced by {}".format(old_port, port_range_start, port_range_end, self._console_host, port) + log.warning(msg) + project.emit("log.warning", {"message": msg}) + return port + try: + PortManager._check_port(self._console_host, port, "TCP") + except OSError: + old_port = port + port = self.get_free_tcp_port(project, port_range_start=port_range_start, port_range_end=port_range_end) + msg = "TCP port {} already in use on host {}. Port has been replaced by {}".format(old_port, self._console_host, port) + log.warning(msg) + project.emit("log.warning", {"message": msg}) + return port + self._used_tcp_ports.add(port) project.record_tcp_port(port) log.debug("TCP port {} has been reserved".format(port)) @@ -229,7 +271,6 @@ class PortManager: :param project: Project instance """ - port = self.find_unused_port(self._udp_port_range[0], self._udp_port_range[1], host=self._udp_host, diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index 2504656e..26604066 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -19,6 +19,7 @@ import aiohttp import os import shutil import asyncio +import hashlib from uuid import UUID, uuid4 from .port_manager import PortManager @@ -65,6 +66,9 @@ class Project: self._used_tcp_ports = set() self._used_udp_ports = set() + # clients listening for notifications + self._listeners = set() + if path is None: path = os.path.join(self._location, self._id) try: @@ -139,10 +143,6 @@ class Project: 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") - old_path = None - if hasattr(self, "_path"): - old_path = self._path - self._path = path self._update_temporary_file() @@ -435,3 +435,72 @@ class Project: # We import it at the last time to avoid circular dependencies from ..modules import MODULES return MODULES + + def emit(self, action, event): + """ + Send an event to all the client listening for notifications + + :param action: Action name + :param event: Event to send + """ + for listener in self._listeners: + listener.put_nowait((action, event, )) + + def get_listen_queue(self): + """Get a queue where you receive all the events related to the + project.""" + + queue = asyncio.Queue() + self._listeners.add(queue) + return queue + + def stop_listen_queue(self, queue): + """Stop sending notification to this clients""" + + self._listeners.remove(queue) + + @property + def listeners(self): + """ + List of current clients listening for event in this projects + """ + return self._listeners + + @asyncio.coroutine + def list_files(self): + """ + :returns: Array of files in project without temporary files. The files are dictionnary {"path": "test.bin", "md5sum": "aaaaa"} + """ + + files = [] + for (dirpath, dirnames, filenames) in os.walk(self.path): + for filename in filenames: + if not filename.endswith(".ghost"): + path = os.path.relpath(dirpath, self.path) + path = os.path.join(path, filename) + path = os.path.normpath(path) + file_info = {"path": path} + + try: + file_info["md5sum"] = yield from wait_run_in_executor(self._hash_file, os.path.join(dirpath, filename)) + except OSError: + continue + files.append(file_info) + + return files + + def _hash_file(self, path): + """ + Compute and md5 hash for file + + :returns: hexadecimal md5 + """ + + m = hashlib.md5() + with open(path, "rb") as f: + while True: + buf = f.read(128) + if not buf: + break + m.update(buf) + return m.hexdigest() diff --git a/gns3server/modules/project_manager.py b/gns3server/modules/project_manager.py index 4ea21612..7ecf4e54 100644 --- a/gns3server/modules/project_manager.py +++ b/gns3server/modules/project_manager.py @@ -95,3 +95,18 @@ class ProjectManager: 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] + + def check_hardware_virtualization(self, source_vm): + """ + Checks if hardware virtualization can be used. + + :returns: boolean + """ + + for project in self._projects.values(): + for vm in project.vms: + if vm == source_vm: + continue + if vm.hw_virtualization and vm.__class__.__name__ != source_vm.__class__.__name__: + return False + return True diff --git a/gns3server/modules/qemu/__init__.py b/gns3server/modules/qemu/__init__.py index 0e9fe9b7..3ee441b0 100644 --- a/gns3server/modules/qemu/__init__.py +++ b/gns3server/modules/qemu/__init__.py @@ -21,6 +21,7 @@ Qemu server module. import asyncio import os +import platform import sys import re import subprocess @@ -39,14 +40,40 @@ class Qemu(BaseManager): _VM_CLASS = QemuVM @staticmethod - def binary_list(): + @asyncio.coroutine + def get_kvm_archs(): """ - Gets QEMU binaries list available on the host. + Gets a list of architectures for which KVM is available on this server. - :returns: Array of dictionary {"path": Qemu binary path, "version": version of Qemu} + :returns: List of architectures for which KVM is available on this server. + """ + kvm = [] + + try: + process = yield from asyncio.create_subprocess_exec("kvm-ok") + yield from process.wait() + except OSError: + return kvm + + if process.returncode == 0: + arch = platform.machine() + if arch == "x86_64": + kvm.append("x86_64") + kvm.append("i386") + elif arch == "i386": + kvm.append("i386") + else: + kvm.append(platform.machine()) + return kvm + + @staticmethod + def paths_list(): + """ + Gets a folder list of possibly available QEMU binaries on the host. + + :returns: List of folders where Qemu binaries MAY reside. """ - qemus = [] paths = set() try: paths.add(os.getcwd()) @@ -71,28 +98,70 @@ class Qemu(BaseManager): if "PROGRAMFILES" in os.environ and os.path.exists(os.environ["PROGRAMFILES"]): paths.add(os.path.join(os.environ["PROGRAMFILES"], "qemu")) elif sys.platform.startswith("darwin"): - # add specific locations on Mac OS X regardless of what's in $PATH - paths.update(["/usr/local/bin", "/opt/local/bin"]) if hasattr(sys, "frozen"): + # add specific locations on Mac OS X regardless of what's in $PATH + paths.update(["/usr/bin", "/usr/local/bin", "/opt/local/bin"]) try: - paths.add(os.path.abspath(os.path.join(os.getcwd(), "../../../qemu/bin/"))) + exec_dir = os.path.dirname(os.path.abspath(sys.executable)) + paths.add(os.path.abspath(os.path.join(exec_dir, "../Resources/qemu/bin/"))) # If the user run the server by hand from outside except FileNotFoundError: paths.add("/Applications/GNS3.app/Contents/Resources/qemu/bin") - for path in paths: + return paths + + @staticmethod + def binary_list(archs=None): + """ + Gets QEMU binaries list available on the host. + + :returns: Array of dictionary {"path": Qemu binary path, "version": version of Qemu} + """ + + qemus = [] + for path in Qemu.paths_list(): try: for f in os.listdir(path): if (f.startswith("qemu-system") or f.startswith("qemu-kvm") or f == "qemu" or f == "qemu.exe") and \ os.access(os.path.join(path, f), os.X_OK) and \ os.path.isfile(os.path.join(path, f)): - qemu_path = os.path.join(path, f) - version = yield from Qemu.get_qemu_version(qemu_path) - qemus.append({"path": qemu_path, "version": version}) + if archs is not None: + for arch in archs: + if f.endswith(arch) or f.endswith("{}.exe".format(arch)) or f.endswith("{}w.exe".format(arch)): + qemu_path = os.path.join(path, f) + version = yield from Qemu.get_qemu_version(qemu_path) + qemus.append({"path": qemu_path, "version": version}) + else: + qemu_path = os.path.join(path, f) + version = yield from Qemu.get_qemu_version(qemu_path) + qemus.append({"path": qemu_path, "version": version}) + except OSError: continue return qemus + @staticmethod + def img_binary_list(): + """ + Gets QEMU-img binaries list available on the host. + + :returns: Array of dictionary {"path": Qemu-img binary path, "version": version of Qemu-img} + """ + qemu_imgs = [] + for path in Qemu.paths_list(): + try: + for f in os.listdir(path): + if (f == "qemu-img" or f == "qemu-img.exe") and \ + os.access(os.path.join(path, f), os.X_OK) and \ + os.path.isfile(os.path.join(path, f)): + qemu_path = os.path.join(path, f) + version = yield from Qemu._get_qemu_img_version(qemu_path) + qemu_imgs.append({"path": qemu_path, "version": version}) + except OSError: + continue + + return qemu_imgs + @staticmethod @asyncio.coroutine def get_qemu_version(qemu_path): @@ -128,6 +197,26 @@ class Qemu(BaseManager): except subprocess.SubprocessError as e: raise QemuError("Error while looking for the Qemu version: {}".format(e)) + @staticmethod + @asyncio.coroutine + def _get_qemu_img_version(qemu_img_path): + """ + Gets the Qemu-img version. + + :param qemu_img_path: path to Qemu-img executable. + """ + + try: + output = yield from subprocess_check_output(qemu_img_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-img version for {}".format(qemu_img_path)) + except subprocess.SubprocessError as e: + raise QemuError("Error while looking for the Qemu-img version: {}".format(e)) + @staticmethod def get_legacy_vm_workdir(legacy_vm_id, name): """ @@ -146,3 +235,35 @@ class Qemu(BaseManager): Return the full path of the images directory on disk """ return os.path.join(os.path.expanduser(self.config.get_section_config("Server").get("images_path", "~/GNS3/images")), "QEMU") + + @asyncio.coroutine + def create_disk(self, qemu_img, path, options): + """ + Create a qemu disk with qemu-img + + :param qemu_img: qemu-img binary path + :param path: Image path + :param options: Disk image creation options + """ + + try: + img_format = options.pop("format") + img_size = options.pop("size") + + if not os.path.isabs(path): + directory = self.get_images_directory() + os.makedirs(directory, exist_ok=True) + path = os.path.join(directory, os.path.basename(path)) + if os.path.exists(path): + raise QemuError("Could not create disk image {} already exist".format(path)) + + command = [qemu_img, "create", "-f", img_format] + for option in sorted(options.keys()): + command.extend(["-o", "{}={}".format(option, options[option])]) + command.append(path) + command.append("{}M".format(img_size)) + + process = yield from asyncio.create_subprocess_exec(*command) + yield from process.wait() + except (OSError, subprocess.SubprocessError) as e: + raise QemuError("Could not create disk image {}:{}".format(path, e)) diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index bd7137de..077d7469 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -22,11 +22,13 @@ order to run a QEMU VM. import sys import os +import re import shutil import subprocess import shlex import asyncio import socket +import gns3server from pkg_resources import parse_version from .qemu_error import QemuError @@ -35,7 +37,9 @@ from ..nios.nio_udp import NIOUDP from ..nios.nio_tap import NIOTAP from ..nios.nio_nat import NIONAT from ..base_vm import BaseVM -from ...schemas.qemu import QEMU_OBJECT_SCHEMA +from ...schemas.qemu import QEMU_OBJECT_SCHEMA, QEMU_PLATFORMS +from ...utils.asyncio import monitor_process +from ...utils.images import md5sum import logging log = logging.getLogger(__name__) @@ -53,39 +57,61 @@ class QemuVM(BaseVM): :param manager: Manager instance :param console: TCP console port :param qemu_path: path to the QEMU binary + :param platform: Platform to emulate :param console: TCP console port """ - def __init__(self, name, vm_id, project, manager, qemu_path=None, console=None): + def __init__(self, name, vm_id, project, manager, linked_clone=True, qemu_path=None, console=None, console_type="telnet", platform=None): - super().__init__(name, vm_id, project, manager, console=console) + super().__init__(name, vm_id, project, manager, console=console, console_type=console_type) 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._linked_clone = linked_clone self._command = [] - self._started = False self._process = None self._cpulimit_process = None self._monitor = None self._stdout_file = "" + self._execute_lock = asyncio.Lock() # QEMU VM settings - self.qemu_path = qemu_path + if qemu_path: + try: + self.qemu_path = qemu_path + except QemuError as e: + if platform: + self.platform = platform + else: + raise e + else: + self.platform = platform + self._hda_disk_image = "" self._hdb_disk_image = "" self._hdc_disk_image = "" self._hdd_disk_image = "" + self._hda_disk_interface = "ide" + self._hdb_disk_interface = "ide" + self._hdc_disk_interface = "ide" + self._hdd_disk_interface = "ide" + self._cdrom_image = "" + self._boot_priority = "c" + self._mac_address = "" self._options = "" self._ram = 256 + self._cpus = 1 self._ethernet_adapters = [] self._adapter_type = "e1000" self._initrd = "" self._kernel_image = "" self._kernel_command_line = "" self._legacy_networking = False + self._acpi_shutdown = False self._cpu_throttling = 0 # means no CPU throttling self._process_priority = "low" + self.mac_address = "" # this will generate a MAC address self.adapters = 1 # creates 1 adapter by default log.info('QEMU VM "{name}" [{id}] has been created'.format(name=self._name, id=self._id)) @@ -118,19 +144,48 @@ class QemuVM(BaseVM): """ if qemu_path and os.pathsep not in qemu_path: - qemu_path = shutil.which(qemu_path) + new_qemu_path = shutil.which(qemu_path, path=os.pathsep.join(self._manager.paths_list())) + if new_qemu_path is None: + raise QemuError("QEMU binary path {} is not found in the path".format(qemu_path)) + qemu_path = new_qemu_path + self._check_qemu_path(qemu_path) + self._qemu_path = qemu_path + self._platform = os.path.basename(qemu_path) + if self._platform == "qemu-kvm": + self._platform = "x86_64" + else: + qemu_bin = os.path.basename(qemu_path) + qemu_bin = re.sub(r'(w)?\.(exe|EXE)$', '', qemu_bin) + self._platform = re.sub(r'^qemu-system-(.*)$', r'\1', qemu_bin, re.IGNORECASE) + if self._platform.split(".")[0] not in QEMU_PLATFORMS: + raise QemuError("Platform {} is unknown".format(self._platform)) + log.info('QEMU VM "{name}" [{id}] has set the QEMU path to {qemu_path}'.format(name=self._name, + id=self._id, + qemu_path=qemu_path)) + + def _check_qemu_path(self, qemu_path): if qemu_path is None: - raise QemuError("QEMU binary path is not set or not found in the path") + raise QemuError("QEMU binary path is not set") 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}] has set the QEMU path to {qemu_path}'.format(name=self._name, - id=self._id, - qemu_path=qemu_path)) + @property + def platform(self): + """ + Return the current platform + """ + return self._platform + + @platform.setter + def platform(self, platform): + self._platform = platform + if sys.platform.startswith("win"): + self.qemu_path = "qemu-system-{}w.exe".format(platform) + else: + self.qemu_path = "qemu-system-{}".format(platform) @property def hda_disk_image(self): @@ -149,7 +204,6 @@ class QemuVM(BaseVM): :param hda_disk_image: QEMU hda disk image path """ - self._hda_disk_image = self.manager.get_abs_image_path(hda_disk_image) log.info('QEMU VM "{name}" [{id}] has set the QEMU hda disk image path to {disk_image}'.format(name=self._name, id=self._id, @@ -225,6 +279,143 @@ class QemuVM(BaseVM): id=self._id, disk_image=self._hdd_disk_image)) + @property + def hda_disk_interface(self): + """ + Returns the hda disk interface this QEMU VM. + + :returns: QEMU hda disk interface + """ + + return self._hda_disk_interface + + @hda_disk_interface.setter + def hda_disk_interface(self, hda_disk_interface): + """ + Sets the hda disk interface for this QEMU VM. + + :param hda_disk_interface: QEMU hda disk interface + """ + + self._hda_disk_interface = hda_disk_interface + log.info('QEMU VM "{name}" [{id}] has set the QEMU hda disk interface to {interface}'.format(name=self._name, + id=self._id, + interface=self._hda_disk_interface)) + + @property + def hdb_disk_interface(self): + """ + Returns the hdb disk interface this QEMU VM. + + :returns: QEMU hdb disk interface + """ + + return self._hdb_disk_interface + + @hdb_disk_interface.setter + def hdb_disk_interface(self, hdb_disk_interface): + """ + Sets the hda disk interface for this QEMU VM. + + :param hdb_disk_interface: QEMU hdb disk interface + """ + + self._hdb_disk_interface = hdb_disk_interface + log.info('QEMU VM "{name}" [{id}] has set the QEMU hdb disk interface to {interface}'.format(name=self._name, + id=self._id, + interface=self._hdb_disk_interface)) + + @property + def hdc_disk_interface(self): + """ + Returns the hdc disk interface this QEMU VM. + + :returns: QEMU hdc disk interface + """ + + return self._hdc_disk_interface + + @hdc_disk_interface.setter + def hdc_disk_interface(self, hdc_disk_interface): + """ + Sets the hdc disk interface for this QEMU VM. + + :param hdc_disk_interface: QEMU hdc disk interface + """ + + self._hdc_disk_interface = hdc_disk_interface + log.info('QEMU VM "{name}" [{id}] has set the QEMU hdc disk interface to {interface}'.format(name=self._name, + id=self._id, + interface=self._hdc_disk_interface)) + + @property + def hdd_disk_interface(self): + """ + Returns the hda disk interface this QEMU VM. + + :returns: QEMU hda disk interface + """ + + return self._hdd_disk_interface + + @hdd_disk_interface.setter + def hdd_disk_interface(self, hdd_disk_interface): + """ + Sets the hdd disk interface for this QEMU VM. + + :param hdd_disk_interface: QEMU hdd disk interface + """ + + self._hdd_disk_interface = hdd_disk_interface + log.info('QEMU VM "{name}" [{id}] has set the QEMU hdd disk interface to {interface}'.format(name=self._name, + id=self._id, + interface=self._hdd_disk_interface)) + + @property + def cdrom_image(self): + """ + Returns the cdrom image path for this QEMU VM. + + :returns: QEMU cdrom image path + """ + + return self._cdrom_image + + @cdrom_image.setter + def cdrom_image(self, cdrom_image): + """ + Sets the cdrom image for this QEMU VM. + + :param cdrom_image: QEMU cdrom image path + """ + self._cdrom_image = self.manager.get_abs_image_path(cdrom_image) + log.info('QEMU VM "{name}" [{id}] has set the QEMU cdrom image path to {cdrom_image}'.format(name=self._name, + id=self._id, + cdrom_image=self._cdrom_image)) + + @property + def boot_priority(self): + """ + Returns the boot priority for this QEMU VM. + + :returns: QEMU boot priority + """ + + return self._boot_priority + + @boot_priority.setter + def boot_priority(self, boot_priority): + """ + Sets the boot priority for this QEMU VM. + + :param boot_priority: QEMU boot priority + """ + + self._boot_priority = boot_priority + log.info('QEMU VM "{name}" [{id}] has set the boot priority to {boot_priority}'.format(name=self._name, + id=self._id, + boot_priority=self._boot_priority)) + @property def adapters(self): """ @@ -275,6 +466,33 @@ class QemuVM(BaseVM): id=self._id, adapter_type=adapter_type)) + @property + def mac_address(self): + """ + Returns the MAC address for this QEMU VM. + + :returns: adapter type (string) + """ + + return self._mac_address + + @mac_address.setter + def mac_address(self, mac_address): + """ + Sets the MAC address for this QEMU VM. + + :param mac_address: MAC address + """ + + if not mac_address: + self._mac_address = "00:00:ab:%s:%s:00" % (self.id[-4:-2], self.id[-2:]) + else: + self._mac_address = mac_address + + log.info('QEMU VM "{name}" [{id}]: MAC address changed to {mac_addr}'.format(name=self._name, + id=self._id, + mac_addr=mac_address)) + @property def legacy_networking(self): """ @@ -299,6 +517,30 @@ class QemuVM(BaseVM): log.info('QEMU VM "{name}" [{id}] has disabled legacy networking'.format(name=self._name, id=self._id)) self._legacy_networking = legacy_networking + @property + def acpi_shutdown(self): + """ + Returns either this QEMU VM can be ACPI shutdown. + + :returns: boolean + """ + + return self._acpi_shutdown + + @acpi_shutdown.setter + def acpi_shutdown(self, acpi_shutdown): + """ + Sets either this QEMU VM can be ACPI shutdown. + + :param acpi_shutdown: boolean + """ + + if acpi_shutdown: + log.info('QEMU VM "{name}" [{id}] has enabled ACPI shutdown'.format(name=self._name, id=self._id)) + else: + log.info('QEMU VM "{name}" [{id}] has disabled ACPI shutdown'.format(name=self._name, id=self._id)) + self._acpi_shutdown = acpi_shutdown + @property def cpu_throttling(self): """ @@ -369,6 +611,27 @@ class QemuVM(BaseVM): log.info('QEMU VM "{name}" [{id}] has set the RAM to {ram}'.format(name=self._name, id=self._id, ram=ram)) self._ram = ram + @property + def cpus(self): + """ + Returns the number of vCPUs this QEMU VM. + + :returns: number of vCPUs. + """ + + return self._cpus + + @cpus.setter + def cpus(self, cpus): + """ + Sets the number of vCPUs this QEMU VM. + + :param cpus: number of vCPUs. + """ + + log.info('QEMU VM "{name}" [{id}] has set the number of vCPUs to {cpus}'.format(name=self._name, id=self._id, cpus=cpus)) + self._cpus = cpus + @property def options(self): """ @@ -390,7 +653,17 @@ class QemuVM(BaseVM): log.info('QEMU VM "{name}" [{id}] has set the QEMU options to {options}'.format(name=self._name, id=self._id, options=options)) - self._options = options + + if not sys.platform.startswith("linux"): + if "-no-kvm" in options: + options = options.replace("-no-kvm", "") + if "-enable-kvm" in options: + options = options.replace("-enable-kvm", "") + elif "-icount" in options and ("-no-kvm" not in options): + # automatically add the -no-kvm option if -icount is detected + # to help with the migration of ASA VMs created before version 1.4 + options = "-no-kvm " + options + self._options = options.strip() @property def initrd(self): @@ -475,6 +748,9 @@ class QemuVM(BaseVM): Changes the process priority """ + if self._process_priority == "normal": + return + if sys.platform.startswith("win"): try: import win32api @@ -483,7 +759,7 @@ class QemuVM(BaseVM): except ImportError: log.error("pywin32 must be installed to change the priority class for QEMU VM {}".format(self._name)) else: - log.info("setting QEMU VM {} priority class to BELOW_NORMAL".format(self._name)) + log.info("Setting QEMU VM {} priority class to {}".format(self._name, self._process_priority)) handle = win32api.OpenProcess(win32con.PROCESS_ALL_ACCESS, 0, self._process.pid) if self._process_priority == "realtime": priority = win32process.REALTIME_PRIORITY_CLASS @@ -497,7 +773,10 @@ class QemuVM(BaseVM): priority = win32process.IDLE_PRIORITY_CLASS else: priority = win32process.NORMAL_PRIORITY_CLASS - win32process.SetPriorityClass(handle, priority) + try: + win32process.SetPriorityClass(handle, priority) + except win32process.error as e: + log.error('Could not change process priority for QEMU VM "{}": {}'.format(self._name, e)) else: if self._process_priority == "realtime": priority = -20 @@ -555,42 +834,73 @@ class QemuVM(BaseVM): Starts this QEMU VM. """ - if self.is_running(): - # resume the VM if it is paused - yield from self.resume() - return + with (yield from self._execute_lock): + if self.is_running(): + # resume the VM if it is paused + yield from self.resume() + return - else: + else: - if self._manager.config.get_section_config("Qemu").getboolean("monitor", True): + if self._manager.config.get_section_config("Qemu").getboolean("monitor", True): + try: + info = socket.getaddrinfo(self._monitor_host, 0, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE) + if not info: + raise QemuError("getaddrinfo returns an empty list on {}".format(self._monitor_host)) + for res in info: + af, socktype, proto, _, sa = res + # let the OS find an unused port for the Qemu monitor + with socket.socket(af, socktype, proto) as sock: + sock.bind(sa) + self._monitor = sock.getsockname()[1] + except OSError as e: + raise QemuError("Could not find free port for the Qemu monitor: {}".format(e)) + + # check if there is enough RAM to run + self.check_available_ram(self.ram) + + self._command = yield from self._build_command() + command_string = " ".join(shlex.quote(s) for s in self._command) try: - # let the OS find an unused port for the Qemu monitor - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: - sock.bind((self._monitor_host, 0)) - self._monitor = sock.getsockname()[1] - except OSError as e: - raise QemuError("Could not find free port for the Qemu monitor: {}".format(e)) - - 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") - log.info("logging to {}".format(self._stdout_file)) - with open(self._stdout_file, "w", encoding="utf-8") as fd: - self._process = yield from asyncio.create_subprocess_exec(*self._command, - stdout=fd, - stderr=subprocess.STDOUT, - cwd=self.working_dir) - log.info('QEMU VM "{}" started PID={}'.format(self._name, 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)) - - yield from self._set_process_priority() - if self._cpu_throttling: - self._set_cpu_throttling() + log.info("Starting QEMU with: {}".format(command_string)) + 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", encoding="utf-8") as fd: + fd.write("Start QEMU with {}\n\nExecution log:\n".format(command_string)) + self._process = yield from asyncio.create_subprocess_exec(*self._command, + stdout=fd, + stderr=subprocess.STDOUT, + cwd=self.working_dir) + log.info('QEMU VM "{}" started PID={}'.format(self._name, self._process.pid)) + + self.status = "started" + monitor_process(self._process, self._termination_callback) + except (OSError, subprocess.SubprocessError, UnicodeEncodeError) 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)) + + yield from self._set_process_priority() + if self._cpu_throttling: + self._set_cpu_throttling() + + if "-enable-kvm" in command_string: + self._hw_virtualization = True + + def _termination_callback(self, returncode): + """ + Called when the process has stopped. + + :param returncode: Process returncode + """ + + if self.started: + log.info("QEMU process has stopped, return code: %d", returncode) + self.status = "stopped" + self._hw_virtualization = False + self._process = None + if returncode != 0: + self.project.emit("log.error", {"message": "QEMU process has stopped, return code: {}\n{}".format(returncode, self.read_stdout())}) @asyncio.coroutine def stop(self): @@ -598,24 +908,30 @@ class QemuVM(BaseVM): Stops this QEMU VM. """ - # stop the QEMU process - if self.is_running(): - log.info('Stopping QEMU VM "{}" PID={}'.format(self._name, self._process.pid)) - try: - self._process.terminate() - yield from self._process.wait() - except subprocess.TimeoutExpired: + with (yield from self._execute_lock): + # stop the QEMU process + self._hw_virtualization = False + if self.is_running(): + log.info('Stopping QEMU VM "{}" PID={}'.format(self._name, self._process.pid)) + self.status = "stopped" try: - self._process.kill() - except OSError as e: - log.error("Cannot stop the Qemu process: {}".format(e)) - if self._process.returncode is None: - log.warn('QEMU VM "{}" with PID={} is still running'.format(self._name, self._process.pid)) - except ProcessLookupError: - pass - self._process = None - self._started = False - self._stop_cpulimit() + if self.acpi_shutdown: + yield from self._control_vm("system_powerdown") + yield from gns3server.utils.asyncio.wait_for_process_termination(self._process, timeout=30) + else: + self._process.terminate() + yield from gns3server.utils.asyncio.wait_for_process_termination(self._process, timeout=3) + except ProcessLookupError: + pass + except asyncio.TimeoutError: + try: + self._process.kill() + except ProcessLookupError: + pass + if self._process.returncode is None: + log.warn('QEMU VM "{}" PID={} is still running'.format(self._name, self._process.pid)) + self._process = None + self._stop_cpulimit() @asyncio.coroutine def _control_vm(self, command, expected=None): @@ -665,6 +981,8 @@ class QemuVM(BaseVM): """ log.debug('QEMU VM "{name}" [{id}] is closing'.format(name=self._name, id=self._id)) + self.acpi_shutdown = False + yield from self.stop() if self._console: self._manager.port_manager.release_tcp_port(self._console, self._project) @@ -823,7 +1141,7 @@ class QemuVM(BaseVM): :returns: boolean """ - return self._started + return self.status == "started" def read_stdout(self): """ @@ -870,6 +1188,14 @@ class QemuVM(BaseVM): else: return [] + def _vnc_options(self): + + if self._console: + vnc_port = self._console - 5900 # subtract by 5900 to get the display number + return ["-vnc", "{}:{}".format(self._manager.port_manager.console_host, vnc_port)] + else: + return [] + def _monitor_options(self): if self._monitor: @@ -877,10 +1203,13 @@ class QemuVM(BaseVM): else: return [] - @asyncio.coroutine - def _disk_options(self): + def _get_qemu_img(self): + """ + Search the qemu-img binary in the same binary of the qemu binary + for avoiding version incompatibily. - options = [] + :returns: qemu-img path or raise an error + """ qemu_img_path = "" qemu_path_dir = os.path.dirname(self.qemu_path) try: @@ -893,31 +1222,34 @@ class QemuVM(BaseVM): if not qemu_img_path: raise QemuError("Could not find qemu-img in {}".format(qemu_path_dir)) - try: - if self._hda_disk_image: - if not os.path.isfile(self._hda_disk_image) or not os.path.exists(self._hda_disk_image): - if os.path.islink(self._hda_disk_image): - raise QemuError("hda disk image '{}' linked to '{}' is not accessible".format(self._hda_disk_image, os.path.realpath(self._hda_disk_image))) - else: - raise QemuError("hda disk image '{}' is not accessible".format(self._hda_disk_image)) + return qemu_img_path + + @asyncio.coroutine + def _disk_options(self): + options = [] + qemu_img_path = self._get_qemu_img() + + if self._hda_disk_image: + if not os.path.isfile(self._hda_disk_image) or not os.path.exists(self._hda_disk_image): + if os.path.islink(self._hda_disk_image): + raise QemuError("hda disk image '{}' linked to '{}' is not accessible".format(self._hda_disk_image, os.path.realpath(self._hda_disk_image))) + else: + raise QemuError("hda disk image '{}' is not accessible".format(self._hda_disk_image)) + if self._linked_clone: hda_disk = os.path.join(self.working_dir, "hda_disk.qcow2") if not os.path.exists(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)) + # create the disk + try: + 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)) + except (OSError, subprocess.SubprocessError) as e: + raise QemuError("Could not create hda disk image {}".format(e)) else: - # create a "FLASH" with 256MB if no disk image has been specified - hda_disk = os.path.join(self.working_dir, "flash.qcow2") - if not os.path.exists(hda_disk): - 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 hda disk image {}".format(e)) - options.extend(["-hda", hda_disk]) + hda_disk = self._hda_disk_image + options.extend(["-drive", 'file={},if={},index=0,media=disk'.format(hda_disk, self.hda_disk_interface)]) if self._hdb_disk_image: if not os.path.isfile(self._hdb_disk_image) or not os.path.exists(self._hdb_disk_image): @@ -925,17 +1257,20 @@ class QemuVM(BaseVM): raise QemuError("hdb disk image '{}' linked to '{}' is not accessible".format(self._hdb_disk_image, os.path.realpath(self._hdb_disk_image))) else: raise QemuError("hdb disk image '{}' is not accessible".format(self._hdb_disk_image)) - hdb_disk = os.path.join(self.working_dir, "hdb_disk.qcow2") - if not os.path.exists(hdb_disk): - try: - 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 hdb disk image {}".format(e)) - options.extend(["-hdb", hdb_disk]) + if self._linked_clone: + hdb_disk = os.path.join(self.working_dir, "hdb_disk.qcow2") + if not os.path.exists(hdb_disk): + try: + 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 hdb disk image {}".format(e)) + else: + hdb_disk = self._hdb_disk_image + options.extend(["-drive", 'file={},if={},index=1,media=disk'.format(hdb_disk, self.hdb_disk_interface)]) if self._hdc_disk_image: if not os.path.isfile(self._hdc_disk_image) or not os.path.exists(self._hdc_disk_image): @@ -943,17 +1278,20 @@ class QemuVM(BaseVM): 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._linked_clone: + 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)) + else: + hdc_disk = self._hdc_disk_image + options.extend(["-drive", 'file={},if={},index=2,media=disk'.format(hdc_disk, self.hdc_disk_interface)]) if self._hdd_disk_image: if not os.path.isfile(self._hdd_disk_image) or not os.path.exists(self._hdd_disk_image): @@ -961,18 +1299,35 @@ class QemuVM(BaseVM): 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]) + if self._linked_clone: + 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)) + else: + hdd_disk = self._hdd_disk_image + options.extend(["-drive", 'file={},if={},index=3,media=disk'.format(hdd_disk, self.hdd_disk_interface)]) + + return options + def _cdrom_option(self): + + options = [] + if self._cdrom_image: + if not os.path.isfile(self._cdrom_image) or not os.path.exists(self._cdrom_image): + if os.path.islink(self._cdrom_image): + raise QemuError("cdrom image '{}' linked to '{}' is not accessible".format(self._cdrom_image, os.path.realpath(self._cdrom_image))) + else: + raise QemuError("cdrom image '{}' is not accessible".format(self._cdrom_image)) + if self._hdc_disk_image: + raise QemuError("You cannot use a disk image on hdc disk and a CDROM image at the same time") + options.extend(["-cdrom", self._cdrom_image]) return options def _linux_boot_options(self): @@ -1011,8 +1366,7 @@ class QemuVM(BaseVM): patched_qemu = True for adapter_number, adapter in enumerate(self._ethernet_adapters): - # TODO: let users specify a base mac address - mac = "00:00:ab:%s:%s:%02x" % (self.id[-4:-2], self.id[-2:], adapter_number) + mac = "%s%02x" % (self._mac_address[:-2], (int(self._mac_address[-2:]) + adapter_number) % 255) nio = adapter.get_nio(0) if self._legacy_networking: # legacy QEMU networking syntax (-net) @@ -1071,6 +1425,27 @@ class QemuVM(BaseVM): return [] return ["-nographic"] + def _run_with_kvm(self, qemu_path, options): + """ + Check if we could run qemu with KVM + + :param qemu_path: Path to qemu + :param options: String of qemu user options + :returns: Boolean True if we need to enable KVM + """ + + if sys.platform.startswith("linux") and self.manager.config.get_section_config("Qemu").getboolean("enable_kvm", True) \ + and "-no-kvm" not in options: + + # Turn OFF kvm for non x86 architectures + if os.path.basename(qemu_path) not in ["qemu-system-x86_64", "qemu-system-i386", "qemu-kvm"]: + return False + + if not os.path.exists("/dev/kvm"): + raise QemuError("KVM acceleration cannot be used (/dev/kvm doesn't exist). You can turn off KVM support in the gns3_server.conf by adding enable_kvm = false to the [Qemu] section.") + return True + return False + @asyncio.coroutine def _build_command(self): """ @@ -1080,36 +1455,60 @@ class QemuVM(BaseVM): command = [self.qemu_path] command.extend(["-name", self._name]) - command.extend(["-m", str(self._ram)]) + command.extend(["-m", "{}M".format(self._ram)]) + command.extend(["-smp", "cpus={}".format(self._cpus)]) + if self._run_with_kvm(self.qemu_path, self._options): + command.extend(["-enable-kvm"]) + command.extend(["-boot", "order={}".format(self._boot_priority)]) + cdrom_option = self._cdrom_option() + command.extend(cdrom_option) command.extend((yield from self._disk_options())) command.extend(self._linux_boot_options()) - command.extend(self._serial_options()) + if self._console_type == "telnet": + command.extend(self._serial_options()) + elif self._console_type == "vnc": + command.extend(self._vnc_options()) + else: + raise QemuError("Console type {} is unknown".format(self._console_type)) command.extend(self._monitor_options()) + command.extend((yield from self._network_options())) + command.extend(self._graphic()) additional_options = self._options.strip() if additional_options: try: command.extend(shlex.split(additional_options)) except ValueError as e: - QemuError("Invalid additional options: {} error {}".format(additional_options, e)) - command.extend((yield from self._network_options())) - command.extend(self._graphic()) + raise QemuError("Invalid additional options: {} error {}".format(additional_options, e)) return command def __json__(self): answer = { "project_id": self.project.id, - "vm_id": self.id + "vm_id": self.id, + "vm_directory": self.working_dir } # 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) + try: + answer[field] = getattr(self, field) + except AttributeError: + pass answer["hda_disk_image"] = self.manager.get_relative_image_path(self._hda_disk_image) + answer["hda_disk_image_md5sum"] = md5sum(self._hda_disk_image) answer["hdb_disk_image"] = self.manager.get_relative_image_path(self._hdb_disk_image) + answer["hdb_disk_image_md5sum"] = md5sum(self._hdb_disk_image) answer["hdc_disk_image"] = self.manager.get_relative_image_path(self._hdc_disk_image) + answer["hdc_disk_image_md5sum"] = md5sum(self._hdc_disk_image) answer["hdd_disk_image"] = self.manager.get_relative_image_path(self._hdd_disk_image) + answer["hdd_disk_image_md5sum"] = md5sum(self._hdd_disk_image) + answer["cdrom_image"] = self.manager.get_relative_image_path(self._cdrom_image) + answer["cdrom_image_md5sum"] = md5sum(self._cdrom_image) answer["initrd"] = self.manager.get_relative_image_path(self._initrd) + answer["initrd_md5sum"] = md5sum(self._initrd) + answer["kernel_image"] = self.manager.get_relative_image_path(self._kernel_image) + answer["kernel_image_md5sum"] = md5sum(self._kernel_image) return answer diff --git a/gns3server/modules/virtualbox/__init__.py b/gns3server/modules/virtualbox/__init__.py index 0b7ed20c..7f9e424f 100644 --- a/gns3server/modules/virtualbox/__init__.py +++ b/gns3server/modules/virtualbox/__init__.py @@ -92,15 +92,10 @@ class VirtualBox(BaseManager): vboxmanage_path = self.find_vboxmanage() command = [vboxmanage_path, "--nologo", subcommand] command.extend(args) - log.debug("Executing VBoxManage with command: {}".format(command)) + command_string = " ".join(command) + log.info("Executing VBoxManage with command: {}".format(command_string)) try: - 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) + 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)) @@ -160,7 +155,7 @@ class VirtualBox(BaseManager): continue @asyncio.coroutine - def get_list(self): + def list_images(self): """ Gets VirtualBox VM list. """ @@ -169,7 +164,7 @@ class VirtualBox(BaseManager): result = yield from self.execute("list", ["vms"]) for line in result: if len(line) == 0 or line[0] != '"' or line[-1:] != "}": - continue # Broken output (perhaps a carriage return in VM name + continue # Broken output (perhaps a carriage return in VM name) vmname, _ = line.rsplit(' ', 1) vmname = vmname.strip('"') if vmname == "": diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 350bd92e..40181b12 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -29,11 +29,12 @@ import socket import asyncio from pkg_resources import parse_version +from gns3server.utils.telnet_server import TelnetServer +from gns3server.utils.asyncio import wait_for_file_creation, wait_for_named_pipe_creation from .virtualbox_error import VirtualBoxError from ..nios.nio_udp import NIOUDP from ..nios.nio_nat import NIONAT 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'): @@ -65,6 +66,7 @@ class VirtualBoxVM(BaseVM): self._adapters = adapters self._ethernet_adapters = {} self._headless = False + self._acpi_shutdown = False self._enable_remote_console = False self._vmname = vmname self._use_any_adapter = False @@ -73,17 +75,23 @@ class VirtualBoxVM(BaseVM): def __json__(self): - return {"name": self.name, + json = {"name": self.name, "vm_id": self.id, "console": self.console, "project_id": self.project.id, "vmname": self.vmname, "headless": self.headless, + "acpi_shutdown": self.acpi_shutdown, "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} + if self._linked_clone: + json["vm_directory"] = self.working_dir + else: + json["vm_directory"] = None + return json @asyncio.coroutine def _get_system_properties(self): @@ -142,7 +150,7 @@ class VirtualBoxVM(BaseVM): yield from self._get_system_properties() if "API version" not in self._system_properties: - raise VirtualBoxError("Can't access to VirtualBox API Version") + raise VirtualBoxError("Can't access to VirtualBox API version:\n{}".format(self._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)) @@ -162,6 +170,19 @@ class VirtualBoxVM(BaseVM): if "memory" in vm_info: self._ram = int(vm_info["memory"]) + @asyncio.coroutine + def check_hw_virtualization(self): + """ + Returns either hardware virtualization is activated or not. + + :returns: boolean + """ + + vm_info = yield from self._get_vm_info() + if "hwvirtex" in vm_info and vm_info["hwvirtex"] == "on": + return True + return False + @asyncio.coroutine def start(self): """ @@ -174,13 +195,16 @@ class VirtualBoxVM(BaseVM): yield from 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") + # VM must be powered off to start it + if vm_state != "poweroff": + raise VirtualBoxError("VirtualBox VM not powered off") yield from self._set_network_options() yield from self._set_serial_console() + # check if there is enough RAM to run + self.check_available_ram(self.ram) + args = [self._vmname] if self._headless: args.extend(["--type", "headless"]) @@ -194,22 +218,39 @@ class VirtualBoxVM(BaseVM): yield from self.manager.execute("guestproperty", ["set", self._vmname, "ProjectDirInGNS3", self.working_dir]) if self._enable_remote_console and self._console is not None: + try: + # wait for VirtualBox to create the pipe file. + if sys.platform.startswith("win"): + yield from wait_for_named_pipe_creation(self._get_pipe_name()) + else: + yield from wait_for_file_creation(self._get_pipe_name()) + except asyncio.TimeoutError: + raise VirtualBoxError('Pipe file "{}" for remote console has not been created by VirtualBox'.format(self._get_pipe_name())) self._start_remote_console() + if (yield from self.check_hw_virtualization()): + self._hw_virtualization = True + @asyncio.coroutine def stop(self): """ Stops this VirtualBox VM. """ + self._hw_virtualization = False 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)) + if self.acpi_shutdown: + # use ACPI to shutdown the VM + result = yield from self._control_vm("acpipowerbutton") + log.debug("ACPI shutdown result: {}".format(result)) + else: + # power off the VM + result = yield from self._control_vm("poweroff") + log.debug("Stop result: {}".format(result)) + log.info("VirtualBox VM '{name}' [{id}] stopped".format(name=self.name, id=self.id)) # yield from asyncio.sleep(0.5) # give some time for VirtualBox to unlock the VM try: # deactivate the first serial port @@ -318,6 +359,7 @@ class VirtualBoxVM(BaseVM): if nio and isinstance(nio, NIOUDP): self.manager.port_manager.release_udp_port(nio.lport, self._project) + self.acpi_shutdown = False yield from self.stop() if self._linked_clone: @@ -389,6 +431,30 @@ class VirtualBoxVM(BaseVM): log.info("VirtualBox VM '{name}' [{id}] has disabled the headless mode".format(name=self.name, id=self.id)) self._headless = headless + @property + def acpi_shutdown(self): + """ + Returns either the VM will use ACPI shutdown + + :returns: boolean + """ + + return self._acpi_shutdown + + @acpi_shutdown.setter + def acpi_shutdown(self, acpi_shutdown): + """ + Sets either the VM will use ACPI shutdown + + :param acpi_shutdown: boolean + """ + + if acpi_shutdown: + log.info("VirtualBox VM '{name}' [{id}] has enabled the ACPI shutdown mode".format(name=self.name, id=self.id)) + else: + log.info("VirtualBox VM '{name}' [{id}] has disabled the ACPI shutdown mode".format(name=self.name, id=self.id)) + self._acpi_shutdown = acpi_shutdown + @property def enable_remote_console(self): """ @@ -504,7 +570,7 @@ class VirtualBoxVM(BaseVM): """ Returns either GNS3 can use any VirtualBox adapter on this instance. - :returns: index + :returns: boolean """ return self._use_any_adapter @@ -520,7 +586,7 @@ class VirtualBoxVM(BaseVM): 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)) + log.info("VirtualBox VM '{name}' [{id}] is not allowed to use any adapter".format(name=self.name, id=self.id)) self._use_any_adapter = use_any_adapter @property diff --git a/gns3server/modules/vmware/__init__.py b/gns3server/modules/vmware/__init__.py new file mode 100644 index 00000000..72f1761e --- /dev/null +++ b/gns3server/modules/vmware/__init__.py @@ -0,0 +1,588 @@ +# -*- 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 . + +""" +VMware player/workstation server module. +""" + +import os +import sys +import re +import shutil +import asyncio +import subprocess +import logging +import codecs + +from collections import OrderedDict +from gns3server.utils.interfaces import interfaces +from gns3server.utils.asyncio import subprocess_check_output + +log = logging.getLogger(__name__) + +from ..base_manager import BaseManager +from .vmware_vm import VMwareVM +from .vmware_error import VMwareError +from .nio_vmnet import NIOVMNET + + +class VMware(BaseManager): + + _VM_CLASS = VMwareVM + + def __init__(self): + + super().__init__() + self._execute_lock = asyncio.Lock() + self._vmrun_path = None + self._vmnets = [] + self._vmnet_start_range = 2 + if sys.platform.startswith("win"): + self._vmnet_end_range = 19 + else: + self._vmnet_end_range = 255 + + @property + def vmrun_path(self): + """ + Returns the path vmrun utility. + + :returns: path + """ + + return self._vmrun_path + + @staticmethod + def _find_vmrun_registry(regkey): + + import winreg + try: + # default path not used, let's look in the registry + hkey = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, regkey) + install_path, _ = winreg.QueryValueEx(hkey, "InstallPath") + vmrun_path = os.path.join(install_path, "vmrun.exe") + winreg.CloseKey(hkey) + if os.path.exists(vmrun_path): + return vmrun_path + except OSError: + pass + return None + + def find_vmrun(self): + """ + Searches for vmrun. + + :returns: path to vmrun + """ + + # look for vmrun + vmrun_path = self.config.get_section_config("VMware").get("vmrun_path") + if not vmrun_path: + if sys.platform.startswith("win"): + vmrun_path = shutil.which("vmrun") + if vmrun_path is None: + # look for vmrun.exe in default VMware Workstation directory + vmrun_ws = os.path.expandvars(r"%PROGRAMFILES(X86)%\VMware\VMware Workstation\vmrun.exe") + if os.path.exists(vmrun_ws): + vmrun_path = vmrun_ws + else: + # look for vmrun.exe using the directory listed in the registry + vmrun_path = self._find_vmrun_registry(r"SOFTWARE\Wow6432Node\VMware, Inc.\VMware Workstation") + if vmrun_path is None: + # look for vmrun.exe in default VMware VIX directory + vmrun_vix = os.path.expandvars(r"%PROGRAMFILES(X86)%\VMware\VMware VIX\vmrun.exe") + if os.path.exists(vmrun_vix): + vmrun_path = vmrun_vix + else: + # look for vmrun.exe using the directory listed in the registry + vmrun_path = self._find_vmrun_registry(r"SOFTWARE\Wow6432Node\VMware, Inc.\VMware VIX") + elif sys.platform.startswith("darwin"): + vmrun_path = "/Applications/VMware Fusion.app/Contents/Library/vmrun" + else: + vmrun_path = shutil.which("vmrun") + + if not vmrun_path: + raise VMwareError("Could not find vmrun") + if not os.path.isfile(vmrun_path): + raise VMwareError("vmrun {} is not accessible".format(vmrun_path)) + if not os.access(vmrun_path, os.X_OK): + raise VMwareError("vmrun is not executable") + if os.path.basename(vmrun_path) not in ["vmrun", "vmrun.exe"]: + raise VMwareError("Invalid vmrun executable name {}".format(os.path.basename(vmrun_path))) + + self._vmrun_path = vmrun_path + return vmrun_path + + @staticmethod + def _find_vmware_version_registry(regkey): + + import winreg + version = None + try: + # default path not used, let's look in the registry + hkey = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, regkey) + version, _ = winreg.QueryValueEx(hkey, "ProductVersion") + winreg.CloseKey(hkey) + except OSError: + pass + if version is not None: + match = re.search("([0-9]+)\.", version) + if match: + version = match.group(1) + return version + + @asyncio.coroutine + def check_vmware_version(self): + """ + Check VMware version + """ + + if sys.platform.startswith("win"): + # look for vmrun.exe using the directory listed in the registry + ws_version = self._find_vmware_version_registry(r"SOFTWARE\Wow6432Node\VMware, Inc.\VMware Workstation") + if ws_version is None: + player_version = self._find_vmware_version_registry(r"SOFTWARE\Wow6432Node\VMware, Inc.\VMware Player") + if player_version: + log.debug("VMware Player version {} detected".format(player_version)) + if int(player_version) < 6: + raise VMwareError("Using VMware Player requires version 6 or above") + else: + log.warning("Could not find VMware version") + else: + log.debug("VMware Workstation version {} detected".format(ws_version)) + if int(ws_version) < 10: + raise VMwareError("Using VMware Workstation requires version 10 or above") + return + else: + if sys.platform.startswith("darwin"): + return # FIXME: no version checking on Mac OS X + + vmware_path = VMware._get_linux_vmware_binary() + if vmware_path is None: + raise VMwareError("VMware is not installed (vmware or vmplayer executable could not be found in $PATH)") + + try: + output = yield from subprocess_check_output(vmware_path, "-v") + match = re.search("VMware Workstation ([0-9]+)\.", output) + version = None + if match: + version = match.group(1) + log.debug("VMware Workstation version {} detected".format(version)) + if int(version) < 10: + raise VMwareError("Using VMware Workstation requires version 10 or above") + match = re.search("VMware Player ([0-9]+)\.", output) + if match: + version = match.group(1) + log.debug("VMware Player version {} detected".format(version)) + if int(version) < 6: + raise VMwareError("Using VMware Player requires version 6 or above") + if version is None: + log.warning("Could not find VMware version") + except (OSError, subprocess.SubprocessError) as e: + log.error("Error while looking for the VMware version: {}".format(e)) + + @staticmethod + def _get_vmnet_interfaces_registry(): + + import winreg + vmnet_interfaces = [] + regkey = r"SOFTWARE\Wow6432Node\VMware, Inc.\VMnetLib\VMnetConfig" + try: + hkey = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, regkey) + for index in range(winreg.QueryInfoKey(hkey)[0]): + vmnet = winreg.EnumKey(hkey, index) + hkeyvmnet = winreg.OpenKey(hkey, vmnet) + if winreg.QueryInfoKey(hkeyvmnet)[1]: + # the vmnet has not been configure if the key has no values + vmnet = vmnet.replace("vm", "VM") + if vmnet not in ("VMnet1", "VMnet8"): + vmnet_interfaces.append(vmnet) + winreg.CloseKey(hkeyvmnet) + winreg.CloseKey(hkey) + except OSError as e: + raise VMwareError("Could not read registry key {}: {}".format(regkey, e)) + return vmnet_interfaces + + @staticmethod + def _get_vmnet_interfaces(): + + if sys.platform.startswith("win"): + return VMware._get_vmnet_interfaces_registry() + elif sys.platform.startswith("darwin"): + vmware_networking_file = "/Library/Preferences/VMware Fusion/networking" + else: + # location on Linux + vmware_networking_file = "/etc/vmware/networking" + vmnet_interfaces = [] + try: + with open(vmware_networking_file, "r", encoding="utf-8") as f: + for line in f.read().splitlines(): + match = re.search("VNET_([0-9]+)_VIRTUAL_ADAPTER", line) + if match: + vmnet = "vmnet{}".format(match.group(1)) + if vmnet not in ("vmnet1", "vmnet8"): + vmnet_interfaces.append(vmnet) + except OSError as e: + raise VMwareError("Cannot open {}: {}".format(vmware_networking_file, e)) + return vmnet_interfaces + + @staticmethod + def _get_vmnet_interfaces_ubridge(): + + vmnet_interfaces = [] + for interface in interfaces(): + if sys.platform.startswith("win"): + if "netcard" in interface: + windows_name = interface["netcard"] + else: + windows_name = interface["name"] + match = re.search("(VMnet[0-9]+)", windows_name) + if match: + vmnet = match.group(1) + if vmnet not in ("VMnet1", "VMnet8"): + vmnet_interfaces.append(vmnet) + elif interface["name"].startswith("vmnet"): + vmnet = interface["name"] + if vmnet not in ("vmnet1", "vmnet8"): + vmnet_interfaces.append(interface["name"]) + return vmnet_interfaces + + def is_managed_vmnet(self, vmnet): + + self._vmnet_start_range = self.config.get_section_config("VMware").getint("vmnet_start_range", self._vmnet_start_range) + self._vmnet_end_range = self.config.get_section_config("VMware").getint("vmnet_end_range", self._vmnet_end_range) + match = re.search("vmnet([0-9]+)$", vmnet, re.IGNORECASE) + if match: + vmnet_number = match.group(1) + if self._vmnet_start_range <= int(vmnet_number) <= self._vmnet_end_range: + return True + return False + + def allocate_vmnet(self): + + if not self._vmnets: + raise VMwareError("No VMnet interface available between vmnet{} and vmnet{}. Go to preferences VMware / Network / Configure to add more interfaces.".format(self._vmnet_start_range, self._vmnet_end_range)) + return self._vmnets.pop(0) + + def refresh_vmnet_list(self, ubridge=True): + + if ubridge: + # VMnet host adapters must be present when uBridge is used + vmnet_interfaces = self._get_vmnet_interfaces_ubridge() + else: + vmnet_interfaces = self._get_vmnet_interfaces() + + # remove vmnets already in use + for vm in self._vms.values(): + for used_vmnet in vm.vmnets: + if used_vmnet in vmnet_interfaces: + log.debug("{} is already in use".format(used_vmnet)) + vmnet_interfaces.remove(used_vmnet) + + # remove vmnets that are not managed + for vmnet in vmnet_interfaces.copy(): + if vmnet in vmnet_interfaces and self.is_managed_vmnet(vmnet) is False: + vmnet_interfaces.remove(vmnet) + + self._vmnets = vmnet_interfaces + + def create_nio(self, executable, nio_settings): + + if nio_settings["type"] == "nio_vmnet": + return NIOVMNET(nio_settings["vmnet"]) + else: + return super().create_nio(None, nio_settings) + + @property + def host_type(self): + """ + Returns the VMware host type. + player = VMware player + ws = VMware Workstation + fusion = VMware Fusion + + :returns: host type (string) + """ + + if sys.platform.startswith("darwin"): + host_type = "fusion" + else: + host_type = self.config.get_section_config("VMware").get("host_type", "ws") + return host_type + + @asyncio.coroutine + def execute(self, subcommand, args, timeout=120, host_type=None): + + with (yield from self._execute_lock): + vmrun_path = self.vmrun_path + if not vmrun_path: + vmrun_path = self.find_vmrun() + if host_type is None: + host_type = self.host_type + + command = [vmrun_path, "-T", host_type, subcommand] + command.extend(args) + command_string = " ".join(command) + log.info("Executing vmrun with command: {}".format(command_string)) + try: + process = yield from asyncio.create_subprocess_exec(*command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) + except (OSError, subprocess.SubprocessError) as e: + raise VMwareError("Could not execute vmrun: {}".format(e)) + + try: + stdout_data, _ = yield from asyncio.wait_for(process.communicate(), timeout=timeout) + except asyncio.TimeoutError: + raise VMwareError("vmrun has timed out after {} seconds!".format(timeout)) + + if process.returncode: + # vmrun print errors on stdout + vmrun_error = stdout_data.decode("utf-8", errors="ignore") + raise VMwareError("vmrun has returned an error: {}".format(vmrun_error)) + + return stdout_data.decode("utf-8", errors="ignore").splitlines() + + @staticmethod + def parse_vmware_file(path): + """ + Parses a VMware file (VMX, preferences or inventory). + + :param path: path to the VMware file + + :returns: dict + """ + + pairs = OrderedDict() + encoding = "utf-8" + # get the first line to read the .encoding value + with open(path, "rb") as f: + line = f.readline().decode(encoding, errors="ignore") + if line.startswith("#!"): + # skip the shebang + line = f.readline().decode(encoding, errors="ignore") + try: + key, value = line.split('=', 1) + if key.strip().lower() == ".encoding": + file_encoding = value.strip('" ') + try: + codecs.lookup(file_encoding) + encoding = file_encoding + except LookupError: + log.warning("Invalid file encoding detected in '{}': {}".format(path, file_encoding)) + except ValueError: + log.warning("Couldn't find file encoding in {}, using {}...".format(path, encoding)) + + # read the file with the correct encoding + with open(path, encoding=encoding, errors="ignore") as f: + for line in f.read().splitlines(): + try: + key, value = line.split('=', 1) + pairs[key.strip().lower()] = value.strip('" ') + except ValueError: + continue + return pairs + + @staticmethod + def write_vmware_file(path, pairs): + """ + Write a VMware file (excepting VMX file). + + :param path: path to the VMware file + :param pairs: settings to write + """ + + encoding = "utf-8" + if ".encoding" in pairs: + file_encoding = pairs[".encoding"] + try: + codecs.lookup(file_encoding) + encoding = file_encoding + except LookupError: + log.warning("Invalid file encoding detected in '{}': {}".format(path, file_encoding)) + with open(path, "w", encoding=encoding, errors="ignore") as f: + for key, value in pairs.items(): + entry = '{} = "{}"\n'.format(key, value) + f.write(entry) + + @staticmethod + def write_vmx_file(path, pairs): + """ + Write a VMware VMX file. + + :param path: path to the VMX file + :param pairs: settings to write + """ + + encoding = "utf-8" + if ".encoding" in pairs: + file_encoding = pairs[".encoding"] + try: + codecs.lookup(file_encoding) + encoding = file_encoding + except LookupError: + log.warning("Invalid file encoding detected in '{}': {}".format(path, file_encoding)) + with open(path, "w", encoding=encoding, errors="ignore") as f: + if sys.platform.startswith("linux"): + # write the shebang on the first line on Linux + vmware_path = VMware._get_linux_vmware_binary() + if vmware_path: + f.write("#!{}\n".format(vmware_path)) + for key, value in pairs.items(): + entry = '{} = "{}"\n'.format(key, value) + f.write(entry) + + def _get_vms_from_inventory(self, inventory_path): + """ + Searches for VMs by parsing a VMware inventory file. + + :param inventory_path: path to the inventory file + + :returns: list of VMs + """ + + vm_entries = {} + vms = [] + try: + log.debug('Reading VMware inventory file "{}"'.format(inventory_path)) + pairs = self.parse_vmware_file(inventory_path) + for key, value in pairs.items(): + if key.startswith("vmlist"): + try: + vm_entry, variable_name = key.split('.', 1) + except ValueError: + continue + if vm_entry not in vm_entries: + vm_entries[vm_entry] = {} + vm_entries[vm_entry][variable_name.strip()] = value + except OSError as e: + log.warning("Could not read VMware inventory file {}: {}".format(inventory_path, e)) + + for vm_settings in vm_entries.values(): + if "displayname" in vm_settings and "config" in vm_settings: + log.debug('Found VM named "{}" with VMX file "{}"'.format(vm_settings["displayname"], vm_settings["config"])) + vms.append({"vmname": vm_settings["displayname"], "vmx_path": vm_settings["config"]}) + return vms + + def _get_vms_from_directory(self, directory): + """ + Searches for VMs in a given directory. + + :param directory: path to the directory + + :returns: list of VMs + """ + + vms = [] + for path, _, filenames in os.walk(directory): + for filename in filenames: + if os.path.splitext(filename)[1] == ".vmx": + vmx_path = os.path.join(path, filename) + log.debug('Reading VMware VMX file "{}"'.format(vmx_path)) + try: + pairs = self.parse_vmware_file(vmx_path) + if "displayname" in pairs: + log.debug('Found VM named "{}"'.format(pairs["displayname"])) + vms.append({"vmname": pairs["displayname"], "vmx_path": vmx_path}) + except OSError as e: + log.warning('Could not read VMware VMX file "{}": {}'.format(vmx_path, e)) + continue + return vms + + @staticmethod + def get_vmware_inventory_path(): + """ + Returns VMware inventory file path. + + :returns: path to the inventory file + """ + + if sys.platform.startswith("win"): + return os.path.expandvars(r"%APPDATA%\Vmware\Inventory.vmls") + elif sys.platform.startswith("darwin"): + return os.path.expanduser("~/Library/Application Support/VMware Fusion/vmInventory") + else: + return os.path.expanduser("~/.vmware/inventory.vmls") + + @staticmethod + def get_vmware_preferences_path(): + """ + Returns VMware preferences file path. + + :returns: path to the preferences file + """ + + if sys.platform.startswith("win"): + return os.path.expandvars(r"%APPDATA%\VMware\preferences.ini") + elif sys.platform.startswith("darwin"): + return os.path.expanduser("~/Library/Preferences/VMware Fusion/preferences") + else: + return os.path.expanduser("~/.vmware/preferences") + + @staticmethod + def get_vmware_default_vm_path(): + """ + Returns VMware default VM directory path. + + :returns: path to the default VM directory + """ + + if sys.platform.startswith("win"): + return os.path.expandvars(r"%USERPROFILE%\Documents\Virtual Machines") + elif sys.platform.startswith("darwin"): + return os.path.expanduser("~/Documents/Virtual Machines.localized") + else: + return os.path.expanduser("~/vmware") + + @asyncio.coroutine + def list_vms(self): + """ + Gets VMware VM list. + """ + + # check for the right VMware version + yield from self.check_vmware_version() + + inventory_path = self.get_vmware_inventory_path() + if os.path.exists(inventory_path): + # FIXME: inventory may exist if VMware workstation has not been fully uninstalled, therefore VMware player VMs are not searched + return self._get_vms_from_inventory(inventory_path) + else: + # VMware player has no inventory file, let's search the default location for VMs. + vmware_preferences_path = self.get_vmware_preferences_path() + default_vm_path = self.get_vmware_default_vm_path() + + if os.path.exists(vmware_preferences_path): + # the default vm path may be present in VMware preferences file. + try: + pairs = self.parse_vmware_file(vmware_preferences_path) + if "prefvmx.defaultvmpath" in pairs: + default_vm_path = pairs["prefvmx.defaultvmpath"] + except OSError as e: + log.warning('Could not read VMware preferences file "{}": {}'.format(vmware_preferences_path, e)) + + if not os.path.isdir(default_vm_path): + raise VMwareError('Could not find the default VM directory: "{}"'.format(default_vm_path)) + return self._get_vms_from_directory(default_vm_path) + + @staticmethod + def _get_linux_vmware_binary(): + """ + Return the path of the vmware binary on Linux or None + """ + path = shutil.which("vmware") + if path is None: + path = shutil.which("vmplayer") + return path diff --git a/gns3server/modules/vmware/nio_vmnet.py b/gns3server/modules/vmware/nio_vmnet.py new file mode 100644 index 00000000..13e2c211 --- /dev/null +++ b/gns3server/modules/vmware/nio_vmnet.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 . + +""" +Interface for VMnet NIOs. +""" + +from ..nios.nio import NIO + + +class NIOVMNET(NIO): + + """ + VMnet NIO. + """ + + def __init__(self, vmnet): + + super().__init__() + self._vmnet = vmnet + + @property + def vmnet(self): + """ + Returns vmnet interface used by this NIO. + + :returns: vmnet interface name + """ + + return self._vmnet + + def __str__(self): + + return "NIO VMNET" + + def __json__(self): + + return {"type": "nio_vmnet", + "vmnet": self._vmnet} diff --git a/gns3server/modules/vmware/vmware_error.py b/gns3server/modules/vmware/vmware_error.py new file mode 100644 index 00000000..88ea393c --- /dev/null +++ b/gns3server/modules/vmware/vmware_error.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2015 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Custom exceptions for the VMware module. +""" + +from ..vm_error import VMError + + +class VMwareError(VMError): + + pass diff --git a/gns3server/modules/vmware/vmware_vm.py b/gns3server/modules/vmware/vmware_vm.py new file mode 100644 index 00000000..7f25821e --- /dev/null +++ b/gns3server/modules/vmware/vmware_vm.py @@ -0,0 +1,1013 @@ +# -*- 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 . + +""" +VMware VM instance. +""" + +import sys +import os +import socket +import asyncio +import tempfile + +from gns3server.utils.telnet_server import TelnetServer +from gns3server.utils.interfaces import get_windows_interfaces +from gns3server.utils.asyncio import wait_for_file_creation, wait_for_named_pipe_creation +from collections import OrderedDict +from .vmware_error import VMwareError +from ..nios.nio_udp import NIOUDP +from ..nios.nio_nat import NIONAT +from .nio_vmnet import NIOVMNET +from ..adapters.ethernet_adapter import EthernetAdapter +from ..base_vm import BaseVM + +if sys.platform.startswith('win'): + import msvcrt + import win32file + +import logging +log = logging.getLogger(__name__) + + +class VMwareVM(BaseVM): + + """ + VMware VM implementation. + """ + + def __init__(self, name, vm_id, project, manager, vmx_path, linked_clone, console=None): + + super().__init__(name, vm_id, project, manager, console=console) + + self._linked_clone = linked_clone + self._vmx_pairs = OrderedDict() + self._telnet_server_thread = None + self._serial_pipe = None + self._vmnets = [] + self._maximum_adapters = 10 + self._started = False + self._closed = False + + # VMware VM settings + self._headless = False + self._vmx_path = vmx_path + self._enable_remote_console = False + self._acpi_shutdown = False + self._adapters = 0 + self._ethernet_adapters = {} + self._adapter_type = "e1000" + self._use_ubridge = True + self._use_any_adapter = False + + if not os.path.exists(vmx_path): + raise VMwareError('VMware VM "{name}" [{id}]: could not find VMX file "{vmx_path}"'.format(name=name, id=vm_id, vmx_path=vmx_path)) + + def __json__(self): + + json = {"name": self.name, + "vm_id": self.id, + "console": self.console, + "project_id": self.project.id, + "vmx_path": self.vmx_path, + "headless": self.headless, + "acpi_shutdown": self.acpi_shutdown, + "enable_remote_console": self.enable_remote_console, + "adapters": self._adapters, + "adapter_type": self.adapter_type, + "use_ubridge": self.use_ubridge, + "use_any_adapter": self.use_any_adapter, + "vm_directory": self.working_dir} + return json + + @property + def vmnets(self): + + return self._vmnets + + @asyncio.coroutine + def _control_vm(self, subcommand, *additional_args): + + args = [self._vmx_path] + args.extend(additional_args) + result = yield from self.manager.execute(subcommand, args) + log.debug("Control VM '{}' result: {}".format(subcommand, result)) + return result + + def _read_vmx_file(self): + """ + Reads from the VMware VMX file corresponding to this VM. + """ + + try: + self._vmx_pairs = self.manager.parse_vmware_file(self._vmx_path) + except OSError as e: + raise VMwareError('Could not read VMware VMX file "{}": {}'.format(self._vmx_path, e)) + + def _write_vmx_file(self): + """ + Writes pairs to the VMware VMX file corresponding to this VM. + """ + + try: + self.manager.write_vmx_file(self._vmx_path, self._vmx_pairs) + except OSError as e: + raise VMwareError('Could not write VMware VMX file "{}": {}'.format(self._vmx_path, e)) + + @asyncio.coroutine + def is_running(self): + + result = yield from self.manager.execute("list", []) + if self._vmx_path in result: + return True + return False + + @asyncio.coroutine + def create(self): + """ + Creates this VM and handle linked clones. + """ + + if self._linked_clone and not os.path.exists(os.path.join(self.working_dir, os.path.basename(self._vmx_path))): + # create the base snapshot for linked clones + base_snapshot_name = "GNS3 Linked Base for clones" + vmsd_path = os.path.splitext(self._vmx_path)[0] + ".vmsd" + if not os.path.exists(vmsd_path): + raise VMwareError("{} doesn't not exist".format(vmsd_path)) + try: + vmsd_pairs = self.manager.parse_vmware_file(vmsd_path) + except OSError as e: + raise VMwareError('Could not read VMware VMSD file "{}": {}'.format(vmsd_path, e)) + gns3_snapshot_exists = False + for value in vmsd_pairs.values(): + if value == base_snapshot_name: + gns3_snapshot_exists = True + break + if not gns3_snapshot_exists: + log.info("Creating snapshot '{}'".format(base_snapshot_name)) + yield from self._control_vm("snapshot", base_snapshot_name) + + # create the linked clone based on the base snapshot + new_vmx_path = os.path.join(self.working_dir, self.name + ".vmx") + yield from self._control_vm("clone", + new_vmx_path, + "linked", + "-snapshot={}".format(base_snapshot_name), + "-cloneName={}".format(self.name)) + + try: + vmsd_pairs = self.manager.parse_vmware_file(vmsd_path) + except OSError as e: + raise VMwareError('Could not read VMware VMSD file "{}": {}'.format(vmsd_path, e)) + + snapshot_name = None + for name, value in vmsd_pairs.items(): + if value == base_snapshot_name: + snapshot_name = name.split(".", 1)[0] + break + + if snapshot_name is None: + raise VMwareError("Could not find the linked base snapshot in {}".format(vmsd_path)) + + num_clones_entry = "{}.numClones".format(snapshot_name) + if num_clones_entry in vmsd_pairs: + try: + nb_of_clones = int(vmsd_pairs[num_clones_entry]) + except ValueError: + raise VMwareError("Value of {} in {} is not a number".format(num_clones_entry, vmsd_path)) + vmsd_pairs[num_clones_entry] = str(nb_of_clones - 1) + + for clone_nb in range(0, nb_of_clones): + clone_entry = "{}.clone{}".format(snapshot_name, clone_nb) + if clone_entry in vmsd_pairs: + del vmsd_pairs[clone_entry] + + try: + self.manager.write_vmware_file(vmsd_path, vmsd_pairs) + except OSError as e: + raise VMwareError('Could not write VMware VMSD file "{}": {}'.format(vmsd_path, e)) + + # update the VMX file path + self._vmx_path = new_vmx_path + + def _get_vmx_setting(self, name, value=None): + + if name in self._vmx_pairs: + if value is not None: + if self._vmx_pairs[name] == value: + return value + else: + return self._vmx_pairs[name] + return None + + def _set_network_options(self): + """ + Set up VMware networking. + """ + + # first some sanity checks + for adapter_number in range(0, self._adapters): + + # we want the vmnet interface to be connected when starting the VM + connected = "ethernet{}.startConnected".format(adapter_number) + if self._get_vmx_setting(connected): + del self._vmx_pairs[connected] + + # check for adapter type + if self._adapter_type != "default": + adapter_type = "ethernet{}.virtualdev".format(adapter_number) + if adapter_type in self._vmx_pairs and self._vmx_pairs[adapter_type] != self._adapter_type: + raise VMwareError("Existing VMware network adapter {} is not of type {}, please fix or set adapter type to default in GNS3".format(adapter_number, + self._adapter_type)) + + # # check if any vmnet interface managed by GNS3 is being used on existing VMware adapters + # if self._get_vmx_setting("ethernet{}.present".format(adapter_number), "TRUE"): + # connection_type = "ethernet{}.connectiontype".format(adapter_number) + # if connection_type in self._vmx_pairs and self._vmx_pairs[connection_type] in ("hostonly", "custom"): + # vnet = "ethernet{}.vnet".format(adapter_number) + # if vnet in self._vmx_pairs: + # vmnet = os.path.basename(self._vmx_pairs[vnet]) + # #nio = self._ethernet_adapters[adapter_number].get_nio(0) + # if self.manager.is_managed_vmnet(vmnet): + # raise VMwareError("Network adapter {} is already associated with VMnet interface {} which is managed by GNS3, please remove".format(adapter_number, vmnet)) + + # then configure VMware network adapters + self.manager.refresh_vmnet_list(ubridge=self._use_ubridge) + for adapter_number in range(0, self._adapters): + + # add/update the interface + ethernet_adapter = {"ethernet{}.present".format(adapter_number): "TRUE", + "ethernet{}.addresstype".format(adapter_number): "generated", + "ethernet{}.generatedaddressoffset".format(adapter_number): "0"} + self._vmx_pairs.update(ethernet_adapter) + if self._adapter_type != "default": + self._vmx_pairs["ethernet{}.virtualdev".format(adapter_number)] = self._adapter_type + + connection_type = "ethernet{}.connectiontype".format(adapter_number) + if not self._use_any_adapter and connection_type in self._vmx_pairs and self._vmx_pairs[connection_type] in ("nat", "bridged", "hostonly"): + continue + self._vmx_pairs["ethernet{}.connectiontype".format(adapter_number)] = "custom" + + if self._use_ubridge: + # make sure we have a vmnet per adapter if we use uBridge + allocate_vmnet = False + + # first check if a vmnet is already assigned to the adapter + vnet = "ethernet{}.vnet".format(adapter_number) + if vnet in self._vmx_pairs: + vmnet = os.path.basename(self._vmx_pairs[vnet]) + if self.manager.is_managed_vmnet(vmnet) or vmnet == "vmnet0": + # vmnet already managed, try to allocate a new one + allocate_vmnet = True + else: + # otherwise allocate a new one + allocate_vmnet = True + + if allocate_vmnet: + try: + vmnet = self.manager.allocate_vmnet() + except: + # clear everything up in case of error (e.g. no enough vmnets) + self._vmnets.clear() + raise + + # mark the vmnet managed by us + if vmnet not in self._vmnets: + self._vmnets.append(vmnet) + self._vmx_pairs["ethernet{}.vnet".format(adapter_number)] = vmnet + else: + # not connected to anything... + vnet = "ethernet{}.vnet".format(adapter_number) + if vnet not in self._vmx_pairs: + self._vmx_pairs["ethernet{}.startconnected".format(adapter_number)] = "FALSE" + + # disable remaining network adapters + for adapter_number in range(self._adapters, self._maximum_adapters): + if self._get_vmx_setting("ethernet{}.present".format(adapter_number), "TRUE"): + log.debug("disabling remaining adapter {}".format(adapter_number)) + self._vmx_pairs["ethernet{}.startconnected".format(adapter_number)] = "FALSE" + + @asyncio.coroutine + def _add_ubridge_connection(self, nio, adapter_number): + """ + Creates a connection in uBridge. + + :param nio: NIO instance + :param adapter_number: adapter number + """ + + vnet = "ethernet{}.vnet".format(adapter_number) + if vnet not in self._vmx_pairs: + raise VMwareError("vnet {} not in VMX file".format(vnet)) + yield from self._ubridge_hypervisor.send("bridge create {name}".format(name=vnet)) + vmnet_interface = os.path.basename(self._vmx_pairs[vnet]) + if sys.platform.startswith("linux"): + yield from self._ubridge_hypervisor.send('bridge add_nio_linux_raw {name} "{interface}"'.format(name=vnet, + interface=vmnet_interface)) + elif sys.platform.startswith("win"): + windows_interfaces = get_windows_interfaces() + npf = None + for interface in windows_interfaces: + if "netcard" in interface and vmnet_interface in interface["netcard"]: + npf = interface["id"] + elif vmnet_interface in interface["name"]: + npf = interface["id"] + if npf: + yield from self._ubridge_hypervisor.send('bridge add_nio_ethernet {name} "{interface}"'.format(name=vnet, + interface=npf)) + else: + raise VMwareError("Could not find NPF id for VMnet interface {}".format(vmnet_interface)) + elif sys.platform.startswith("darwin"): + yield from self._ubridge_hypervisor.send('bridge add_nio_fusion_vmnet {name} "{interface}"'.format(name=vnet, + interface=vmnet_interface)) + else: + yield from self._ubridge_hypervisor.send('bridge add_nio_ethernet {name} "{interface}"'.format(name=vnet, + interface=vmnet_interface)) + + if isinstance(nio, NIOUDP): + yield from self._ubridge_hypervisor.send('bridge add_nio_udp {name} {lport} {rhost} {rport}'.format(name=vnet, + lport=nio.lport, + rhost=nio.rhost, + rport=nio.rport)) + + if nio.capturing: + yield from self._ubridge_hypervisor.send('bridge start_capture {name} "{pcap_file}"'.format(name=vnet, + pcap_file=nio.pcap_output_file)) + + yield from self._ubridge_hypervisor.send('bridge start {name}'.format(name=vnet)) + + @asyncio.coroutine + def _delete_ubridge_connection(self, adapter_number): + """ + Deletes a connection in uBridge. + + :param adapter_number: adapter number + """ + + vnet = "ethernet{}.vnet".format(adapter_number) + if vnet not in self._vmx_pairs: + raise VMwareError("vnet {} not in VMX file".format(vnet)) + yield from self._ubridge_hypervisor.send("bridge delete {name}".format(name=vnet)) + + @asyncio.coroutine + def _start_ubridge_capture(self, adapter_number, output_file): + """ + Start a packet capture in uBridge. + + :param adapter_number: adapter number + :param output_file: PCAP destination file for the capture + """ + + vnet = "ethernet{}.vnet".format(adapter_number) + if vnet not in self._vmx_pairs: + raise VMwareError("vnet {} not in VMX file".format(vnet)) + yield from self._ubridge_hypervisor.send('bridge start_capture {name} "{output_file}"'.format(name=vnet, + output_file=output_file)) + + @asyncio.coroutine + def _stop_ubridge_capture(self, adapter_number): + """ + Stop a packet capture in uBridge. + + :param adapter_number: adapter number + """ + + vnet = "ethernet{}.vnet".format(adapter_number) + if vnet not in self._vmx_pairs: + raise VMwareError("vnet {} not in VMX file".format(vnet)) + yield from self._ubridge_hypervisor.send("bridge stop_capture {name}".format(name=vnet)) + + def check_hw_virtualization(self): + """ + Returns either hardware virtualization is activated or not. + + :returns: boolean + """ + + self._read_vmx_file() + if self._get_vmx_setting("vhv.enable", "TRUE"): + return True + return False + + @asyncio.coroutine + def start(self): + """ + Starts this VMware VM. + """ + + if (yield from self.is_running()): + raise VMwareError("The VM is already running in VMware") + + ubridge_path = self.ubridge_path + if not ubridge_path or not os.path.isfile(ubridge_path): + raise VMwareError("ubridge is necessary to start a VMware VM") + + if self._use_ubridge: + yield from self._start_ubridge() + + self._read_vmx_file() + # check if there is enough RAM to run + if "memsize" in self._vmx_pairs: + self.check_available_ram(int(self._vmx_pairs["memsize"])) + self._set_network_options() + self._set_serial_console() + self._write_vmx_file() + + if self._headless: + yield from self._control_vm("start", "nogui") + else: + yield from self._control_vm("start") + + if self._use_ubridge and self._ubridge_hypervisor: + for adapter_number in range(0, self._adapters): + nio = self._ethernet_adapters[adapter_number].get_nio(0) + if nio: + yield from self._add_ubridge_connection(nio, adapter_number) + + if self._enable_remote_console and self._console is not None: + try: + if sys.platform.startswith("win"): + yield from wait_for_named_pipe_creation(self._get_pipe_name()) + else: + yield from wait_for_file_creation(self._get_pipe_name()) # wait for VMware to create the pipe file. + except asyncio.TimeoutError: + raise VMwareError('Pipe file "{}" for remote console has not been created by VMware'.format(self._get_pipe_name())) + self._start_remote_console() + + if self._get_vmx_setting("vhv.enable", "TRUE"): + self._hw_virtualization = True + + self._started = True + log.info("VMware VM '{name}' [{id}] started".format(name=self.name, id=self.id)) + + @asyncio.coroutine + def stop(self): + """ + Stops this VMware VM. + """ + + self._hw_virtualization = False + self._stop_remote_console() + if self._ubridge_hypervisor and self._ubridge_hypervisor.is_running(): + yield from self._ubridge_hypervisor.stop() + + try: + if self.acpi_shutdown: + # use ACPI to shutdown the VM + yield from self._control_vm("stop", "soft") + else: + yield from self._control_vm("stop") + finally: + self._started = False + + self._read_vmx_file() + if self._use_ubridge: + self._vmnets.clear() + # remove the adapters managed by GNS3 + for adapter_number in range(0, self._adapters): + if self._get_vmx_setting("ethernet{}.vnet".format(adapter_number)) or \ + self._get_vmx_setting("ethernet{}.connectiontype".format(adapter_number)) is None: + vnet = "ethernet{}.vnet".format(adapter_number) + if vnet in self._vmx_pairs: + vmnet = os.path.basename(self._vmx_pairs[vnet]) + if not self.manager.is_managed_vmnet(vmnet): + continue + log.debug("removing adapter {}".format(adapter_number)) + for key in list(self._vmx_pairs.keys()): + if key.startswith("ethernet{}.".format(adapter_number)): + del self._vmx_pairs[key] + + # re-enable any remaining network adapters + for adapter_number in range(self._adapters, self._maximum_adapters): + if self._get_vmx_setting("ethernet{}.present".format(adapter_number), "TRUE"): + log.debug("enabling remaining adapter {}".format(adapter_number)) + self._vmx_pairs["ethernet{}.startconnected".format(adapter_number)] = "TRUE" + self._write_vmx_file() + + log.info("VMware VM '{name}' [{id}] stopped".format(name=self.name, id=self.id)) + + @asyncio.coroutine + def suspend(self): + """ + Suspends this VMware VM. + """ + + if self.manager.host_type != "ws": + raise VMwareError("Pausing a VM is only supported by VMware Workstation") + yield from self._control_vm("pause") + log.info("VMware VM '{name}' [{id}] paused".format(name=self.name, id=self.id)) + + @asyncio.coroutine + def resume(self): + """ + Resumes this VMware VM. + """ + + if self.manager.host_type != "ws": + raise VMwareError("Unpausing a VM is only supported by VMware Workstation") + yield from self._control_vm("unpause") + log.info("VMware VM '{name}' [{id}] resumed".format(name=self.name, id=self.id)) + + @asyncio.coroutine + def reload(self): + """ + Reloads this VMware VM. + """ + + yield from self._control_vm("reset") + log.info("VMware VM '{name}' [{id}] reloaded".format(name=self.name, id=self.id)) + + @asyncio.coroutine + def close(self): + """ + Closes this VMware VM. + """ + + if self._closed: + # VM is already closed + return + + log.debug("VMware 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.values(): + 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 nio and isinstance(nio, NIOVMNET) and nio.vmnet in self._vmnets: + self._vmnets.remove(nio.vmnet) + try: + self.acpi_shutdown = False + yield from self.stop() + except VMwareError: + pass + + if self._linked_clone: + # clean the VMware inventory path from this linked clone + inventory_path = self.manager.get_vmware_inventory_path() + inventory_pairs = {} + if os.path.exists(inventory_path): + try: + inventory_pairs = self.manager.parse_vmware_file(inventory_path) + except OSError as e: + log.warning('Could not read VMware inventory file "{}": {}'.format(inventory_path, e)) + return + + vmlist_entry = None + for name, value in inventory_pairs.items(): + if value == self._vmx_path: + vmlist_entry = name.split(".", 1)[0] + break + + if vmlist_entry is not None: + for name in inventory_pairs.keys(): + if name.startswith(vmlist_entry): + del inventory_pairs[name] + + try: + self.manager.write_vmware_file(inventory_path, inventory_pairs) + except OSError as e: + raise VMwareError('Could not write VMware inventory file "{}": {}'.format(inventory_path, e)) + + log.info("VirtualBox VM '{name}' [{id}] closed".format(name=self.name, id=self.id)) + self._closed = True + + @property + def headless(self): + """ + Returns either the VM will start in headless mode + + :returns: boolean + """ + + return self._headless + + @headless.setter + def headless(self, headless): + """ + Sets either the VM will start in headless mode + + :param headless: boolean + """ + + if headless: + log.info("VMware VM '{name}' [{id}] has enabled the headless mode".format(name=self.name, id=self.id)) + else: + log.info("VMware VM '{name}' [{id}] has disabled the headless mode".format(name=self.name, id=self.id)) + self._headless = headless + + @property + def acpi_shutdown(self): + """ + Returns either the VM will use ACPI shutdown + + :returns: boolean + """ + + return self._acpi_shutdown + + @acpi_shutdown.setter + def acpi_shutdown(self, acpi_shutdown): + """ + Sets either the VM will use ACPI shutdown + + :param acpi_shutdown: boolean + """ + + if acpi_shutdown: + log.info("VMware VM '{name}' [{id}] has enabled the ACPI shutdown mode".format(name=self.name, id=self.id)) + else: + log.info("VMware VM '{name}' [{id}] has disabled the ACPI shutdown mode".format(name=self.name, id=self.id)) + self._acpi_shutdown = acpi_shutdown + + @property + def vmx_path(self): + """ + Returns the path to the vmx file. + + :returns: VMware vmx file + """ + + return self._vmx_path + + @vmx_path.setter + def vmx_path(self, vmx_path): + """ + Sets the path to the vmx file. + + :param vmx_path: VMware vmx file + """ + + log.info("VMware VM '{name}' [{id}] has set the vmx file path to '{vmx}'".format(name=self.name, id=self.id, vmx=vmx_path)) + self._vmx_path = vmx_path + + @property + def enable_remote_console(self): + """ + Returns either the remote console is enabled or not + + :returns: boolean + """ + + return self._enable_remote_console + + @enable_remote_console.setter + def enable_remote_console(self, enable_remote_console): + """ + Sets either the console is enabled or not + + :param enable_remote_console: boolean + """ + + if enable_remote_console: + log.info("VMware VM '{name}' [{id}] has enabled the console".format(name=self.name, id=self.id)) + if self._started: + self._start_remote_console() + else: + log.info("VMware 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 adapters(self): + """ + Returns the number of adapters configured for this VMware VM. + + :returns: number of adapters + """ + + return self._adapters + + @adapters.setter + def adapters(self, adapters): + """ + Sets the number of Ethernet adapters for this VMware VM instance. + + :param adapters: number of adapters + """ + + # VMware VMs are limited to 10 adapters + if adapters > 10: + raise VMwareError("Number of adapters above the maximum supported of 10") + + self._ethernet_adapters.clear() + for adapter_number in range(0, adapters): + self._ethernet_adapters[adapter_number] = EthernetAdapter() + + self._adapters = len(self._ethernet_adapters) + log.info("VMware VM '{name}' [{id}] has changed the number of Ethernet adapters to {adapters}".format(name=self.name, + id=self.id, + adapters=adapters)) + + @property + def adapter_type(self): + """ + Returns the adapter type for this VMware VM instance. + + :returns: adapter type (string) + """ + + return self._adapter_type + + @adapter_type.setter + def adapter_type(self, adapter_type): + """ + Sets the adapter type for this VMware VM instance. + + :param adapter_type: adapter type (string) + """ + + self._adapter_type = adapter_type + log.info("VMware VM '{name}' [{id}]: adapter type changed to {adapter_type}".format(name=self.name, + id=self.id, + adapter_type=adapter_type)) + + @property + def use_ubridge(self): + """ + Returns either GNS3 can use uBridge for network connections. + + :returns: boolean + """ + + return self._use_ubridge + + @use_ubridge.setter + def use_ubridge(self, use_ubridge): + """ + Allows GNS3 to use uBridge for network connections. + + :param use_ubridge: boolean + """ + + if use_ubridge: + log.info("VMware VM '{name}' [{id}] will use uBridge for network connections".format(name=self.name, id=self.id)) + else: + log.info("VMware VM '{name}' [{id}] will not use uBridge for network connections".format(name=self.name, id=self.id)) + self._use_ubridge = use_ubridge + + @property + def use_any_adapter(self): + """ + Returns either GNS3 can use any VMware adapter on this instance. + + :returns: boolean + """ + + return self._use_any_adapter + + @use_any_adapter.setter + def use_any_adapter(self, use_any_adapter): + """ + Allows GNS3 to use any VMware adapter on this instance. + + :param use_any_adapter: boolean + """ + + if use_any_adapter: + log.info("VMware VM '{name}' [{id}] is allowed to use any adapter".format(name=self.name, id=self.id)) + else: + log.info("VMware VM '{name}' [{id}] is not allowed to use any adapter".format(name=self.name, id=self.id)) + self._use_any_adapter = use_any_adapter + + @asyncio.coroutine + def adapter_add_nio_binding(self, adapter_number, nio): + """ + Adds an adapter NIO binding. + + :param adapter_number: adapter number + :param nio: NIO instance to add to the slot/port + """ + + try: + adapter = self._ethernet_adapters[adapter_number] + except IndexError: + raise VMwareError("Adapter {adapter_number} doesn't exist on VMware VM '{name}'".format(name=self.name, + adapter_number=adapter_number)) + + self._read_vmx_file() + if isinstance(nio, NIONAT): + if self._started: + raise VMwareError("Sorry, adding a link to NAT for a started VMware VM is not supported") + self._vmx_pairs["ethernet{}.connectiontype".format(adapter_number)] = "nat" + self._write_vmx_file() + else: + # check if trying to connect to a nat, bridged or host-only adapter + if not self._use_any_adapter and self._get_vmx_setting("ethernet{}.present".format(adapter_number), "TRUE"): + # check for the connection type + connection_type = "ethernet{}.connectiontype".format(adapter_number) + if connection_type in self._vmx_pairs and self._vmx_pairs[connection_type] in ("nat", "bridged", "hostonly"): + raise VMwareError("Attachment ({}) already configured on network adapter {}. " + "Please remove it or allow GNS3 to use any adapter.".format(self._vmx_pairs[connection_type], + adapter_number)) + + if isinstance(nio, NIOVMNET): + if self._started: + raise VMwareError("Sorry, adding a link to a started VMware VM is not supported without uBridge enabled") + self._vmx_pairs["ethernet{}.vnet".format(adapter_number)] = nio.vmnet + self._write_vmx_file() + if nio.vmnet not in self._vmnets: + self._vmnets.append(nio.vmnet) + adapter.add_nio(0, nio) + if self._started and self._use_ubridge and self._ubridge_hypervisor: + yield from self._add_ubridge_connection(nio, adapter_number) + + log.info("VMware VM '{name}' [{id}]: {nio} added to adapter {adapter_number}".format(name=self.name, + id=self.id, + nio=nio, + adapter_number=adapter_number)) + + @asyncio.coroutine + def adapter_remove_nio_binding(self, adapter_number): + """ + Removes an adapter NIO binding. + + :param adapter_number: adapter number + + :returns: NIO instance + """ + + try: + adapter = self._ethernet_adapters[adapter_number] + except IndexError: + raise VMwareError("Adapter {adapter_number} doesn't exist on VMware VM '{name}'".format(name=self.name, + adapter_number=adapter_number)) + + nio = adapter.get_nio(0) + if isinstance(nio, NIOUDP): + self.manager.port_manager.release_udp_port(nio.lport, self._project) + if isinstance(nio, NIOVMNET): + self._read_vmx_file() + vnet = "ethernet{}.vnet".format(adapter_number) + if vnet in self._vmx_pairs: + del self._vmx_pairs[vnet] + self._write_vmx_file() + if nio.vmnet in self._vmnets: + self._vmnets.remove(nio.vmnet) + adapter.remove_nio(0) + if self._started and self._use_ubridge and self._ubridge_hypervisor: + yield from self._delete_ubridge_connection(adapter_number) + + log.info("VMware 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 _get_pipe_name(self): + """ + Returns the pipe name to create a serial connection. + + :returns: pipe path (string) + """ + + if sys.platform.startswith("win"): + pipe_name = r"\\.\pipe\gns3_vmware\{}".format(self.id) + else: + pipe_name = os.path.join(tempfile.gettempdir(), "gns3_vmware", "{}".format(self.id)) + try: + os.makedirs(os.path.dirname(pipe_name), exist_ok=True) + except OSError as e: + raise VMwareError("Could not create the VMware pipe directory: {}".format(e)) + return pipe_name + + def _set_serial_console(self): + """ + Configures the first serial port to allow a serial console connection. + """ + + pipe_name = self._get_pipe_name() + serial_port = {"serial0.present": "TRUE", + "serial0.filetype": "pipe", + "serial0.filename": pipe_name, + "serial0.pipe.endpoint": "server"} + self._vmx_pairs.update(serial_port) + + def _start_remote_console(self): + """ + Starts remote console support for this VM. + """ + + # starts the Telnet to pipe thread + pipe_name = self._get_pipe_name() + if sys.platform.startswith("win"): + try: + self._serial_pipe = open(pipe_name, "a+b") + except OSError as e: + raise VMwareError("Could not open the pipe {}: {}".format(pipe_name, e)) + try: + self._telnet_server_thread = TelnetServer(self.name, msvcrt.get_osfhandle(self._serial_pipe.fileno()), self._manager.port_manager.console_host, self._console) + except OSError as e: + raise VMwareError("Unable to create Telnet server: {}".format(e)) + self._telnet_server_thread.start() + else: + try: + self._serial_pipe = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self._serial_pipe.connect(pipe_name) + except OSError as e: + raise VMwareError("Could not connect to the pipe {}: {}".format(pipe_name, e)) + try: + self._telnet_server_thread = TelnetServer(self.name, self._serial_pipe, self._manager.port_manager.console_host, self._console) + except OSError as e: + raise VMwareError("Unable to create Telnet server: {}".format(e)) + self._telnet_server_thread.start() + + def _stop_remote_console(self): + """ + Stops remote console support for this VM. + """ + + if self._telnet_server_thread: + 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"): + win32file.CloseHandle(msvcrt.get_osfhandle(self._serial_pipe.fileno())) + else: + self._serial_pipe.close() + self._serial_pipe = None + + @asyncio.coroutine + def start_capture(self, adapter_number, output_file): + """ + Starts a packet capture. + + :param adapter_number: adapter number + :param output_file: PCAP destination file for the capture + """ + + try: + adapter = self._ethernet_adapters[adapter_number] + except KeyError: + raise VMwareError("Adapter {adapter_number} doesn't exist on VMware VM '{name}'".format(name=self.name, + adapter_number=adapter_number)) + + nio = adapter.get_nio(0) + + if isinstance(nio, NIOVMNET): + raise VMwareError("Sorry, packet capture is not supported without uBridge enabled") + + if not nio: + raise VMwareError("Adapter {} is not connected".format(adapter_number)) + + if nio.capturing: + raise VMwareError("Packet capture is already activated on adapter {adapter_number}".format(adapter_number=adapter_number)) + + nio.startPacketCapture(output_file) + + if self._started: + yield from self._start_ubridge_capture(adapter_number, output_file) + + log.info("VMware VM '{name}' [{id}]: starting packet capture on adapter {adapter_number}".format(name=self.name, + id=self.id, + adapter_number=adapter_number)) + + def stop_capture(self, adapter_number): + """ + Stops a packet capture. + + :param adapter_number: adapter number + """ + + try: + adapter = self._ethernet_adapters[adapter_number] + except KeyError: + raise VMwareError("Adapter {adapter_number} doesn't exist on VirtualBox VM '{name}'".format(name=self.name, + adapter_number=adapter_number)) + + nio = adapter.get_nio(0) + + if not nio: + raise VMwareError("Adapter {} is not connected".format(adapter_number)) + + nio.stopPacketCapture() + + if self._started: + yield from self._stop_ubridge_capture(adapter_number) + + log.info("VMware 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/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index f9b42bf7..3e57659d 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -27,16 +27,16 @@ import signal import re import asyncio import shutil -import gns3server.utils.asyncio +from ...utils.asyncio import wait_for_process_termination +from ...utils.asyncio import monitor_process +from ...utils.asyncio import subprocess_check_output 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__) @@ -62,6 +62,7 @@ class VPCSVM(BaseVM): self._command = [] self._process = None self._vpcs_stdout_file = "" + self._vpcs_version = None self._started = False # VPCS settings @@ -109,6 +110,8 @@ class VPCSVM(BaseVM): return {"name": self.name, "vm_id": self.id, + "vm_directory": self.working_dir, + "status": self.status, "console": self._console, "project_id": self.project.id, "startup_script": self.startup_script, @@ -194,15 +197,16 @@ class VPCSVM(BaseVM): @asyncio.coroutine def _check_vpcs_version(self): """ - Checks if the VPCS executable version is >= 0.5b1. + Checks if the VPCS executable version is >= 0.8b or == 0.6.1. """ 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") + self._vpcs_version = parse_version(version) + if self._vpcs_version < parse_version("0.8b") and self._vpcs_version != parse_version("0.6.1"): + raise VPCSError("VPCS executable version must be >= 0.8b or 0.6.1") else: raise VPCSError("Could not determine the VPCS version for {}".format(self.vpcs_path)) except (OSError, subprocess.SubprocessError) as e: @@ -233,13 +237,29 @@ class VPCSVM(BaseVM): stderr=subprocess.STDOUT, cwd=self.working_dir, creationflags=flags) + monitor_process(self._process, self._termination_callback) log.info("VPCS instance {} started PID={}".format(self.name, self._process.pid)) self._started = True + self.status = "started" 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)) + def _termination_callback(self, returncode): + """ + Called when the process has stopped. + + :param returncode: Process returncode + """ + if self._started: + log.info("VPCS process has stopped, return code: %d", returncode) + self._started = False + self.status = "stopped" + self._process = None + if returncode != 0: + self.project.emit("log.error", {"message": "VPCS process has stopped, return code: {}\n{}".format(returncode, self.read_vpcs_stdout())}) + @asyncio.coroutine def stop(self): """ @@ -250,7 +270,7 @@ class VPCSVM(BaseVM): self._terminate_process() if self._process.returncode is None: try: - yield from gns3server.utils.asyncio.wait_for_process_termination(self._process, timeout=3) + yield from wait_for_process_termination(self._process, timeout=3) except asyncio.TimeoutError: if self._process.returncode is None: try: @@ -262,6 +282,7 @@ class VPCSVM(BaseVM): self._process = None self._started = False + self.status = "stopped" @asyncio.coroutine def reload(self): @@ -397,6 +418,8 @@ class VPCSVM(BaseVM): command.extend(["-m", str(self._manager.get_mac_id(self.id))]) # the unique ID is used to set the MAC address offset command.extend(["-i", "1"]) # option to start only one VPC instance command.extend(["-F"]) # option to avoid the daemonization of VPCS + if self._vpcs_version > parse_version("0.8"): + command.extend(["-R"]) # disable relay feature of VPCS (starting with VPCS 0.8) nio = self._ethernet_adapter.get_nio(0) if nio: diff --git a/gns3server/run.py b/gns3server/run.py new file mode 100644 index 00000000..9604d467 --- /dev/null +++ b/gns3server/run.py @@ -0,0 +1,247 @@ +#!/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 . + + +""" +Start the program. Use main.py to load it. +""" + +import os +import datetime +import sys +import locale +import argparse +import asyncio + +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__) + + +def locale_check(): + """ + Checks if this application runs with a correct locale (i.e. supports UTF-8 encoding) and attempt to fix + if this is not the case. + + This is to prevent UnicodeEncodeError with unicode paths when using standard library I/O operation + methods (e.g. os.stat() or os.path.*) which rely on the system or user locale. + + More information can be found there: http://seasonofcode.com/posts/unicode-i-o-and-locales-in-python.html + or there: http://robjwells.com/post/61198832297/get-your-us-ascii-out-of-my-face + """ + + # no need to check on Windows or when this application is frozen + if sys.platform.startswith("win") or hasattr(sys, "frozen"): + return + + language = encoding = None + try: + language, encoding = locale.getlocale() + except ValueError as 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...") + 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)) + raise SystemExit + elif encoding != "UTF-8": + 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)) + raise SystemExit + else: + log.info("Current locale is {}.{}".format(language, encoding)) + + +def parse_arguments(argv): + """ + Parse command line arguments and override local configuration + + :params args: Array of command line arguments + """ + + parser = argparse.ArgumentParser(description="GNS3 server version {}".format(__version__)) + 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("--config", help="Configuration file") + 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 (for developers)") + 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)") + parser.add_argument("--log", help="send output to logfile instead of console") + parser.add_argument("--daemon", action="store_true", help="start as a daemon") + parser.add_argument("--pid", help="store process pid") + + args = parser.parse_args(argv) + if args.config: + Config.instance(files=[args.config]) + + config = Config.instance().get_section_config("Server") + 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), + "logfile": config.getboolean("logfile", ""), + } + + parser.set_defaults(**defaults) + 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) + + +def pid_lock(path): + """ + Write the file in a file on the system. + Check if the process is not already running. + """ + + if os.path.exists(path): + pid = None + try: + with open(path) as f: + pid = int(f.read()) + try: + os.kill(pid, 0) # If the proces is not running kill return an error + except OSError: + pid = None + except OSError as e: + log.critical("Can't open pid file %s: %s", pid, str(e)) + sys.exit(1) + + if pid: + log.critical("GNS3 is already running pid: %d", pid) + sys.exit(1) + + try: + with open(path, 'w+') as f: + f.write(str(os.getpid())) + except OSError as e: + log.critical("Can't write pid file %s: %s", path, str(e)) + sys.exit(1) + + +def run(): + args = parse_arguments(sys.argv[1:]) + + if args.daemon and sys.platform.startswith("win"): + log.critical("Daemon is not supported on Windows") + sys.exit(1) + + if args.pid: + pid_lock(args.pid) + + level = logging.INFO + if args.debug: + level = logging.DEBUG + + user_log = init_logger(level, logfile=args.log, 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.4 + if sys.version_info < (3, 4): + raise SystemExit("Python 3.4 or higher is required") + + user_log.info("Running with Python {major}.{minor}.{micro} and has PID {pid}".format( + 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) + locale_check() + + try: + os.getcwd() + except FileNotFoundError: + log.critical("The current working directory doesn't exist") + return + + Project.clean_project_directory() + + CrashReport.instance() + host = server_config["host"] + port = int(server_config["port"]) + + server = Server.instance(host, port) + try: + server.run() + except OSError as e: + # This is to ignore OSError: [WinError 0] The operation completed successfully exception on Windows. + if not sys.platform.startswith("win") and not e.winerror == 0: + raise + except Exception as e: + log.critical("Critical error while running the server: {}".format(e), exc_info=1) + CrashReport.instance().capture_exception() + return + + if args.pid: + log.info("Remove PID file %s", args.pid) + try: + os.remove(args.pid) + except OSError as e: + log.critical("Can't remove pid file %s: %s", args.pid, str(e)) diff --git a/gns3server/schemas/docker.py b/gns3server/schemas/docker.py new file mode 100644 index 00000000..1fb13190 --- /dev/null +++ b/gns3server/schemas/docker.py @@ -0,0 +1,164 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2015 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +DOCKER_CREATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to create a new Docker container", + "type": "object", + "properties": { + "vm_id": { + "description": "Docker 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 + ] + }, + "name": { + "description": "Docker container name", + "type": "string", + "minLength": 1, + }, + "startcmd": { + "description": "Docker CMD entry", + "type": "string", + "minLength": 1, + }, + "imagename": { + "description": "Docker image name", + "type": "string", + "minLength": 1, + }, + "adapters": { + "description": "number of adapters", + "type": "integer", + "minimum": 0, + "maximum": 64, + }, + "adapter_type": { + "description": "Docker adapter type", + "type": "string", + "minLength": 1, + }, + "console": { + "description": "console name", + "type": "string", + "minLength": 1, + }, + }, + "additionalProperties": False, +} + +DOCKER_UPDATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to update a Docker container", + "type": "object", + "properties": { + "name": { + "description": "Docker container name", + "type": "string", + "minLength": 1, + }, + "image": { + "description": "Docker image name", + "type": "string", + "minLength": 1, + }, + "adapters": { + "description": "number of adapters", + "type": "integer", + "minimum": 0, + "maximum": 64, + }, + "adapter_type": { + "description": "Docker adapter type", + "type": "string", + "minLength": 1, + }, + }, + "additionalProperties": False, +} + +DOCKER_CAPTURE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to start a packet capture on a Docker container port", + "type": "object", + "properties": { + "capture_file_name": { + "description": "Capture file name", + "type": "string", + "minLength": 1, + }, + }, + "additionalProperties": False, + "required": ["capture_file_name"] +} + +DOCKER_OBJECT_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Docker instance", + "type": "object", + "properties": { + "name": { + "description": "Docker container name", + "type": "string", + "minLength": 1, + }, + "vm_id": { + "description": "Docker container 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}$" + }, + "cid": { + "description": "Docker container ID", + "type": "string", + "minLength": 64, + "maxLength": 64, + "pattern": "^[a-zA-Z0-9_.-]{64}$" + }, + "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}$" + }, + "image": { + "description": "Docker image name", + "type": "string", + "minLength": 1, + }, + "adapters": { + "description": "number of adapters", + "type": "integer", + "minimum": 0, + "maximum": 64, + }, + "adapter_type": { + "description": "Docker adapter type", + "type": "string", + "minLength": 1, + }, + }, + "additionalProperties": False, + "required": ["vm_id", "project_id"] +} diff --git a/gns3server/schemas/dynamips_device.py b/gns3server/schemas/dynamips_device.py index 201cd805..45d6da0c 100644 --- a/gns3server/schemas/dynamips_device.py +++ b/gns3server/schemas/dynamips_device.py @@ -63,10 +63,15 @@ DEVICE_UPDATE_SCHEMA = { "description": "Port type", "enum": ["access", "dot1q", "qinq"], }, + "vlan": {"description": "VLAN number", "type": "integer", "minimum": 1 }, + "ethertype": { + "description": "QinQ Ethertype", + "enum": ["", "0x8100", "0x88A8", "0x9100", "0x9200"], + }, }, "required": ["port", "type", "vlan"], "additionalProperties": False @@ -112,6 +117,10 @@ DEVICE_OBJECT_SCHEMA = { "type": "integer", "minimum": 1 }, + "ethertype": { + "description": "QinQ Ethertype", + "enum": ["", "0x8100", "0x88A8", "0x9100", "0x9200"], + }, }, "required": ["port", "type", "vlan"], "additionalProperties": False @@ -321,6 +330,10 @@ DEVICE_NIO_SCHEMA = { "type": "integer", "minimum": 1 }, + "ethertype": { + "description": "QinQ Ethertype", + "enum": ["", "0x8100", "0x88A8", "0x9100", "0x9200"], + }, }, "required": ["type", "vlan"], "additionalProperties": False diff --git a/gns3server/schemas/dynamips_vm.py b/gns3server/schemas/dynamips_vm.py index cbc4243d..942e9a06 100644 --- a/gns3server/schemas/dynamips_vm.py +++ b/gns3server/schemas/dynamips_vm.py @@ -57,6 +57,11 @@ VM_CREATE_SCHEMA = { "type": "string", "minLength": 1, }, + "image_md5sum": { + "description": "checksum of the IOS image", + "type": ["string", "null"], + "minLength": 1, + }, "startup_config": { "description": "path to the IOS startup configuration file", "type": "string", @@ -288,6 +293,11 @@ VM_UPDATE_SCHEMA = { "type": "string", "minLength": 1, }, + "image_md5sum": { + "description": "checksum of the IOS image", + "type": ["string", "null"], + "minLength": 1, + }, "startup_config_content": { "description": "Content of IOS startup configuration file", "type": "string", @@ -517,6 +527,10 @@ VM_OBJECT_SCHEMA = { "maxLength": 36, "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" }, + "vm_directory": { + "decription": "Path to the VM working directory", + "type": "string" + }, "project_id": { "description": "Project UUID", "type": "string", @@ -546,6 +560,11 @@ VM_OBJECT_SCHEMA = { "type": "string", "minLength": 1, }, + "image_md5sum": { + "description": "checksum of the IOS image", + "type": ["string", "null"], + "minLength": 1, + }, "startup_config": { "description": "path to the IOS startup configuration file", "type": "string", diff --git a/gns3server/schemas/file.py b/gns3server/schemas/file.py new file mode 100644 index 00000000..38ce7a10 --- /dev/null +++ b/gns3server/schemas/file.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 . + + +FILE_STREAM_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation retrieval of a file stream", + "type": "object", + "properties": { + "location": { + "description": "File path", + "type": ["string"], + "minLength": 1 + } + }, + "additionalProperties": False, + "required": ["location"] +} diff --git a/gns3server/schemas/iou.py b/gns3server/schemas/iou.py index 40420994..cd6a9f4a 100644 --- a/gns3server/schemas/iou.py +++ b/gns3server/schemas/iou.py @@ -46,6 +46,10 @@ IOU_CREATE_SCHEMA = { "description": "Path of iou binary", "type": "string" }, + "md5sum": { + "description": "Checksum of iou binary", + "type": ["string", "null"] + }, "serial_adapters": { "description": "How many serial adapters are connected to the IOU", "type": "integer" @@ -70,16 +74,24 @@ IOU_CREATE_SCHEMA = { "description": "Use default IOU values", "type": ["boolean", "null"] }, - "initial_config": { - "description": "Path to the initial configuration of IOU", + "startup_config": { + "description": "Path to the startup-config of IOU", + "type": ["string", "null"] + }, + "private_config": { + "description": "Path to the private-config of IOU", "type": ["string", "null"] }, - "initial_config_content": { - "description": "Initial configuration of IOU", + "startup_config_content": { + "description": "Startup-config of IOU", + "type": ["string", "null"] + }, + "private_config_content": { + "description": "Private-config of 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", + "description": "Content of the iourc file. Ignored if Null", "type": ["string", "null"] } }, @@ -107,6 +119,10 @@ IOU_UPDATE_SCHEMA = { "description": "Path of iou binary", "type": ["string", "null"] }, + "md5sum": { + "description": "Checksum of iou binary", + "type": ["string", "null"] + }, "serial_adapters": { "description": "How many serial adapters are connected to the IOU", "type": ["integer", "null"] @@ -127,8 +143,12 @@ IOU_UPDATE_SCHEMA = { "description": "Always up ethernet interface", "type": ["boolean", "null"] }, - "initial_config_content": { - "description": "Initial configuration of IOU", + "startup_config_content": { + "description": "Startup-config of IOU", + "type": ["string", "null"] + }, + "private_config_content": { + "description": "Private-config of IOU", "type": ["string", "null"] }, "use_default_iou_values": { @@ -136,13 +156,27 @@ IOU_UPDATE_SCHEMA = { "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", + "description": "Content of the iourc file. Ignored if Null", "type": ["string", "null"] } }, "additionalProperties": False, } + +IOU_START_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to start an IOU instance", + "type": "object", + "properties": { + "iourc_content": { + "description": "Content of the iourc file. Ignored if Null", + "type": ["string", "null"] + } + } +} + + IOU_OBJECT_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", "description": "IOU instance", @@ -160,6 +194,10 @@ IOU_OBJECT_SCHEMA = { "maxLength": 36, "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" }, + "vm_directory": { + "decription": "Path to the VM working directory", + "type": "string" + }, "console": { "description": "console TCP port", "minimum": 1, @@ -177,6 +215,10 @@ IOU_OBJECT_SCHEMA = { "description": "Path of iou binary", "type": "string" }, + "md5sum": { + "description": "Checksum of iou binary", + "type": ["string", "null"] + }, "serial_adapters": { "description": "How many serial adapters are connected to the IOU", "type": "integer" @@ -197,8 +239,12 @@ IOU_OBJECT_SCHEMA = { "description": "Always up ethernet interface", "type": "boolean" }, - "initial_config": { - "description": "Path of the initial config content relative to project directory", + "startup_config": { + "description": "Path of the startup-config content relative to project directory", + "type": ["string", "null"] + }, + "private_config": { + "description": "Path of the private-config content relative to project directory", "type": ["string", "null"] }, "use_default_iou_values": { @@ -211,7 +257,8 @@ IOU_OBJECT_SCHEMA = { } }, "additionalProperties": False, - "required": ["name", "vm_id", "console", "project_id", "path", "serial_adapters", "ethernet_adapters", "ram", "nvram", "l1_keepalives", "initial_config", "use_default_iou_values"] + "required": ["name", "vm_id", "console", "project_id", "path", "md5sum", "serial_adapters", "ethernet_adapters", + "ram", "nvram", "l1_keepalives", "startup_config", "private_config", "use_default_iou_values"] } IOU_CAPTURE_SCHEMA = { @@ -234,16 +281,21 @@ IOU_CAPTURE_SCHEMA = { "required": ["capture_file_name", "data_link_type"] } -IOU_INITIAL_CONFIG_SCHEMA = { +IOU_CONFIGS_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to get the initial configuration file", + "description": "Request validation to get the startup and private configuration file", "type": "object", "properties": { - "content": { - "description": "Content of the initial configuration file", - "type": ["string", "null"] + "startup_config_content": { + "description": "Content of the startup configuration file", + "type": ["string", "null"], + "minLength": 1, + }, + "private_config_content": { + "description": "Content of the private configuration file", + "type": ["string", "null"], + "minLength": 1, }, }, "additionalProperties": False, - "required": ["content"] } diff --git a/gns3server/schemas/nio.py b/gns3server/schemas/nio.py index ca701314..ead1287f 100644 --- a/gns3server/schemas/nio.py +++ b/gns3server/schemas/nio.py @@ -143,6 +143,21 @@ NIO_SCHEMA = { "required": ["type", "control_file", "local_file"], "additionalProperties": False }, + "VMNET": { + "description": "VMNET Network Input/Output", + "properties": { + "type": { + "enum": ["nio_vmnet"] + }, + "vmnet": { + "description": "VMnet interface name e.g. vmnet12", + "type": "string", + "minLength": 1 + }, + }, + "required": ["type", "vmnet"], + "additionalProperties": False + }, "NULL": { "description": "NULL Network Input/Output", "properties": { @@ -162,6 +177,7 @@ NIO_SCHEMA = { {"$ref": "#/definitions/TAP"}, {"$ref": "#/definitions/UNIX"}, {"$ref": "#/definitions/VDE"}, + {"$ref": "#/definitions/VMNET"}, {"$ref": "#/definitions/NULL"}, ], "additionalProperties": True, diff --git a/gns3server/schemas/project.py b/gns3server/schemas/project.py index 3e9dfa6d..ec2b9636 100644 --- a/gns3server/schemas/project.py +++ b/gns3server/schemas/project.py @@ -75,7 +75,7 @@ PROJECT_OBJECT_SCHEMA = { "properties": { "name": { "description": "Project name", - "type": "string", + "type": ["string", "null"], "minLength": 1 }, "location": { @@ -103,3 +103,33 @@ PROJECT_OBJECT_SCHEMA = { "additionalProperties": False, "required": ["location", "project_id", "temporary"] } + +PROJECT_LIST_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "List of projects", + "type": "array", + "items": PROJECT_OBJECT_SCHEMA +} + +PROJECT_FILE_LIST_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "List files in the project", + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "path": { + "description": "File path", + "type": ["string"] + }, + "md5sum": { + "description": "MD5 hash of the file", + "type": ["string"] + }, + + }, + } + ], + "additionalProperties": False, +} diff --git a/gns3server/schemas/qemu.py b/gns3server/schemas/qemu.py index d8a5005b..89362fbe 100644 --- a/gns3server/schemas/qemu.py +++ b/gns3server/schemas/qemu.py @@ -15,6 +15,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +QEMU_PLATFORMS = ["aarch64", "alpha", "arm", "cris", "i386", "lm32", "m68k", "microblaze", "microblazeel", "mips", "mips64", "mips64el", "mipsel", "moxie", "or32", "ppc", "ppc64", "ppcemb", "s390x", "sh4", "sh4eb", "sparc", "sparc64", "tricore", "unicore32", "x86_64", "xtensa", "xtensaeb"] + QEMU_CREATE_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", @@ -36,37 +38,103 @@ QEMU_CREATE_SCHEMA = { "type": "string", "minLength": 1, }, + "usage": { + "description": "How to use the qemu VM", + "type": "string", + }, + "linked_clone": { + "description": "either the VM is a linked clone or not", + "type": "boolean" + }, "qemu_path": { "description": "Path to QEMU", - "type": "string", + "type": ["string", "null"], "minLength": 1, }, + "platform": { + "description": "Platform to emulate", + "enum": QEMU_PLATFORMS + ["null"] + }, "console": { "description": "console TCP port", "minimum": 1, "maximum": 65535, "type": ["integer", "null"] }, + "console_type": { + "description": "console type", + "enum": ["telnet", "vnc"] + }, "hda_disk_image": { "description": "QEMU hda disk image path", - "type": ["string", "null"], + "type": "string", + }, + "hda_disk_interface": { + "description": "QEMU hda interface", + "type": "string", + }, + "hda_disk_image_md5sum": { + "description": "QEMU hda disk image checksum", + "type": ["string", "null"] }, "hdb_disk_image": { "description": "QEMU hdb disk image path", + "type": "string", + }, + "hdb_disk_interface": { + "description": "QEMU hdb interface", + "type": "string", + }, + "hdb_disk_image_md5sum": { + "description": "QEMU hdb disk image checksum", "type": ["string", "null"], }, "hdc_disk_image": { "description": "QEMU hdc disk image path", + "type": "string", + }, + "hdc_disk_interface": { + "description": "QEMU hdc interface", + "type": "string", + }, + "hdc_disk_image_md5sum": { + "description": "QEMU hdc disk image checksum", "type": ["string", "null"], }, "hdd_disk_image": { "description": "QEMU hdd disk image path", + "type": "string", + }, + "hdd_disk_interface": { + "description": "QEMU hdd interface", + "type": "string", + }, + "hdd_disk_image_md5sum": { + "description": "QEMU hdd disk image checksum", "type": ["string", "null"], }, + "cdrom_image": { + "description": "QEMU cdrom image path", + "type": "string", + }, + "cdrom_image_md5sum": { + "description": "QEMU cdrom image checksum", + "type": ["string", "null"], + }, + "boot_priority": { + "description": "QEMU boot priority", + "enum": ["c", "d", "n", "cn", "cd"] + }, "ram": { "description": "amount of RAM in MB", "type": ["integer", "null"] }, + "cpus": { + "description": "number of vCPUs", + "type": ["integer", "null"], + "minimum": 1, + "maximum": 255, + }, "adapters": { "description": "number of adapters", "type": ["integer", "null"], @@ -78,12 +146,26 @@ QEMU_CREATE_SCHEMA = { "type": ["string", "null"], "minLength": 1, }, + "mac_address": { + "description": "QEMU MAC address", + "type": ["string", "null"], + "minLength": 1, + "pattern": "^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$" + }, "initrd": { + "description": "QEMU initrd path", + "type": "string", + }, + "initrd_md5sum": { "description": "QEMU initrd path", "type": ["string", "null"], }, "kernel_image": { "description": "QEMU kernel image path", + "type": "string", + }, + "kernel_image_md5sum": { + "description": "QEMU kernel image checksum", "type": ["string", "null"], }, "kernel_command_line": { @@ -94,6 +176,10 @@ QEMU_CREATE_SCHEMA = { "description": "Use QEMU legagy networking commands (-net syntax)", "type": ["boolean", "null"], }, + "acpi_shutdown": { + "description": "ACPI shutdown support", + "type": ["boolean", "null"], + }, "cpu_throttling": { "description": "Percentage of CPU allowed for QEMU", "minimum": 0, @@ -116,7 +202,7 @@ QEMU_CREATE_SCHEMA = { }, }, "additionalProperties": False, - "required": ["name", "qemu_path"], + "required": ["name"], } QEMU_UPDATE_SCHEMA = { @@ -129,37 +215,99 @@ QEMU_UPDATE_SCHEMA = { "type": ["string", "null"], "minLength": 1, }, + "usage": { + "description": "How to use the qemu VM", + "type": "string", + }, "qemu_path": { "description": "Path to QEMU", "type": ["string", "null"], "minLength": 1, }, + "platform": { + "description": "Platform to emulate", + "enum": QEMU_PLATFORMS + ["null"] + }, "console": { "description": "console TCP port", "minimum": 1, "maximum": 65535, "type": ["integer", "null"] }, + "console_type": { + "description": "console type", + "enum": ["telnet", "vnc"] + }, "hda_disk_image": { "description": "QEMU hda disk image path", - "type": ["string", "null"], + "type": "string", + }, + "hda_disk_interface": { + "description": "QEMU hda interface", + "type": "string", + }, + "hda_disk_image_md5sum": { + "description": "QEMU hda disk image checksum", + "type": ["string", "null"] }, "hdb_disk_image": { "description": "QEMU hdb disk image path", + "type": "string", + }, + "hdb_disk_interface": { + "description": "QEMU hdb interface", + "type": "string", + }, + "hdb_disk_image_md5sum": { + "description": "QEMU hdb disk image checksum", "type": ["string", "null"], }, "hdc_disk_image": { "description": "QEMU hdc disk image path", + "type": "string", + }, + "hdc_disk_interface": { + "description": "QEMU hdc interface", + "type": "string", + }, + "hdc_disk_image_md5sum": { + "description": "QEMU hdc disk image checksum", "type": ["string", "null"], }, "hdd_disk_image": { "description": "QEMU hdd disk image path", + "type": "string", + }, + "hdd_disk_interface": { + "description": "QEMU hdd interface", + "type": "string", + }, + "hdd_disk_image_md5sum": { + "description": "QEMU hdd disk image checksum", + "type": ["string", "null"], + }, + "cdrom_image": { + "description": "QEMU cdrom image path", + "type": "string", + }, + "cdrom_image_md5sum": { + "description": "QEMU cdrom image checksum", "type": ["string", "null"], }, + "boot_priority": { + "description": "QEMU boot priority", + "enum": ["c", "d"] + }, "ram": { "description": "amount of RAM in MB", "type": ["integer", "null"] }, + "cpus": { + "description": "number of vCPUs", + "type": ["integer", "null"], + "minimum": 1, + "maximum": 255, + }, "adapters": { "description": "number of adapters", "type": ["integer", "null"], @@ -171,12 +319,26 @@ QEMU_UPDATE_SCHEMA = { "type": ["string", "null"], "minLength": 1, }, + "mac_address": { + "description": "QEMU MAC address", + "type": ["string", "null"], + "minLength": 1, + "pattern": "^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$" + }, "initrd": { + "description": "QEMU initrd path", + "type": "string", + }, + "initrd_md5sum": { "description": "QEMU initrd path", "type": ["string", "null"], }, "kernel_image": { "description": "QEMU kernel image path", + "type": "string", + }, + "kernel_image_md5sum": { + "description": "QEMU kernel image checksum", "type": ["string", "null"], }, "kernel_command_line": { @@ -187,6 +349,10 @@ QEMU_UPDATE_SCHEMA = { "description": "Use QEMU legagy networking commands (-net syntax)", "type": ["boolean", "null"], }, + "acpi_shutdown": { + "description": "ACPI shutdown support", + "type": ["boolean", "null"], + }, "cpu_throttling": { "description": "Percentage of CPU allowed for QEMU", "minimum": 0, @@ -231,31 +397,93 @@ QEMU_OBJECT_SCHEMA = { "type": "string", "minLength": 1, }, + "usage": { + "description": "How to use the qemu VM", + "type": "string", + }, "qemu_path": { "description": "path to QEMU", "type": "string", "minLength": 1, }, + "platform": { + "description": "Platform to emulate", + "enum": QEMU_PLATFORMS + }, "hda_disk_image": { "description": "QEMU hda disk image path", "type": "string", }, + "hda_disk_interface": { + "description": "QEMU hda interface", + "type": "string", + }, + "hda_disk_image_md5sum": { + "description": "QEMU hda disk image checksum", + "type": ["string", "null"] + }, "hdb_disk_image": { "description": "QEMU hdb disk image path", "type": "string", }, + "hdb_disk_interface": { + "description": "QEMU hdb interface", + "type": "string", + }, + "hdb_disk_image_md5sum": { + "description": "QEMU hdb disk image checksum", + "type": ["string", "null"], + }, "hdc_disk_image": { "description": "QEMU hdc disk image path", "type": "string", }, + "hdc_disk_interface": { + "description": "QEMU hdc interface", + "type": "string", + }, + "hdc_disk_image_md5sum": { + "description": "QEMU hdc disk image checksum", + "type": ["string", "null"], + }, "hdd_disk_image": { "description": "QEMU hdd disk image path", "type": "string", }, + "hdd_disk_interface": { + "description": "QEMU hdd interface", + "type": "string", + }, + "hdd_disk_image_md5sum": { + "description": "QEMU hdd disk image checksum", + "type": ["string", "null"], + }, + "cdrom_image": { + "description": "QEMU cdrom image path", + "type": "string", + }, + "cdrom_image_md5sum": { + "description": "QEMU cdrom image checksum", + "type": ["string", "null"], + }, + "boot_priority": { + "description": "QEMU boot priority", + "enum": ["c", "d"] + }, + "vm_directory": { + "decription": "Path to the VM working directory", + "type": "string" + }, "ram": { "description": "amount of RAM in MB", "type": "integer" }, + "cpus": { + "description": "number of vCPUs", + "type": ["integer", "null"], + "minimum": 1, + "maximum": 255, + }, "adapters": { "description": "number of adapters", "type": "integer", @@ -267,20 +495,38 @@ QEMU_OBJECT_SCHEMA = { "type": "string", "minLength": 1, }, + "mac_address": { + "description": "QEMU MAC address", + "type": "string", + "minLength": 1, + "pattern": "^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$" + }, "console": { "description": "console TCP port", "minimum": 1, "maximum": 65535, "type": "integer" }, + "console_type": { + "description": "console type", + "enum": ["telnet", "vnc"] + }, "initrd": { "description": "QEMU initrd path", "type": "string", }, + "initrd_md5sum": { + "description": "QEMU initrd path", + "type": ["string", "null"], + }, "kernel_image": { "description": "QEMU kernel image path", "type": "string", }, + "kernel_image_md5sum": { + "description": "QEMU kernel image checksum", + "type": ["string", "null"], + }, "kernel_command_line": { "description": "QEMU kernel command line", "type": "string", @@ -289,6 +535,10 @@ QEMU_OBJECT_SCHEMA = { "description": "Use QEMU legagy networking commands (-net syntax)", "type": "boolean", }, + "acpi_shutdown": { + "description": "ACPI shutdown support", + "type": "boolean", + }, "cpu_throttling": { "description": "Percentage of CPU allowed for QEMU", "minimum": 0, @@ -310,11 +560,60 @@ QEMU_OBJECT_SCHEMA = { }, }, "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", - "initrd", "kernel_image", "kernel_command_line", - "legacy_networking", "cpu_throttling", "process_priority", "options" - ] + "required": ["vm_id", + "project_id", + "name", + "usage", + "qemu_path", + "platform", + "console_type", + "hda_disk_image", + "hdb_disk_image", + "hdc_disk_image", + "hdd_disk_image", + "hda_disk_image_md5sum", + "hdb_disk_image_md5sum", + "hdc_disk_image_md5sum", + "hdd_disk_image_md5sum", + "hda_disk_interface", + "hdb_disk_interface", + "hdc_disk_interface", + "hdd_disk_interface", + "cdrom_image", + "cdrom_image_md5sum", + "boot_priority", + "ram", + "cpus", + "adapters", + "adapter_type", + "mac_address", + "console", + "initrd", + "kernel_image", + "initrd_md5sum", + "kernel_image_md5sum", + "kernel_command_line", + "legacy_networking", + "acpi_shutdown", + "cpu_throttling", + "process_priority", + "options", + "vm_directory"] +} + +QEMU_BINARY_FILTER_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation for a list of qemu capabilities", + "properties": { + "archs": { + "description": "Architectures to filter binaries by", + "type": "array", + "items": { + "enum": QEMU_PLATFORMS + } + } + }, + "additionalProperties": False, } QEMU_BINARY_LIST_SCHEMA = { @@ -341,3 +640,81 @@ QEMU_BINARY_LIST_SCHEMA = { }, "additionalProperties": False, } + +QEMU_CAPABILITY_LIST_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation for a list of qemu capabilities", + "properties": { + "kvm": { + "description": "Architectures that KVM is enabled for", + "type": "array", + "items": { + "enum": QEMU_PLATFORMS + } + } + }, + "additionalProperties": False, +} + +QEMU_IMAGE_CREATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Create a new qemu image. Options can be specific to a format. Read qemu-img manual for more information", + "type": "object", + "properties": { + "qemu_img": { + "description": "Path to the qemu-img binary", + "type": "string" + }, + "path": { + "description": "Absolute or relative path of the image", + "type": "string" + }, + "format": { + "description": "Image format type", + "enum": ["qcow2", "qcow", "vpc", "vdi", "vmdk", "raw"] + }, + "size": { + "description": "Image size in M", + "type": "integer" + }, + "preallocation": { + "enum": ["off", "metadata", "falloc", "full"] + }, + "cluster_size": { + "type": "integer" + }, + "refcount_bits": { + "type": "integer" + }, + "lazy_refcounts": { + "enum": ["on", "off"] + }, + "subformat": { + "enum": [ + "dynamic", + "fixed", + "streamOptimized", + "twoGbMaxExtentSparse", + "twoGbMaxExtentFlat", + "monolithicSparse", + "monolithicFlat", + ] + }, + "static": { + "enum": ["on", "off"] + }, + "zeroed_grain": { + "enum": ["on", "off"] + }, + "adapter_type": { + "enum": [ + "ide", + "lsilogic", + "buslogic", + "legacyESX" + ] + } + }, + "required": ["qemu_img", "path", "format", "size"], + "additionalProperties": False +} diff --git a/gns3server/schemas/virtualbox.py b/gns3server/schemas/virtualbox.py index 8e57c35c..762acce4 100644 --- a/gns3server/schemas/virtualbox.py +++ b/gns3server/schemas/virtualbox.py @@ -80,6 +80,10 @@ VBOX_CREATE_SCHEMA = { "description": "headless mode", "type": "boolean" }, + "acpi_shutdown": { + "description": "ACPI shutdown", + "type": "boolean" + }, }, "additionalProperties": False, "required": ["name", "vmname", "linked_clone"], @@ -135,6 +139,10 @@ VBOX_UPDATE_SCHEMA = { "description": "headless mode", "type": "boolean" }, + "acpi_shutdown": { + "description": "ACPI shutdown", + "type": "boolean" + }, }, "additionalProperties": False, } @@ -183,6 +191,10 @@ VBOX_OBJECT_SCHEMA = { "type": "string", "minLength": 1, }, + "vm_directory": { + "decription": "Path to the VM working directory", + "type": ["string", "null"] + }, "enable_remote_console": { "description": "enable the remote console", "type": "boolean" @@ -191,6 +203,10 @@ VBOX_OBJECT_SCHEMA = { "description": "headless mode", "type": "boolean" }, + "acpi_shutdown": { + "description": "ACPI shutdown", + "type": "boolean" + }, "adapters": { "description": "number of adapters", "type": "integer", @@ -220,5 +236,5 @@ VBOX_OBJECT_SCHEMA = { }, }, "additionalProperties": False, - "required": ["name", "vm_id", "project_id"] + "required": ["name", "vm_id", "project_id", "vm_directory"] } diff --git a/gns3server/schemas/vm.py b/gns3server/schemas/vm.py new file mode 100644 index 00000000..239f16b8 --- /dev/null +++ b/gns3server/schemas/vm.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +# +# 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 . + + +VM_LIST_IMAGES_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "List of disk images", + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "filename": { + "description": "Image filename", + "type": "string", + "minLength": 1 + }, + "path": { + "description": "Image path", + "type": "string", + "minLength": 1 + } + }, + "required": ["filename", "path"], + "additionalProperties": False + } + ], + "additionalProperties": False, +} diff --git a/gns3server/schemas/vmware.py b/gns3server/schemas/vmware.py new file mode 100644 index 00000000..24f6321c --- /dev/null +++ b/gns3server/schemas/vmware.py @@ -0,0 +1,231 @@ +# -*- 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 . + + +VMWARE_CREATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to create a new VMware VM instance", + "type": "object", + "properties": { + "vm_id": { + "description": "VMware VM instance identifier", + "type": "string", + "minLength": 36, + "maxLength": 36, + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + }, + "linked_clone": { + "description": "either the VM is a linked clone or not", + "type": "boolean" + }, + "name": { + "description": "VMware VM instance name", + "type": "string", + "minLength": 1, + }, + "vmx_path": { + "description": "path to the vmx file", + "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" + }, + "acpi_shutdown": { + "description": "ACPI shutdown", + "type": "boolean" + }, + "adapters": { + "description": "number of adapters", + "type": "integer", + "minimum": 0, + "maximum": 10, # maximum adapters support by VMware VMs + }, + "adapter_type": { + "description": "VMware adapter type", + "type": "string", + "minLength": 1, + }, + "use_ubridge": { + "description": "use uBridge for network connections", + "type": "boolean", + }, + "use_any_adapter": { + "description": "allow GNS3 to use any VMware adapter", + "type": "boolean", + }, + }, + "additionalProperties": False, + "required": ["name", "vmx_path", "linked_clone"], +} + +VMWARE_UPDATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to update a VMware VM instance", + "type": "object", + "properties": { + "name": { + "description": "VMware VM instance name", + "type": "string", + "minLength": 1, + }, + "vmx_path": { + "description": "path to the vmx file", + "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" + }, + "acpi_shutdown": { + "description": "ACPI shutdown", + "type": "boolean" + }, + "adapters": { + "description": "number of adapters", + "type": "integer", + "minimum": 0, + "maximum": 10, # maximum adapters support by VMware VMs + }, + "adapter_type": { + "description": "VMware adapter type", + "type": "string", + "minLength": 1, + }, + "use_ubridge": { + "description": "use uBridge for network connections", + "type": "boolean", + }, + "use_any_adapter": { + "description": "allow GNS3 to use any VMware adapter", + "type": "boolean", + }, + }, + "additionalProperties": False, +} + +VMWARE_CAPTURE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to start a packet capture on a VMware VM instance port", + "type": "object", + "properties": { + "capture_file_name": { + "description": "Capture file name", + "type": "string", + "minLength": 1, + }, + }, + "additionalProperties": False, + "required": ["capture_file_name"] +} + +VMWARE_OBJECT_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "VMware VM instance", + "type": "object", + "properties": { + "name": { + "description": "VMware VM instance name", + "type": "string", + "minLength": 1, + }, + "vm_id": { + "description": "VMware 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}$" + }, + "vm_directory": { + "decription": "Path to the VM working directory", + "type": ["string", "null"] + }, + "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}$" + }, + "vmx_path": { + "description": "path to the vmx file", + "type": "string", + "minLength": 1, + }, + "enable_remote_console": { + "description": "enable the remote console", + "type": "boolean" + }, + "headless": { + "description": "headless mode", + "type": "boolean" + }, + "acpi_shutdown": { + "description": "ACPI shutdown", + "type": "boolean" + }, + "adapters": { + "description": "number of adapters", + "type": "integer", + "minimum": 0, + "maximum": 10, # maximum adapters support by VMware VMs + }, + "adapter_type": { + "description": "VMware adapter type", + "type": "string", + "minLength": 1, + }, + "use_ubridge": { + "description": "use uBridge for network connections", + "type": "boolean", + }, + "use_any_adapter": { + "description": "allow GNS3 to use any VMware adapter", + "type": "boolean", + }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["name", "vm_id", "project_id"] +} diff --git a/gns3server/schemas/vpcs.py b/gns3server/schemas/vpcs.py index 05d60d98..e53dc079 100644 --- a/gns3server/schemas/vpcs.py +++ b/gns3server/schemas/vpcs.py @@ -92,6 +92,14 @@ VPCS_OBJECT_SCHEMA = { "maxLength": 36, "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" }, + "vm_directory": { + "decription": "Path to the VM working directory", + "type": "string" + }, + "status": { + "description": "VM status", + "enum": ["started", "stopped"] + }, "console": { "description": "console TCP port", "minimum": 1, @@ -115,5 +123,5 @@ VPCS_OBJECT_SCHEMA = { }, }, "additionalProperties": False, - "required": ["name", "vm_id", "console", "project_id", "startup_script_path"] + "required": ["name", "vm_id", "status", "console", "project_id", "startup_script_path"] } diff --git a/gns3server/server.py b/gns3server/server.py index d8fad252..ba6d7650 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -99,6 +99,9 @@ class Server: if self._port_manager.udp_ports: log.warning("UDP ports are still used {}".format(self._port_manager.udp_ports)) + for task in asyncio.Task.all_tasks(): + task.cancel() + self._loop.stop() def _signal_handling(self): @@ -163,6 +166,7 @@ class Server: except ssl.SSLError as e: log.critical("SSL error: {}".format(e)) raise SystemExit + log.info("SSL is enabled") return ssl_context @asyncio.coroutine @@ -219,6 +223,9 @@ class Server: ssl_context = self._create_ssl_context(server_config) self._loop = asyncio.get_event_loop() + # Asyncio will raise error if coroutine is not called + self._loop.set_debug(True) + app = aiohttp.web.Application() for method, route, handler in Route.get_routes(): log.debug("Adding route: {} {}".format(method, route)) @@ -233,7 +240,6 @@ class Server: server = self._run_application(self._handler, ssl_context) self._loop.run_until_complete(server) self._signal_handling() - self._exit_handling() if server_config.getboolean("live"): diff --git a/gns3server/templates/index.html b/gns3server/templates/index.html index 2808ae87..3203897c 100644 --- a/gns3server/templates/index.html +++ b/gns3server/templates/index.html @@ -1,7 +1,7 @@ {% extends "layout.html" %} {% block body %}

- Welcome to GNS 3. + Welcome to GNS3.

  • Website
  • diff --git a/gns3server/templates/layout.html b/gns3server/templates/layout.html index cc451233..54caef09 100644 --- a/gns3server/templates/layout.html +++ b/gns3server/templates/layout.html @@ -1,6 +1,9 @@ + GNS3 Server @@ -12,6 +15,8 @@ Backup images | Backup projects + | + Status {% block body %}{% endblock %} diff --git a/gns3server/templates/status.html b/gns3server/templates/status.html new file mode 100644 index 00000000..bed8c797 --- /dev/null +++ b/gns3server/templates/status.html @@ -0,0 +1,41 @@ +{% extends "layout.html" %} +{% block body %} +

    + Server status +

    +The purpose of this page is to help for GNS3 debug. + +

    Opened projects

    + + + + + + +{% for project in project_manager.projects %} + + + + + + +{% endfor %} +
    NameID + VMsClients connected
    {{project.name}}{{project.id}}{{project.vms|length}}{{project.listeners|length}}
    + + +

    Ports reserved by GNS3

    +

    TCP

    +
      +{% for port in port_manager.tcp_ports %} +
    • {{port}}
    • +{% endfor %} +
    + +

    UDP

    +
      +{% for port in port_manager.udp_ports %} +
    • {{port}}
    • +{% endfor %} +
    +{% endblock %} diff --git a/gns3server/templates/upload.html b/gns3server/templates/upload.html index 91db249e..ae9fc033 100644 --- a/gns3server/templates/upload.html +++ b/gns3server/templates/upload.html @@ -1,8 +1,25 @@ {% extends "layout.html" %} +{% block script %} +function onSubmit() { + if (document.getElementById("uploadInput").files == undefined) { + //OLD browser + return true; + } + + max_size = 200; + var file = document.getElementById("uploadInput").files[0]; + var size = Math.round(file.size / 1000000); + if (size > max_size) { + alert("The file is too big (" + size + " MB). The max upload size is " + max_size + " MB. Please Upload your file with the GNS3 GUI"); + return false; + } + return true; +} +{% endblock %} {% block body %}

    Select & Upload an image for GNS3

    -
    - File path:
    + + File path:
    File type: