Compare commits

...
139 Commits
Author SHA1 Message Date
Jeremy bdb23d6d3b 1.3.8 2015-07-27 12:10:01 -06:00
Jeremy 5986849b67 Catch ProcessLookupError when updating iouyap config. Fixes #255. 2015-07-27 11:40:01 -06:00
Jeremy 76fe0cb316 Fixes IOS adapters and WICS cannot be removed. Fixes #282. 2015-07-26 15:51:55 -06:00
Jeremy 4aeb451ffd Makes sure the loop is running when closing the app. 2015-07-26 15:28:14 -06:00
Jeremy 7b001e4d36 Catch GeneratorExit exception. Fixes #231. 2015-07-25 16:46:23 -06:00
Jeremy b1b602db3b Merge remote-tracking branch 'origin/master' 2015-07-25 16:39:45 -06:00
Jeremy cf5af8a6f7 Fixes missing chipset info for VirtualBox VM (maybe some older VirtualBox version don't have it). Fixes #254. 2015-07-25 16:39:38 -06:00
Julien Duponchelle 4814bc6c65 Fix IOURC upload 2015-07-23 11:36:11 +02:00
Jeremy 2655d426c2 Backport: option to drop nvram & disk files for IOS routers in order to save disk space. 2015-07-22 16:02:08 -06:00
Jeremy f1bbacecd6 Backport: Remove timeout to wait for connections to finish. 2015-07-21 18:00:31 -06:00
Jeremy a8142a04f8 Merge remote-tracking branch 'origin/master' 2015-07-21 16:02:52 -06:00
Jeremy 19425b1e9c Backport: Fixes RuntimeError: Event loop is closed. 2015-07-21 16:02:44 -06:00
Jeremy Grossmann 1b066bef92 Merge pull request #272 from GNS3/backup_upload_images_projects
Backup upload images projects
2015-07-21 11:21:12 -07:00
Julien Duponchelle 6bf7a6aa38 Fix after jeremy feedback 2015-07-21 20:19:29 +02:00
Julien Duponchelle fc14deee1b Restore images & projects tarballs 2015-07-21 16:14:03 +02:00
Julien Duponchelle 270017d945 Allow user to backup projects and images 2015-07-21 14:20:58 +02:00
Jeremy Grossmann 23a03a24de Merge pull request #271 from lykinsbd/master
Update gns3.conf.upstart
2015-07-20 22:44:40 -07:00
Jeremy 439e0df058 Bind host on 0.0.0.0 when checking for a free UDP port. 2015-07-20 19:27:14 -06:00
Brett Lykins c94dc1ec07 Update gns3.conf.upstart
Fixing start-stop-daemon syntax so that it accepts the aruments
2015-07-20 10:42:34 -05:00
Julien Duponchelle 2ccb30eeb5 Fix Incorrect vboxmanage sudo command
Fix #247
2015-06-26 10:47:52 +02:00
Julien Duponchelle 389788cfdc 1.3.8 dev 2015-06-22 20:19:38 +02:00
Julien Duponchelle 26fc8236c9 1.3.7 2015-06-22 19:06:25 +02:00
Julien Duponchelle 4216c5d2ed Prevent install on Python 2
Fix #236
2015-06-17 14:08:22 +02:00
Julien Duponchelle afa4ba9b55 Crash report key for 1.3.7 2015-06-17 09:30:56 +02:00
grossmj 7980ae9ab1 Update version_info. 2015-06-16 15:00:25 -06:00
grossmj 3495035dbf Bump version to 1.3.7.dev1 2015-06-16 14:39:20 -06:00
Julien Duponchelle a2d4c2427d 1.3.6 2015-06-16 21:56:44 +02:00
Julien Duponchelle 0dae4b6930 1.3.6dev1 2015-06-16 19:32:53 +02:00
Julien Duponchelle 947a732bfb Remove netifaces dependencies due to IOUVM 2015-06-16 19:14:09 +02:00
Julien Duponchelle d88c5648de 1.3.5 2015-06-16 18:56:50 +02:00
Julien Duponchelle 94fbd3fac9 Ignore invalid characters when reading the output of a process
It should happend only when user try to use another binary and
the code of VPCS, dynamips... Will detect it's not the
correct binary.

For example we detect this error after an user used SupperPutty
instead of VPCS.

Fix #235
2015-06-16 15:48:59 +02:00
Julien Duponchelle 07eab6e766 Fix tests 2015-06-11 09:18:02 +02:00
Julien Duponchelle a2833cf276 Turn on / off authentication 2015-06-10 23:14:18 +02:00
Julien Duponchelle 079715bc18 Ensure no colored output on Windows
Fix #228
2015-06-09 10:05:00 +02:00
grossmj 5a32d8a779 Merge remote-tracking branch 'origin/master' 2015-06-08 11:28:58 -06:00
grossmj 9f1705a4f1 Do not stop saving IOS router configs when there is an exception while a project is committed. 2015-06-08 11:28:48 -06:00
Julien Duponchelle 02650fa490 Create a private config file if expected
Fix #217
2015-06-05 16:23:52 +02:00
Julien Duponchelle fff3e1474f Distribute our own version of netifaces working with python 3
Fix #97
2015-06-05 15:59:43 +02:00
Julien Duponchelle d9de1718b7 Fix crash if a private config exist in IOS but no private config file
Fix #217
2015-06-05 15:30:30 +02:00
Julien Duponchelle 78891ae00e Basic Auth support 2015-06-03 15:38:34 +02:00
Julien Duponchelle b344def887 Fix crash when virtualbox list of VMS return an empty line
Fix #206
2015-06-03 11:59:53 +02:00
Julien Duponchelle a1bc815f63 Update crash report key 2015-06-02 20:27:33 +02:00
Julien Duponchelle 668cc3f0a5 1.3.5dev1 2015-06-02 20:05:31 +02:00
Julien Duponchelle 42a8c7147a 1.3.4 release 2015-06-02 19:48:04 +02:00
grossmj a0fe9bb498 Control vm command has to be used instead of modify vm. Fixes #205. 2015-06-02 09:00:37 -06:00
Julien Duponchelle 57f9d875ca Fix AttributeError: 'NIONAT' object has no attribute 'lport' for
VirtualBox

Fix #205
2015-06-02 16:27:48 +02:00
Julien Duponchelle a2e51ac090 Avoid duplicate paths in qemu binary list
Fix #204
2015-06-02 15:35:14 +02:00
Julien Duponchelle 887f9b298e 1.3.4 Changelog 2015-06-02 14:44:49 +02:00
Julien Duponchelle d99047ce72 Drop useless dependencie
Fix #203
2015-06-02 14:40:29 +02:00
Jeremy ad27fdf8b9 Bump version to 1.3.4.dev2 2015-06-01 16:29:49 -06:00
Jeremy 9df290f192 Check if port or adapter is connected before starting/stopping a packet capture. Fixes #196. 2015-06-01 15:42:17 -06:00
Julien Duponchelle 05aafb9538 Revert "Start virtualbox VM one by one" because it doesn't fix the issue
This reverts commit da72a9501a.
2015-06-01 16:16:34 +02:00
Julien Duponchelle da72a9501a Start virtualbox VM one by one
Related to #190
2015-06-01 11:40:42 +02:00
Julien Duponchelle a2dfeab315 Load faulthandler module only for dev build 2015-05-28 12:17:56 +02:00
Julien Duponchelle 16cad8426a Enable faulthandler only for dev build 2015-05-28 12:17:25 +02:00
grossmj 0476f2932e Prevent users to add links to running Qemu VMs and start a capture on running VirtualBox VMs. 2015-05-27 13:56:27 -06:00
grossmj 91c0f05a4e Fixes bug: couldn't set PCMCIA disk1 size for IOS routers. 2015-05-27 10:17:46 -06:00
Julien Duponchelle 74ee73581a Fix crash if you pass an invalid hostname
Fix #198
2015-05-27 17:34:01 +02:00
Julien Duponchelle a86bac4214 Catch VPCS kill errors
Fix #199
2015-05-27 17:21:15 +02:00
Julien Duponchelle 8abf22ef24 Skip network interfaces on Travis 2015-05-27 16:45:39 +02:00
Julien Duponchelle 7cad25eb1a Raise a VirtualBox error if adapter doesn't exists
Fix #195
2015-05-27 16:38:57 +02:00
Julien Duponchelle ecf4e91e55 Ignore VirtualBox VM Name with a carriage return in name
Add tests for get_list of VirtualBox

Fix #200
2015-05-27 16:21:18 +02:00
Julien Duponchelle ea67f4aeb9 Test ok on Windows 2015-05-27 11:12:39 +02:00
Julien Duponchelle c98bcedd39 Cleanup the temporary project after modules have been notified of the
path change
2015-05-26 15:20:14 +02:00
Julien Duponchelle 528bb7a7c6 Do not return error if we can't remove the old project directory 2015-05-26 13:27:12 +02:00
Julien Duponchelle d31420b3e1 Script for starting gns3server in development mode on Windows 2015-05-26 13:06:08 +02:00
Julien Duponchelle 50d7a4f335 Catch encoding errors in windows logger 2015-05-26 13:05:37 +02:00
Julien Duponchelle 4216724d0b Give a reason for travis skip test 2015-05-26 12:00:13 +02:00
Julien Duponchelle c03c66ec48 Fix tests crash on travis 2015-05-26 11:35:06 +02:00
Julien Duponchelle dfd18f9483 Travis install netifaces 2015-05-26 10:56:35 +02:00
Julien Duponchelle 8636d3e337 Use setter for the qemu_path (allow to pass only the binary name) 2015-05-26 09:48:36 +02:00
grossmj c43b26d787 Merge remote-tracking branch 'origin/master' 2015-05-25 19:07:23 -06:00
grossmj 08f82e02a0 Fixes TAP connection when using VPCS. 2015-05-25 19:07:12 -06:00
Julien Duponchelle 33bca1a85c Fix tests on Windows 2015-05-21 12:01:37 +02:00
Julien Duponchelle 4d50d00b3e Fix test suite on Windows 2015-05-21 11:46:55 +02:00
Julien Duponchelle 21cc41fd16 Drop coveralls because it's create trouble to tests run on Windows 2015-05-21 11:05:04 +02:00
Julien Duponchelle f8d95291fa Test interfaces (it seem it's crash on Travis) 2015-05-21 10:45:07 +02:00
Julien Duponchelle 9fa873751d Fix crash launching qemu on OSX from another location.
It's append only when frozen an you launch the server by hand.

Fix #194
2015-05-18 11:58:56 +02:00
Julien Duponchelle 8c9758d16b I'm stupid... Remove fake segfault 2015-05-17 23:10:50 +02:00
Julien Duponchelle 0c5b753211 Add the fault handler in order to try to get a proper crash stack 2015-05-17 12:47:04 +02:00
grossmj 221a35baae Adds NAT NIO in device schema validation so they can return an error that it is not supported. 2015-05-14 20:54:38 -06:00
Julien Duponchelle 5bb870dc0f New crash report key 2015-05-14 19:28:26 +02:00
Julien Duponchelle 76be91d544 1.3.4dev1 2015-05-14 19:23:21 +02:00
Julien Duponchelle 078b72cafd Version 1.3.3 2015-05-14 18:57:30 +02:00
Jeremy b2457e0b3b Check for empty iourc path. 2015-05-13 16:05:54 -06:00
Jeremy 2531a05adc Merge remote-tracking branch 'origin/master' 2015-05-13 15:54:03 -06:00
Jeremy dd9f62158f Fixes bugs with IOS router configs. Fixes #354. 2015-05-13 15:53:58 -06:00
Julien Duponchelle a3c0f0754e Fix crash 2015-05-13 23:27:51 +02:00
Julien Duponchelle 157bc18ebd Use a temporary directory as egg cache
We have use with broken permission on their
system. We try to workaround the issue.

Fix #182
2015-05-13 14:29:03 +02:00
Julien Duponchelle 3704911c2d Fix tests 2015-05-13 10:19:50 +02:00
Julien Duponchelle 1e38b11f34 Catch crash error in IOU in case of permission denied
Fix #186
2015-05-13 10:16:24 +02:00
Jeremy bebdadc465 Bump version to 1.3.3.dev3 2015-05-07 11:52:17 -06:00
Julien Duponchelle b0ce091a4c 1.3.3rc1 2015-05-07 16:03:51 +02:00
Jeremy d21469a916 Return an error if an adapter slot doesn't exist on an IOS router. 2015-05-06 17:22:07 -06:00
Jeremy b57a023394 NIO NAT support for VirtualBox VMs. 2015-05-06 15:21:39 -06:00
grossmj a929dfea38 Merge remote-tracking branch 'origin/master' 2015-05-06 14:59:35 -06:00
grossmj fcff2d0813 NIO NAT support for QEMU VMs (user mode back-end is used). 2015-05-06 14:59:01 -06:00
Julien Duponchelle ed39afbf3d Throw an error if user put an invalid port range in config file
Fix #117
2015-05-06 10:40:51 +02:00
Julien Duponchelle 3ba4789ba6 New crash report key
Fix #180
2015-05-06 09:55:14 +02:00
grossmj 17b93e6a89 Bump version to 1.3.3dev2 2015-05-05 14:49:03 -06:00
Julien Duponchelle 4b21135ba7 Turn off configuration parser interpolation 2015-05-05 11:53:33 +02:00
Julien Duponchelle a3f00e1f45 Catch configuration file parsing errors
Fix #176
2015-05-05 11:44:35 +02:00
Julien Duponchelle b7dac1bec4 Force closing the event loop to avoid warning with Python 3.4.3
Fix #177
2015-05-05 11:33:47 +02:00
Julien Duponchelle 18c4154376 Catch error when you can't mark a project as no longer temporary
Fix #172
2015-05-05 10:51:51 +02:00
Julien Duponchelle 22efc7488f Catch BrokenPipeError for OSX frozen server
Fix #166
2015-05-05 10:46:09 +02:00
grossmj 9eeb8910fb Match how IOU initial-config is set for VPCS VM. 2015-05-04 21:54:56 -06:00
Jeremy 71e2586e17 Refactors how startup-config and private-config are handled for IOS routers. 2015-05-04 18:42:32 -06:00
Julien Duponchelle ee2dada88b Fix tests 2015-05-04 21:29:28 +02:00
Jeremy c4054cf810 Catch the "WinError 0 The operation completed successfully" exception at a higher level. 2015-05-04 12:14:04 -06:00
Julien Duponchelle d2d91ebdea Fix temporary project not cleanup with save as 2015-05-04 14:04:57 +02:00
Julien Duponchelle 0dea63c9ea If image is not found in VM directory look in images folder 2015-05-04 10:57:08 +02:00
grossmj 3467b42ab5 Ordered MAC addresses for QEMU based VMs. 2015-05-03 13:18:18 -06:00
grossmj 65103e9332 Merge remote-tracking branch 'origin/master' 2015-05-03 11:41:09 -06:00
grossmj f6bc823b58 Fixes #171. 2015-05-03 11:40:55 -06:00
Julien Duponchelle 151788e48a Force utf-8 configuraton files reading
Fix #170
2015-05-01 17:49:16 +02:00
Julien Duponchelle 6b70fa9794 Do not list file starting with a . in upload handler 2015-05-01 10:55:08 +02:00
Julien Duponchelle 359abb0286 Revert "Merge branch 'unstable'"
This reverts commit 929c337e8b, reversing
changes made to b9bc73fd01.
2015-04-30 16:43:30 +02:00
grossmj d18293ae7c Fixes list images in VirtualBox. 2015-04-29 22:17:17 -06:00
Jeremy 929c337e8b Merge branch 'unstable'
Conflicts:
	gns3server/modules/virtualbox/__init__.py
	gns3server/version.py
	tests/modules/test_manager.py
2015-04-29 16:29:45 -06:00
Julien Duponchelle b9bc73fd01 Do not crash when closing a project if VirtualBox is not accessible
Fix #164
2015-04-29 14:24:27 +02:00
Julien Duponchelle e75fbc9d73 Catch connection reset errors
Fix #162
2015-04-29 11:15:32 +02:00
grossmj 0311a0086e Fixes typo. 2015-04-28 22:16:15 -06:00
Julien Duponchelle 461e3ce53f 1.3.3dev1 2015-04-28 21:49:48 +02:00
Jeremy Grossmann ee1e5b8204 Merge pull request #156 from GNS3/daemon
Server daemonization support.
2015-04-24 16:38:59 -06:00
Jeremy 8f6e5b4ad8 Merge branch 'unstable' into daemon
Conflicts:
	gns3server/main.py
2015-04-24 16:37:56 -06:00
Jeremy Grossmann 14cc01bb8b Merge pull request #152 from GNS3/api_list_images
API in order to get the list of IOU, Dynamips, Qemu images.
2015-04-23 17:20:15 -06:00
Jeremy 40ce22222e Merge branch 'unstable' into api_list_images
Conflicts:
	gns3server/handlers/api/iou_handler.py
2015-04-23 17:19:37 -06:00
Jeremy Grossmann 7e991cc404 Merge pull request #154 from GNS3/wireshark_remote_capture
Support for Wireshark remote packet captures.
2015-04-23 16:17:44 -06:00
Julien Duponchelle 83f2509cfe Cleanup exceptions 2015-04-23 16:56:46 +02:00
Julien Duponchelle fac0f5ecd9 Proper daemon support 2015-04-23 11:10:02 +02:00
Julien Duponchelle 2e39265da1 Merge branch 'master' into unstable 2015-04-22 17:34:17 +02:00
Julien Duponchelle bf618d321c Max 100 thread executor 2015-04-22 10:39:43 +02:00
Julien Duponchelle 8b879c0614 Support wireshark remote capture 2015-04-20 19:27:07 +02:00
Julien Duponchelle cf0adf56a8 Merge branch 'master' into unstable 2015-04-20 19:26:27 +02:00
Julien Duponchelle dfdc18b20c Merge branch 'master' into unstable 2015-04-16 18:38:59 +02:00
Julien Duponchelle bca90bc563 API in order to get the list of IOU, Dynamips, Qemu images 2015-04-16 18:32:12 +02:00
Julien Duponchelle 5a4ffae6a2 Merge branch 'master' into unstable 2015-04-15 16:44:09 +02:00
Julien Duponchelle 789e24795e Merge branch 'master' into unstable 2015-04-15 16:29:54 +02:00
Julien Duponchelle 1d5dc2ecf0 1.4.0 dev1 2015-04-13 10:48:14 +02:00
54 changed files with 1099 additions and 218 deletions

No files matched your search

+86
View File
@@ -1,5 +1,91 @@
# Change Log
## 1.3.8 27/07/2015
* Catch ProcessLookupError when updating iouyap config. Fixes #255.
* Fixes IOS adapters and WICS cannot be removed. Fixes #282.
* Makes sure the loop is running when closing the app.
* Catch GeneratorExit exception. Fixes #231.
* Fixes missing chipset info for VirtualBox VM. Fixes #254.
* Fixes IOURC upload.
* Restore images & projects tarballs
* Allow users to backup projects and images.
* Update gns3.conf.upstart.
* Fix incorrect vboxmanage sudo command.
* Backport from 1.4: option to drop nvram & disk files for IOS routers in order to save disk space.
* Backport from 1.4: Remove timeout to wait for connections to finish.
* 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.3.7 22/06/2015
* Prevent install on Python 2
## 1.3.6 16/06/2015
* Fix an issue with 1.4dev compatibility
## 1.3.5 16/06/15
* Ignore invalid characters when reading the output of a process
* Turn on / off authentication
* Ensure no colored output on Windows
* Do not stop saving IOS router configs when there is an exception while a project is committed.
* Create a private config file if expected
* Distribute our own version of netifaces working with python 3
* Fix crash if a private config exist in IOS but no private config file
* Basic Auth support
* Fix crash when virtualbox list of VMS return an empty line
## 1.3.4 02/06/15
* Drop useless dependencie dateutil
* Check if port or adapter is connected before starting/stopping a packet capture. Fixes #196.
* Prevent users to add links to running Qemu VMs and start a capture on running VirtualBox VMs.
* Fixes bug: couldn't set PCMCIA disk1 size for IOS routers.
* Fix crash if you pass an invalid hostname
* Catch VPCS kill errors
* Raise a VirtualBox error if adapter doesn't exists
* Ignore VirtualBox VM Name with a carriage return in name
* Cleanup the temporary project after modules have been notified of the path change
* Do not return error if we can't remove the old project directory
* Catch encoding errors in windows logger
* Use setter for the qemu_path (allow to pass only the binary name)
* Fixes TAP connection when using VPCS.
* Fix crash launching qemu on OSX from another location.
* Adds NAT NIO in device schema validation so they can return an error that it is not supported.
## 1.3.3 14/05/15
* Check for empty iourc path.
* Fixes bugs with IOS router configs. Fixes #354.
* Use a temporary directory as egg cache
* Catch crash error in IOU in case of permission denied
## 1.3.3rc1 07/05/2015
* Return an error if an adapter slot doesn't exist on an IOS router.
* NIO NAT support for VirtualBox VMs.
* NIO NAT support for QEMU VMs (user mode back-end is used).
* Throw an error if user put an invalid port range in config file
* Turn off configuration parser interpolation
* Catch configuration file parsing errors
* Force closing the event loop to avoid warning with Python 3.4.3
* Catch error when you can't mark a project as no longer temporary
* Catch BrokenPipeError for OSX frozen server
* Match how IOU initial-config is set for VPCS VM.
* Refactors how startup-config and private-config are handled for IOS routers.
* Catch the "WinError 0 The operation completed successfully" exception at a higher level.
* Fix temporary project not cleanup with save as
* If image is not found in VM directory look in images folder
* Ordered MAC addresses for QEMU based VMs.
* Merge remote-tracking branch 'origin/master'
* Force utf-8 configuraton files reading
* Do not list file starting with a . in upload handler
* Do not crash when closing a project if VirtualBox is not accessible
* Catch connection reset errors
## 1.3.2 28/04/2015
* Cleanup the VirtualBox Media Manager after closing a project.
-1
View File
@@ -6,4 +6,3 @@ pep8==1.5.7
pytest-timeout
pytest-capturelog
pytest-cov
python-coveralls
+2
View File
@@ -0,0 +1,2 @@
SET PYTHONPATH=.
python.exe gns3server/main.py --debug --local
+6 -2
View File
@@ -92,7 +92,7 @@ class Config(object):
def clear(self):
"""Restart with a clean config"""
self._config = configparser.ConfigParser()
self._config = configparser.RawConfigParser()
# Override config from command line even if we modify the config file and live reload it.
self._override_config = {}
@@ -135,7 +135,11 @@ class Config(object):
Read the configuration files.
"""
parsed_files = self._config.read(self._files)
try:
parsed_files = self._config.read(self._files, encoding="utf-8")
except configparser.Error as e:
log.error("Can't parse configuration file: %s", str(e))
return
if not parsed_files:
log.warning("No configuration file could be found or read")
else:
+12 -1
View File
@@ -20,6 +20,7 @@ import sys
import struct
import platform
try:
import raven
RAVEN_AVAILABLE = True
@@ -34,13 +35,23 @@ import logging
log = logging.getLogger(__name__)
# Dev build
if __version__[4] != 0:
import faulthandler
# Display a traceback in case of segfault crash. Usefull when frozen
# Not enabled by default for security reason
log.info("Enable catching segfault")
faulthandler.enable()
class CrashReport:
"""
Report crash to a third party service
"""
DSN = "sync+https://22979234ab4749ceabce08e6da4c1476:1432c8c7a43d410b9b5bb33f8e55b2a6@app.getsentry.com/38482"
DSN = "sync+https://c06a407cbe0d4036be7873b54ebfa0ef:d6129d3fe91a4ea0b96ecbcf99678b1b@app.getsentry.com/38482"
if hasattr(sys, "frozen"):
cacert = os.path.join(os.getcwd(), "cacert.pem")
if os.path.isfile(cacert):
+31 -2
View File
@@ -26,6 +26,7 @@ from ...schemas.dynamips_vm import VM_CAPTURE_SCHEMA
from ...schemas.dynamips_vm import VM_OBJECT_SCHEMA
from ...schemas.dynamips_vm import VM_CONFIGS_SCHEMA
from ...modules.dynamips import Dynamips
from ...modules.dynamips.dynamips_error import DynamipsError
from ...modules.project_manager import ProjectManager
DEFAULT_CHASSIS = {
@@ -358,13 +359,41 @@ class DynamipsVMHandler:
project_id=request.match_info["project_id"])
startup_config_base64, private_config_base64 = yield from vm.extract_config()
module_workdir = vm.project.module_working_directory(dynamips_manager.module_name.lower())
result = {}
if startup_config_base64:
startup_config_content = base64.b64decode(startup_config_base64).decode(errors='replace')
startup_config_content = base64.b64decode(startup_config_base64).decode("utf-8", errors='replace')
result["startup_config_content"] = startup_config_content
else:
# nvram doesn't contain anything if the router has not been started at least once
# in this case just use the startup-config file
if vm.startup_config:
startup_config_path = os.path.join(module_workdir, vm.startup_config)
if os.path.isfile(startup_config_path):
try:
with open(startup_config_path, "rb") as f:
content = f.read().decode("utf-8", errors='replace')
if content:
result["startup_config_content"] = content
except OSError as e:
raise DynamipsError("Could not read the startup-config {}: {}".format(startup_config_path, e))
if private_config_base64:
private_config_content = base64.b64decode(private_config_base64).decode(errors='replace')
private_config_content = base64.b64decode(private_config_base64).decode("utf-8", errors='replace')
result["private_config_content"] = private_config_content
else:
# nvram doesn't contain anything if the router has not been started at least once
# in this case just use the private-config file
if vm.private_config:
private_config_path = os.path.join(module_workdir, vm.private_config)
if os.path.isfile(private_config_path):
try:
with open(private_config_path, "rb") as f:
content = f.read().decode("utf-8", errors='replace')
if content:
result["private_config_content"] = content
except OSError as e:
raise DynamipsError("Could not read the private-config {}: {}".format(private_config_path, e))
response.set_status(200)
response.json(result)
+4 -1
View File
@@ -81,13 +81,16 @@ class ProjectHandler:
pm = ProjectManager.instance()
project = pm.get_project(request.match_info["project_id"])
project.temporary = request.json.get("temporary", project.temporary)
project.name = request.json.get("name", project.name)
project_path = request.json.get("path", project.path)
if project_path != project.path:
old_path = project.path
project.path = project_path
for module in MODULES:
yield from module.instance().project_moved(project)
yield from project.clean_old_path(old_path)
# Very important we need to remove temporary flag after moving the project
project.temporary = request.json.get("temporary", project.temporary)
response.json(project)
@classmethod
+1 -1
View File
@@ -247,7 +247,7 @@ class QEMUHandler:
qemu_manager = Qemu.instance()
vm = qemu_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"])
nio_type = request.json["type"]
if nio_type not in ("nio_udp", "nio_tap"):
if nio_type not in ("nio_udp", "nio_tap", "nio_nat"):
raise HTTPConflict(text="NIO of type {} is not supported".format(nio_type))
nio = qemu_manager.create_nio(vm.qemu_path, request.json)
yield from vm.adapter_add_nio_binding(int(request.match_info["adapter_number"]), nio)
@@ -294,7 +294,7 @@ class VirtualBoxHandler:
vbox_manager = VirtualBox.instance()
vm = vbox_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"])
nio_type = request.json["type"]
if nio_type != "nio_udp":
if nio_type not in ("nio_udp", "nio_nat"):
raise HTTPConflict(text="NIO of type {} is not supported".format(nio_type))
nio = vbox_manager.create_nio(vbox_manager.vboxmanage_path, request.json)
yield from vm.adapter_add_nio_binding(int(request.match_info["adapter_number"]), nio)
@@ -344,7 +344,7 @@ class VirtualBoxHandler:
vm = vbox_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"])
adapter_number = int(request.match_info["adapter_number"])
pcap_file_path = os.path.join(vm.project.capture_working_directory(), request.json["capture_file_name"])
vm.start_capture(adapter_number, pcap_file_path)
yield from vm.start_capture(adapter_number, pcap_file_path)
response.json({"pcap_file_path": pcap_file_path})
@Route.post(
+88 -16
View File
@@ -18,6 +18,9 @@
import os
import aiohttp
import stat
import io
import tarfile
import asyncio
from ..config import Config
from ..web.route import Route
@@ -36,8 +39,9 @@ class UploadHandler:
try:
for root, _, files in os.walk(UploadHandler.image_directory()):
for filename in files:
image_file = os.path.join(root, filename)
uploaded_files.append(image_file)
if not filename.startswith("."):
image_file = os.path.join(root, filename)
uploaded_files.append(image_file)
except OSError:
pass
iourc_path = os.path.join(os.path.expanduser("~/"), ".iourc")
@@ -58,29 +62,97 @@ class UploadHandler:
response.redirect("/upload")
return
if data["type"] not in ["IOU", "IOURC", "QEMU", "IOS"]:
raise aiohttp.web.HTTPForbidden("You are not authorized to upload this kind of image {}".format(data["type"]))
if data["type"] not in ["IOU", "IOURC", "QEMU", "IOS", "IMAGES", "PROJECTS"]:
raise aiohttp.web.HTTPForbidden(text="You are not authorized to upload this kind of image {}".format(data["type"]))
if data["type"] == "IOURC":
destination_dir = os.path.expanduser("~/")
destination_path = os.path.join(destination_dir, ".iourc")
else:
destination_dir = os.path.join(UploadHandler.image_directory(), data["type"])
destination_path = os.path.join(destination_dir, data["file"].filename)
try:
os.makedirs(destination_dir, exist_ok=True)
with open(destination_path, "wb+") as f:
chunk = data["file"].file.read()
f.write(chunk)
st = os.stat(destination_path)
os.chmod(destination_path, st.st_mode | stat.S_IXUSR)
if data["type"] == "IMAGES":
UploadHandler._restore_directory(data["file"], UploadHandler.image_directory())
elif data["type"] == "PROJECTS":
UploadHandler._restore_directory(data["file"], UploadHandler.project_directory())
else:
if data["type"] == "IOURC":
destination_dir = os.path.expanduser("~/")
destination_path = os.path.join(destination_dir, ".iourc")
else:
destination_dir = os.path.join(UploadHandler.image_directory(), data["type"])
destination_path = os.path.join(destination_dir, data["file"].filename)
os.makedirs(destination_dir, exist_ok=True)
with open(destination_path, "wb+") as f:
chunk = data["file"].file.read()
f.write(chunk)
st = os.stat(destination_path)
os.chmod(destination_path, st.st_mode | stat.S_IXUSR)
except OSError as e:
response.html("Could not upload file: {}".format(e))
response.set_status(200)
return
response.redirect("/upload")
@classmethod
@Route.get(
r"/backup/images.tar",
description="Backup GNS3 images",
api_version=None
)
def backup_images(request, response):
yield from UploadHandler._backup_directory(request, response, UploadHandler.image_directory())
@classmethod
@Route.get(
r"/backup/projects.tar",
description="Backup GNS3 projects",
api_version=None
)
def backup_images(request, response):
yield from UploadHandler._backup_directory(request, response, UploadHandler.project_directory())
@staticmethod
def _restore_directory(file, directory):
"""
Extract from HTTP stream the content of a tar
"""
destination_path = os.path.join(directory, "archive.tar")
os.makedirs(directory, exist_ok=True)
with open(destination_path, "wb+") as f:
chunk = file.file.read()
f.write(chunk)
t = tarfile.open(destination_path)
t.extractall(directory)
t.close()
os.remove(destination_path)
@staticmethod
@asyncio.coroutine
def _backup_directory(request, response, directory):
"""
Return a tar archive from a directory
"""
response.content_type = 'application/x-gtar'
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
response.start(request)
buffer = io.BytesIO()
with tarfile.open('arch.tar', 'w', fileobj=buffer) as tar:
for root, dirs, files in os.walk(directory):
for file in files:
path = os.path.join(root, file)
tar.add(os.path.join(root, file), arcname=os.path.relpath(path, directory))
response.write(buffer.getvalue())
yield from response.drain()
buffer.truncate(0)
buffer.seek(0)
yield from response.write_eof()
@staticmethod
def image_directory():
server_config = Config.instance().get_section_config("Server")
return os.path.expanduser(server_config.get("images_path", "~/GNS3/images"))
@staticmethod
def project_directory():
server_config = Config.instance().get_section_config("Server")
return os.path.expanduser(server_config.get("projects_path", "~/GNS3/projects"))
+21 -1
View File
@@ -16,6 +16,16 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# WARNING
# Due to buggy user machines we choose to put this as the first loading modules
# otherwise the egg cache is initialized in his standard location and
# if is not writetable the application crash. It's the user fault
# because one day the user as used sudo to run an egg and break his
# filesystem permissions, but it's a common mistake.
import gns3server.utils.get_resource
import os
import datetime
import sys
@@ -176,11 +186,21 @@ def main():
Project.clean_project_directory()
CrashReport.instance()
host = server_config["host"]
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()
+14 -2
View File
@@ -34,6 +34,7 @@ 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
@@ -112,7 +113,7 @@ class BaseManager:
for future in done:
try:
future.result()
except Exception as e:
except (Exception, GeneratorExit) as e:
log.error("Could not close VM {}".format(e), exc_info=1)
continue
@@ -370,6 +371,8 @@ class BaseManager:
nio = NIOTAP(tap_device)
elif nio_settings["type"] == "nio_generic_ethernet":
nio = NIOGenericEthernet(nio_settings["ethernet_device"])
elif nio_settings["type"] == "nio_nat":
nio = NIONAT()
assert nio is not None
return nio
@@ -386,7 +389,16 @@ class BaseManager:
img_directory = self.get_images_directory()
if not os.path.isabs(path):
s = os.path.split(path)
return os.path.normpath(os.path.join(img_directory, *s))
path = os.path.normpath(os.path.join(img_directory, *s))
# Compatibility with old topologies we look in parent directory
# We look at first in new location
if not os.path.exists(path):
old_path = os.path.normpath(os.path.join(img_directory, '..', *s))
if os.path.exists(old_path):
return old_path
return path
return path
def get_relative_image_path(self, path):
+54 -35
View File
@@ -130,7 +130,7 @@ class Dynamips(BaseManager):
for future in done:
try:
future.result()
except Exception as e:
except (Exception, GeneratorExit) as e:
log.error("Could not stop device hypervisor {}".format(e), exc_info=1)
continue
@@ -154,7 +154,7 @@ class Dynamips(BaseManager):
for future in done:
try:
future.result()
except Exception as e:
except (Exception, GeneratorExit) as e:
log.error("Could not delete device {}".format(e), exc_info=1)
@asyncio.coroutine
@@ -171,8 +171,11 @@ class Dynamips(BaseManager):
files = glob.glob(os.path.join(project_dir, "*.ghost"))
files += glob.glob(os.path.join(project_dir, "*_lock"))
files += glob.glob(os.path.join(project_dir, "ilt_*"))
files += glob.glob(os.path.join(project_dir, "c[0-9][0-9][0-9][0-9]_*_rommon_vars"))
files += glob.glob(os.path.join(project_dir, "c[0-9][0-9][0-9][0-9]_*_ssa"))
files += glob.glob(os.path.join(project_dir, "c[0-9][0-9][0-9][0-9]_i[0-9]*_rommon_vars"))
files += glob.glob(os.path.join(project_dir, "c[0-9][0-9][0-9][0-9]_i[0-9]*_ssa"))
files += glob.glob(os.path.join(project_dir, "c[0-9][0-9][0-9][0-9]_i[0-9]*_log.txt"))
files += glob.glob(os.path.join(project_dir, "c[0-9][0-9][0-9][0-9]_i[0-9]*_rom"))
files += glob.glob(os.path.join(project_dir, "c[0-9][0-9][0-9][0-9]_i[0-9]*_bootflash"))
for file in files:
try:
log.debug("Deleting file {}".format(file))
@@ -210,7 +213,11 @@ class Dynamips(BaseManager):
# save the configs when the project is committed
for vm in self._vms.copy().values():
if vm.project.id == project.id:
yield from vm.save_configs()
try:
yield from vm.save_configs()
except DynamipsError as e:
log.warning(e)
continue
@property
def dynamips_path(self):
@@ -410,6 +417,8 @@ class Dynamips(BaseManager):
nio = NIOVDE(node.hypervisor, control_file, local_file)
elif nio_settings["type"] == "nio_null":
nio = NIONull(node.hypervisor)
else:
raise aiohttp.web.HTTPConflict(text="NIO of type {} is not supported".format(nio_settings["type"]))
yield from nio.create()
return nio
@@ -471,31 +480,42 @@ class Dynamips(BaseManager):
if hasattr(vm, "set_{}".format(name)):
setter = getattr(vm, "set_{}".format(name))
yield from setter(value)
elif name.startswith("slot") and value in ADAPTER_MATRIX:
slot_id = int(name[-1])
adapter_name = value
adapter = ADAPTER_MATRIX[adapter_name]()
if vm.slots[slot_id] and not isinstance(vm.slots[slot_id], type(adapter)):
yield from vm.slot_remove_binding(slot_id)
if not isinstance(vm.slots[slot_id], type(adapter)):
yield from vm.slot_add_binding(slot_id, adapter)
try:
if vm.slots[slot_id] and not isinstance(vm.slots[slot_id], type(adapter)):
yield from vm.slot_remove_binding(slot_id)
if not isinstance(vm.slots[slot_id], type(adapter)):
yield from vm.slot_add_binding(slot_id, adapter)
except IndexError:
raise DynamipsError("Slot {} doesn't exist on this router".format(slot_id))
elif name.startswith("slot") and value is None:
slot_id = int(name[-1])
if vm.slots[slot_id]:
yield from vm.slot_remove_binding(slot_id)
try:
if vm.slots[slot_id]:
yield from vm.slot_remove_binding(slot_id)
except IndexError:
raise DynamipsError("Slot {} doesn't exist on this router".format(slot_id))
elif name.startswith("wic") and value in WIC_MATRIX:
wic_slot_id = int(name[-1])
wic_name = value
wic = WIC_MATRIX[wic_name]()
if vm.slots[0].wics[wic_slot_id] and not isinstance(vm.slots[0].wics[wic_slot_id], type(wic)):
yield from vm.uninstall_wic(wic_slot_id)
if not isinstance(vm.slots[0].wics[wic_slot_id], type(wic)):
yield from vm.install_wic(wic_slot_id, wic)
try:
if vm.slots[0].wics[wic_slot_id] and not isinstance(vm.slots[0].wics[wic_slot_id], type(wic)):
yield from vm.uninstall_wic(wic_slot_id)
if not isinstance(vm.slots[0].wics[wic_slot_id], type(wic)):
yield from vm.install_wic(wic_slot_id, wic)
except IndexError:
raise DynamipsError("WIC slot {} doesn't exist on this router".format(wic_slot_id))
elif name.startswith("wic") and value is None:
wic_slot_id = int(name[-1])
if vm.slots[0].wics and vm.slots[0].wics[wic_slot_id]:
yield from vm.uninstall_wic(wic_slot_id)
try:
if vm.slots[0].wics and vm.slots[0].wics[wic_slot_id]:
yield from vm.uninstall_wic(wic_slot_id)
except IndexError:
raise DynamipsError("WIC slot {} doesn't exist on this router".format(wic_slot_id))
mmap_support = self.config.get_section_config("Dynamips").getboolean("mmap_support", True)
if mmap_support is False:
@@ -521,38 +541,34 @@ class Dynamips(BaseManager):
default_startup_config_path = os.path.join(module_workdir, "configs", "i{}_startup-config.cfg".format(vm.dynamips_id))
default_private_config_path = os.path.join(module_workdir, "configs", "i{}_private-config.cfg".format(vm.dynamips_id))
startup_config_path = settings.get("startup_config")
startup_config_content = settings.get("startup_config_content")
if startup_config_content:
startup_config_path = self._create_config(vm, startup_config_content, default_startup_config_path)
if startup_config_path:
yield from vm.set_configs(startup_config_path)
elif startup_config_content:
startup_config_path = self._create_config(vm, default_startup_config_path, startup_config_content)
yield from vm.set_configs(startup_config_path)
else:
startup_config_path = settings.get("startup_config")
if startup_config_path:
yield from vm.set_configs(startup_config_path)
private_config_path = settings.get("private_config")
private_config_content = settings.get("private_config_content")
if private_config_content:
private_config_path = self._create_config(vm, private_config_content, default_private_config_path)
if private_config_path:
yield from vm.set_configs(vm.startup_config, private_config_path)
elif private_config_content:
private_config_path = self._create_config(vm, default_private_config_path, private_config_content)
yield from vm.set_configs(vm.startup_config, private_config_path)
else:
private_config_path = settings.get("private_config")
if private_config_path:
yield from vm.set_configs(vm.startup_config, private_config_path)
def _create_config(self, vm, content, path):
def _create_config(self, vm, path, content=None):
"""
Creates a config file.
:param vm: VM instance
:param content: config content
:param path: path to the destination config file
:param content: config content
:returns: relative path to the created config file
"""
log.info("Creating config file {}".format(path))
content = "!\n" + content.replace("\r", "")
content = content.replace('%h', vm.name)
config_dir = os.path.dirname(path)
try:
os.makedirs(config_dir, exist_ok=True)
@@ -561,7 +577,10 @@ class Dynamips(BaseManager):
try:
with open(path, "wb") as f:
f.write(content.encode("utf-8"))
if content:
content = "!\n" + content.replace("\r", "")
content = content.replace('%h', vm.name)
f.write(content.encode("utf-8"))
except OSError as e:
raise DynamipsError("Could not create config file {}: {}".format(path, e))
@@ -276,15 +276,16 @@ class DynamipsHypervisor:
while True:
try:
try:
line = yield from self._reader.readline()
#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
# any remaining data sent by the hypervisor
continue
if not line:
if not chunk:
raise DynamipsError("No data returned from {host}:{port}, Dynamips process running: {run}"
.format(host=self._host, port=self._port, run=self.is_running()))
buf += line.decode("utf-8")
buf += chunk.decode("utf-8")
except OSError as e:
raise DynamipsError("Lost communication with {host}:{port} :{error}, Dynamips process running: {run}"
.format(host=self._host, port=self._port, error=e, run=self.is_running()))
@@ -298,6 +298,9 @@ class EthernetSwitch(Device):
nio = self._nios[port_number]
if not nio:
raise DynamipsError("Port {} is not connected".format(port_number))
data_link_type = data_link_type.lower()
if data_link_type.startswith("dlt_"):
data_link_type = data_link_type[4:]
@@ -324,6 +327,10 @@ class EthernetSwitch(Device):
raise DynamipsError("Port {} is not allocated".format(port_number))
nio = self._nios[port_number]
if not nio:
raise DynamipsError("Port {} is not connected".format(port_number))
yield from nio.unbind_filter("both")
log.info('Ethernet switch "{name}" [{id}]: stopping packet capture on port {port}'.format(name=self._name,
id=self._id,
+94 -11
View File
@@ -87,6 +87,7 @@ class Router(BaseVM):
self._exec_area = 64 # 64 MB on other systems
self._disk0 = 0 # Megabytes
self._disk1 = 0 # Megabytes
self._auto_delete_disks = False
self._aux = aux
self._mac_addr = ""
self._system_id = "FTX0945W0MY" # processor board ID in IOS
@@ -145,6 +146,7 @@ class Router(BaseVM):
"exec_area": self._exec_area,
"disk0": self._disk0,
"disk1": self._disk1,
"auto_delete_disks": self._auto_delete_disks,
"console": self._console,
"aux": self._aux,
"mac_addr": self._mac_addr,
@@ -158,7 +160,7 @@ class Router(BaseVM):
for slot in self._slots:
if slot:
slot = str(slot)
router_info["slot" + str(slot_number)] = slot
router_info["slot" + str(slot_number)] = slot
slot_number += 1
# add the wics
@@ -166,6 +168,8 @@ class Router(BaseVM):
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])
else:
router_info["wic" + str(wic_slot_number)] = None
return router_info
@@ -182,6 +186,16 @@ class Router(BaseVM):
@asyncio.coroutine
def create(self):
# delete any previous file with same Dynamips identifier
project_dir = os.path.join(self.project.module_working_directory(self.manager.module_name.lower()))
for file in glob.glob(os.path.join(project_dir, "c[0-9][0-9][0-9][0-9]_i{}_*".format(self._dynamips_id))):
try:
log.debug("Deleting file {}".format(file))
yield from wait_run_in_executor(os.remove, file)
except OSError as e:
log.warn("Could not delete file {}: {}".format(file, e))
continue
if not self._hypervisor:
module_workdir = self.project.module_working_directory(self.manager.module_name.lower())
self._hypervisor = yield from self.manager.start_new_hypervisor(working_dir=module_workdir)
@@ -342,6 +356,20 @@ class Router(BaseVM):
pass
yield from self.hypervisor.stop()
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(project_dir, "{}_i{}_disk[0-1]".format(self.platform, self.dynamips_id)))
files += glob.glob(os.path.join(project_dir, "{}_i{}_slot[0-1]".format(self.platform, self.dynamips_id)))
files += glob.glob(os.path.join(project_dir, "{}_i{}_nvram".format(self.platform, self.dynamips_id)))
files += glob.glob(os.path.join(project_dir, "{}_i{}_flash[0-1]".format(self.platform, self.dynamips_id)))
for file in files:
try:
log.debug("Deleting file {}".format(file))
yield from wait_run_in_executor(os.remove, file)
except OSError as e:
log.warn("Could not delete file {}: {}".format(file, e))
continue
self._closed = True
@property
@@ -836,7 +864,7 @@ class Router(BaseVM):
return self._disk1
@asyncio.coroutine
def disk1(self, disk1):
def set_disk1(self, disk1):
"""
Sets the size (MB) for PCMCIA disk1.
@@ -851,6 +879,30 @@ class Router(BaseVM):
new_disk1=disk1))
self._disk1 = disk1
@property
def auto_delete_disks(self):
"""
Returns True if auto delete disks is enabled on this router.
:returns: boolean either auto delete disks is activated or not
"""
return self._auto_delete_disks
@asyncio.coroutine
def set_auto_delete_disks(self, auto_delete_disks):
"""
Enable/disable use of auto delete disks
:param auto_delete_disks: activate/deactivate auto delete disks (boolean)
"""
if auto_delete_disks:
log.info('Router "{name}" [{id}]: auto delete disks enabled'.format(name=self._name, id=self._id))
else:
log.info('Router "{name}" [{id}]: auto delete disks disabled'.format(name=self._name, id=self._id))
self._auto_delete_disks = auto_delete_disks
@asyncio.coroutine
def set_console(self, console):
"""
@@ -1178,11 +1230,20 @@ 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))
yield from self._hypervisor.send('vm slot_add_nio_binding "{name}" {slot_number} {port_number} {nio}'.format(name=self._name,
slot_number=slot_number,
port_number=port_number,
nio=nio))
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,
port_number=port_number,
nio=nio))
except DynamipsError:
# in case of error try to remove and add the nio binding
yield from self._hypervisor.send('vm slot_remove_nio_binding "{name}" {slot_number} {port_number}'.format(name=self._name,
slot_number=slot_number,
port_number=port_number))
yield from self._hypervisor.send('vm slot_add_nio_binding "{name}" {slot_number} {port_number} {nio}'.format(name=self._name,
slot_number=slot_number,
port_number=port_number,
nio=nio))
log.info('Router "{name}" [{id}]: NIO {nio_name} bound to port {slot_number}/{port_number}'.format(name=self._name,
id=self._id,
@@ -1303,6 +1364,10 @@ class Router(BaseVM):
nio = adapter.get_nio(port_number)
if not nio:
raise DynamipsError("Port {slot_number}/{port_number} is not connected".format(slot_number=slot_number,
port_number=port_number))
if nio.input_filter[0] is not None and nio.output_filter[0] is not None:
raise DynamipsError("Port {port_number} has already a filter applied on {adapter}".format(adapter=adapter,
port_number=port_number))
@@ -1335,6 +1400,11 @@ class Router(BaseVM):
port_number=port_number))
nio = adapter.get_nio(port_number)
if not nio:
raise DynamipsError("Port {slot_number}/{port_number} is not connected".format(slot_number=slot_number,
port_number=port_number))
yield from nio.unbind_filter("both")
log.info('Router "{name}" [{id}]: stopping packet capture on port {slot_number}/{port_number}'.format(name=self._name,
@@ -1436,6 +1506,17 @@ class Router(BaseVM):
private_config = private_config.replace("\\", '/')
if self._startup_config != startup_config or self._private_config != private_config:
self._startup_config = startup_config
self._private_config = private_config
module_workdir = self.project.module_working_directory(self.manager.module_name.lower())
private_config_path = os.path.join(module_workdir, private_config)
try:
if not os.path.getsize(private_config_path):
# an empty private-config can prevent a router to boot.
private_config = ''
except OSError as e:
raise DynamipsError("Cannot access the private-config {}: {}".format(private_config_path, e))
yield from self._hypervisor.send('vm set_config "{name}" "{startup}" "{private}"'.format(name=self._name,
startup=startup_config,
@@ -1445,15 +1526,11 @@ class Router(BaseVM):
id=self._id,
startup=startup_config))
self._startup_config = startup_config
if private_config:
log.info('Router "{name}" [{id}]: has a new private-config set: "{private}"'.format(name=self._name,
id=self._id,
private=private_config))
self._private_config = private_config
@asyncio.coroutine
def extract_config(self):
"""
@@ -1483,6 +1560,9 @@ class Router(BaseVM):
module_workdir = self.project.module_working_directory(self.manager.module_name.lower())
startup_config_base64, private_config_base64 = yield from self.extract_config()
if startup_config_base64:
if not self.startup_config:
self._startup_config = os.path.join("configs", "i{}_startup-config.cfg".format(self._dynamips_id))
try:
config = base64.b64decode(startup_config_base64).decode("utf-8", errors="replace")
config = "!\n" + config.replace("\r", "")
@@ -1494,6 +1574,9 @@ class Router(BaseVM):
raise DynamipsError("Could not save the startup configuration {}: {}".format(config_path, e))
if private_config_base64:
if not self.private_config:
self._private_config = os.path.join("configs", "i{}_private-config.cfg".format(self._dynamips_id))
try:
config = base64.b64decode(private_config_base64).decode("utf-8", errors="replace")
config = "!\n" + config.replace("\r", "")
+22 -8
View File
@@ -433,11 +433,14 @@ class IOUVM(BaseVM):
yield from self._library_check()
self._rename_nvram_file()
try:
self._rename_nvram_file()
except OSError as e:
raise IOUError("Could not rename nvram files: {}".format(e))
iourc_path = self.iourc_path
if iourc_path is None:
raise IOUError("Could not find a iourc file (IOU license)")
if not iourc_path:
raise IOUError("Could not find an iourc file (IOU license)")
if not os.path.isfile(iourc_path):
raise IOUError("The iourc path '{}' is not a regular file".format(iourc_path))
@@ -859,7 +862,10 @@ class IOUVM(BaseVM):
port_number=port_number))
if self.is_iouyap_running():
self._update_iouyap_config()
os.kill(self._iouyap_process.pid, signal.SIGHUP)
try:
os.kill(self._iouyap_process.pid, signal.SIGHUP)
except ProcessLookupError:
log.error("Could not update iouyap configuration: process (PID={}) not found".format(self._iouyap_process.pid))
def adapter_remove_nio_binding(self, adapter_number, port_number):
"""
@@ -891,8 +897,10 @@ class IOUVM(BaseVM):
port_number=port_number))
if self.is_iouyap_running():
self._update_iouyap_config()
os.kill(self._iouyap_process.pid, signal.SIGHUP)
try:
os.kill(self._iouyap_process.pid, signal.SIGHUP)
except ProcessLookupError:
log.error("Could not update iouyap configuration: process (PID={}) not found".format(self._iouyap_process.pid))
return nio
@property
@@ -1049,7 +1057,10 @@ class IOUVM(BaseVM):
if self.is_iouyap_running():
self._update_iouyap_config()
os.kill(self._iouyap_process.pid, signal.SIGHUP)
try:
os.kill(self._iouyap_process.pid, signal.SIGHUP)
except ProcessLookupError:
log.error("Could not update iouyap configuration: process (PID={}) not found".format(self._iouyap_process.pid))
@asyncio.coroutine
def stop_capture(self, adapter_number, port_number):
@@ -1082,4 +1093,7 @@ class IOUVM(BaseVM):
port_number=port_number))
if self.is_iouyap_running():
self._update_iouyap_config()
os.kill(self._iouyap_process.pid, signal.SIGHUP)
try:
os.kill(self._iouyap_process.pid, signal.SIGHUP)
except ProcessLookupError:
log.error("Could not update iouyap configuration: process (PID={}) not found".format(self._iouyap_process.pid))
+41
View File
@@ -0,0 +1,41 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""
Interface for NAT NIOs.
"""
from .nio import NIO
class NIONAT(NIO):
"""
NAT NIO.
"""
def __init__(self):
super().__init__()
def __str__(self):
return "NIO TAP"
def __json__(self):
return {"type": "nio_nat"}
+3 -2
View File
@@ -34,8 +34,9 @@ class PortManager:
def __init__(self, host="127.0.0.1"):
self._console_host = host
self._udp_host = host
# UDP host must be 0.0.0.0, reason: https://github.com/GNS3/gns3-server/issues/265
self._udp_host = "0.0.0.0"
self._used_tcp_ports = set()
self._used_udp_ports = set()
@@ -140,7 +141,7 @@ class PortManager:
"""
if end_port < start_port:
raise Exception("Invalid port range {}-{}".format(start_port, end_port))
raise HTTPConflict(text="Invalid port range {}-{}".format(start_port, end_port))
if socket_type == "UDP":
socket_type = socket.SOCK_DGRAM
+21 -2
View File
@@ -139,9 +139,25 @@ 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()
@asyncio.coroutine
def clean_old_path(self, old_path):
"""
Called after a project location change. All the modules should
have been notified before
"""
if self._temporary:
try:
yield from wait_run_in_executor(shutil.rmtree, old_path)
except OSError as e:
log.warn("Can't remove temporary directory {}: {}".format(old_path, e))
@property
def name(self):
@@ -228,7 +244,10 @@ class Project:
raise aiohttp.web.HTTPInternalServerError(text="Could not create temporary project: {}".format(e))
else:
if os.path.exists(os.path.join(self._path, ".gns3_temporary")):
os.remove(os.path.join(self._path, ".gns3_temporary"))
try:
os.remove(os.path.join(self._path, ".gns3_temporary"))
except OSError as e:
raise aiohttp.web.HTTPInternalServerError(text="Could not mark project as no longer temporary: {}".format(e))
def module_working_directory(self, module_name):
"""
@@ -344,7 +363,7 @@ class Project:
for future in done:
try:
future.result()
except Exception as e:
except (Exception, GeneratorExit) as e:
log.error("Could not close VM or device {}".format(e), exc_info=1)
if cleanup and os.path.exists(self.path):
+12 -8
View File
@@ -47,13 +47,13 @@ class Qemu(BaseManager):
"""
qemus = []
paths = []
paths = set()
try:
paths.append(os.getcwd())
paths.add(os.getcwd())
except FileNotFoundError:
log.warning("The current working directory doesn't exist")
if "PATH" in os.environ:
paths.extend(os.environ["PATH"].split(os.pathsep))
paths.update(os.environ["PATH"].split(os.pathsep))
else:
log.warning("The PATH environment variable doesn't exist")
# look for Qemu binaries in the current working directory and $PATH
@@ -64,17 +64,21 @@ class Qemu(BaseManager):
exec_dir = os.path.dirname(os.path.abspath(sys.executable))
for f in os.listdir(exec_dir):
if f.lower().startswith("qemu"):
paths.append(os.path.join(exec_dir, f))
paths.add(os.path.join(exec_dir, f))
if "PROGRAMFILES(X86)" in os.environ and os.path.exists(os.environ["PROGRAMFILES(X86)"]):
paths.append(os.path.join(os.environ["PROGRAMFILES(X86)"], "qemu"))
paths.add(os.path.join(os.environ["PROGRAMFILES(X86)"], "qemu"))
if "PROGRAMFILES" in os.environ and os.path.exists(os.environ["PROGRAMFILES"]):
paths.append(os.path.join(os.environ["PROGRAMFILES"], "qemu"))
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.extend(["/usr/local/bin", "/opt/local/bin"])
paths.update(["/usr/local/bin", "/opt/local/bin"])
if hasattr(sys, "frozen"):
paths.append(os.path.abspath(os.path.join(os.getcwd(), "../../../qemu/bin/")))
try:
paths.add(os.path.abspath(os.path.join(os.getcwd(), "../../../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:
try:
for f in os.listdir(path):
+30 -27
View File
@@ -23,7 +23,6 @@ order to run a QEMU VM.
import sys
import os
import shutil
import random
import subprocess
import shlex
import asyncio
@@ -33,6 +32,7 @@ from .qemu_error import QemuError
from ..adapters.ethernet_adapter import EthernetAdapter
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
@@ -69,7 +69,7 @@ class QemuVM(BaseVM):
self._stdout_file = ""
# QEMU VM settings
self._qemu_path = qemu_path
self.qemu_path = qemu_path
self._hda_disk_image = ""
self._hdb_disk_image = ""
self._hdc_disk_image = ""
@@ -741,6 +741,8 @@ class QemuVM(BaseVM):
adapter_number=adapter_number))
if self.is_running():
raise QemuError("Sorry, adding a link to a started Qemu VM is not supported.")
# FIXME: does the code below work? very undocumented feature...
# dynamically configure an UDP tunnel on the QEMU VM adapter
if nio and isinstance(nio, NIOUDP):
if self._legacy_networking:
@@ -751,7 +753,6 @@ class QemuVM(BaseVM):
nio.rport,
nio.rhost))
else:
# FIXME: does it work? very undocumented feature...
# Apparently there is a bug in Qemu...
# netdev_add [user|tap|socket|hubport|netmap],id=str[,prop=value][,...] -- add host network device
# netdev_del id -- remove host network device
@@ -785,6 +786,7 @@ class QemuVM(BaseVM):
adapter_number=adapter_number))
if self.is_running():
# FIXME: does the code below work? very undocumented feature...
# dynamically disable the QEMU VM adapter
yield from self._control_vm("host_net_remove {} gns3-{}".format(adapter_number, adapter_number))
yield from self._control_vm("host_net_add user vlan={},name=gns3-{}".format(adapter_number, adapter_number))
@@ -981,46 +983,47 @@ class QemuVM(BaseVM):
return options
def _get_random_mac(self, adapter_number):
# TODO: let users specify a base mac address
return "00:00:ab:%02x:%02x:%02d" % (random.randint(0x00, 0xff), random.randint(0x00, 0xff), adapter_number)
def _network_options(self):
network_options = []
adapter_number = 0
for adapter in self._ethernet_adapters:
mac = self._get_random_mac(adapter_number)
if self._legacy_networking:
network_options.extend(["-net", "nic,vlan={},macaddr={},model={}".format(adapter_number, mac, self._adapter_type)])
else:
network_options.extend(["-device", "{},mac={},netdev=gns3-{}".format(self._adapter_type, mac, adapter_number)])
network_options.extend(["-net", "none"]) # we do not want any user networking back-end if no adapter is connected.
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)
nio = adapter.get_nio(0)
if nio:
if isinstance(nio, NIOUDP):
if self._legacy_networking:
if self._legacy_networking:
# legacy QEMU networking syntax (-net)
if nio:
network_options.extend(["-net", "nic,vlan={},macaddr={},model={}".format(adapter_number, mac, self._adapter_type)])
if isinstance(nio, NIOUDP):
network_options.extend(["-net", "udp,vlan={},name=gns3-{},sport={},dport={},daddr={}".format(adapter_number,
adapter_number,
nio.lport,
nio.rport,
nio.rhost)])
else:
elif isinstance(nio, NIOTAP):
network_options.extend(["-net", "tap,name=gns3-{},ifname={}".format(adapter_number, nio.tap_device)])
elif isinstance(nio, NIONAT):
network_options.extend(["-net", "user,vlan={},name=gns3-{}".format(adapter_number, adapter_number)])
else:
network_options.extend(["-net", "nic,vlan={},macaddr={},model={}".format(adapter_number, mac, self._adapter_type)])
else:
# newer QEMU networking syntax
if nio:
network_options.extend(["-device", "{},mac={},netdev=gns3-{}".format(self._adapter_type, mac, adapter_number)])
if isinstance(nio, NIOUDP):
network_options.extend(["-netdev", "socket,id=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_number,
nio.rhost,
nio.rport,
self._host,
nio.lport)])
elif isinstance(nio, NIOTAP):
if self._legacy_networking:
network_options.extend(["-net", "tap,name=gns3-{},ifname={}".format(adapter_number, nio.tap_device)])
else:
elif isinstance(nio, NIOTAP):
network_options.extend(["-netdev", "tap,id=gns3-{},ifname={}".format(adapter_number, nio.tap_device)])
else:
if self._legacy_networking:
network_options.extend(["-net", "user,vlan={},name=gns3-{}".format(adapter_number, adapter_number)])
elif isinstance(nio, NIONAT):
network_options.extend(["-netdev", "user,id=gns3-{}".format(adapter_number)])
else:
network_options.extend(["-netdev", "user,id=gns3-{}".format(adapter_number)])
adapter_number += 1
network_options.extend(["-device", "{},mac={}".format(self._adapter_type, mac)])
return network_options
+10 -3
View File
@@ -97,7 +97,7 @@ class VirtualBox(BaseManager):
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)
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)
@@ -122,7 +122,12 @@ class VirtualBox(BaseManager):
"""
hdds = []
properties = yield from self.execute("list", ["hdds"])
try:
properties = yield from self.execute("list", ["hdds"])
# If VirtualBox is not available we have no inaccessible hdd
except VirtualBoxError:
return hdds
flag_inaccessible = False
for prop in properties:
try:
@@ -151,7 +156,7 @@ class VirtualBox(BaseManager):
try:
yield from self.execute("closemedium", ["disk", hdd_file])
except VirtualBoxError as e:
log.warning("Could not close VirtualBox VM disk file {}: {error}".format(os.path.basename(hdd_file), e))
log.warning("Could not close VirtualBox VM disk file {}: {}".format(os.path.basename(hdd_file), e))
continue
@asyncio.coroutine
@@ -163,6 +168,8 @@ class VirtualBox(BaseManager):
vms = []
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
vmname, _ = line.rsplit(' ', 1)
vmname = vmname.strip('"')
if vmname == "<inaccessible>":
+45 -22
View File
@@ -31,6 +31,7 @@ import asyncio
from pkg_resources import parse_version
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
@@ -573,10 +574,11 @@ class VirtualBoxVM(BaseVM):
# check the maximum number of adapters supported by the VM
vm_info = yield from self._get_vm_info()
chipset = vm_info["chipset"]
maximum_adapters = 8
if chipset == "ich9":
maximum_adapters = int(self._system_properties["Maximum ICH9 Network Adapter count"])
if "chipset" in vm_info:
chipset = vm_info["chipset"]
if chipset == "ich9":
maximum_adapters = int(self._system_properties["Maximum ICH9 Network Adapter count"])
return maximum_adapters
def _get_pipe_name(self):
@@ -659,12 +661,12 @@ class VirtualBoxVM(BaseVM):
yield from self._modify_vm("--cableconnected{} off".format(adapter_number + 1))
nio = self._ethernet_adapters[adapter_number].get_nio(0)
if nio:
if not self._use_any_adapter and attachment not in ("none", "null", "generic"):
if not isinstance(nio, NIONAT) and not self._use_any_adapter and attachment not in ("none", "null", "generic"):
raise VirtualBoxError("Attachment ({}) already configured on adapter {}. "
"Please set it to 'Not attached' to allow GNS3 to use it.".format(attachment,
adapter_number + 1))
yield from self._modify_vm("--nictrace{} off".format(adapter_number + 1))
yield from self._modify_vm("--nictrace{} off".format(adapter_number + 1))
vbox_adapter_type = "82540EM"
if self._adapter_type == "PCnet-PCI II (Am79C970A)":
vbox_adapter_type = "Am79C970A"
@@ -681,13 +683,17 @@ class VirtualBoxVM(BaseVM):
args = [self._vmname, "--nictype{}".format(adapter_number + 1), vbox_adapter_type]
yield from self.manager.execute("modifyvm", args)
log.debug("setting UDP params on adapter {}".format(adapter_number))
yield from self._modify_vm("--nic{} generic".format(adapter_number + 1))
yield from self._modify_vm("--nicgenericdrv{} UDPTunnel".format(adapter_number + 1))
yield from self._modify_vm("--nicproperty{} sport={}".format(adapter_number + 1, nio.lport))
yield from self._modify_vm("--nicproperty{} dest={}".format(adapter_number + 1, nio.rhost))
yield from self._modify_vm("--nicproperty{} dport={}".format(adapter_number + 1, nio.rport))
yield from self._modify_vm("--cableconnected{} on".format(adapter_number + 1))
if isinstance(nio, NIOUDP):
log.debug("setting UDP params on adapter {}".format(adapter_number))
yield from self._modify_vm("--nic{} generic".format(adapter_number + 1))
yield from self._modify_vm("--nicgenericdrv{} UDPTunnel".format(adapter_number + 1))
yield from self._modify_vm("--nicproperty{} sport={}".format(adapter_number + 1, nio.lport))
yield from self._modify_vm("--nicproperty{} dest={}".format(adapter_number + 1, nio.rhost))
yield from self._modify_vm("--nicproperty{} dport={}".format(adapter_number + 1, nio.rport))
yield from self._modify_vm("--cableconnected{} on".format(adapter_number + 1))
elif isinstance(nio, NIONAT):
yield from self._modify_vm("--nic{} nat".format(adapter_number + 1))
yield from self._modify_vm("--cableconnected{} on".format(adapter_number + 1))
if nio.capturing:
yield from self._modify_vm("--nictrace{} on".format(adapter_number + 1))
@@ -794,18 +800,22 @@ class VirtualBoxVM(BaseVM):
try:
adapter = self._ethernet_adapters[adapter_number]
except IndexError:
except KeyError:
raise VirtualBoxError("Adapter {adapter_number} doesn't exist on VirtualBox VM '{name}'".format(name=self.name,
adapter_number=adapter_number))
vm_state = yield from self._get_vm_state()
if vm_state == "running":
# dynamically configure an UDP tunnel on the VirtualBox adapter
yield from self._control_vm("nic{} generic UDPTunnel".format(adapter_number + 1))
yield from self._control_vm("nicproperty{} sport={}".format(adapter_number + 1, nio.lport))
yield from self._control_vm("nicproperty{} dest={}".format(adapter_number + 1, nio.rhost))
yield from self._control_vm("nicproperty{} dport={}".format(adapter_number + 1, nio.rport))
yield from self._control_vm("setlinkstate{} on".format(adapter_number + 1))
if isinstance(nio, NIOUDP):
# dynamically configure an UDP tunnel on the VirtualBox adapter
yield from self._control_vm("nic{} generic UDPTunnel".format(adapter_number + 1))
yield from self._control_vm("nicproperty{} sport={}".format(adapter_number + 1, nio.lport))
yield from self._control_vm("nicproperty{} dest={}".format(adapter_number + 1, nio.rhost))
yield from self._control_vm("nicproperty{} dport={}".format(adapter_number + 1, nio.rport))
yield from self._control_vm("setlinkstate{} on".format(adapter_number + 1))
elif isinstance(nio, NIONAT):
yield from self._control_vm("nic{} nat".format(adapter_number + 1))
yield from self._control_vm("setlinkstate{} on".format(adapter_number + 1))
adapter.add_nio(0, nio)
log.info("VirtualBox VM '{name}' [{id}]: {nio} added to adapter {adapter_number}".format(name=self.name,
@@ -825,7 +835,7 @@ class VirtualBoxVM(BaseVM):
try:
adapter = self._ethernet_adapters[adapter_number]
except IndexError:
except KeyError:
raise VirtualBoxError("Adapter {adapter_number} doesn't exist on VirtualBox VM '{name}'".format(name=self.name,
adapter_number=adapter_number))
@@ -846,6 +856,7 @@ class VirtualBoxVM(BaseVM):
adapter_number=adapter_number))
return nio
@asyncio.coroutine
def start_capture(self, adapter_number, output_file):
"""
Starts a packet capture.
@@ -856,11 +867,19 @@ class VirtualBoxVM(BaseVM):
try:
adapter = self._ethernet_adapters[adapter_number]
except IndexError:
except KeyError:
raise VirtualBoxError("Adapter {adapter_number} doesn't exist on VirtualBox VM '{name}'".format(name=self.name,
adapter_number=adapter_number))
vm_state = yield from self._get_vm_state()
if vm_state == "running" or vm_state == "paused" or vm_state == "stuck":
raise VirtualBoxError("Sorry, packet capturing on a started VirtualBox VM is not supported.")
nio = adapter.get_nio(0)
if not nio:
raise VirtualBoxError("Adapter {} is not connected".format(adapter_number))
if nio.capturing:
raise VirtualBoxError("Packet capture is already activated on adapter {adapter_number}".format(adapter_number=adapter_number))
@@ -878,11 +897,15 @@ class VirtualBoxVM(BaseVM):
try:
adapter = self._ethernet_adapters[adapter_number]
except IndexError:
except KeyError:
raise VirtualBoxError("Adapter {adapter_number} doesn't exist on VirtualBox VM '{name}'".format(name=self.name,
adapter_number=adapter_number))
nio = adapter.get_nio(0)
if not nio:
raise VirtualBoxError("Adapter {} is not connected".format(adapter_number))
nio.stopPacketCapture()
log.info("VirtualBox VM '{name}' [{id}]: stopping packet capture on adapter {adapter_number}".format(name=self.name,
+12 -10
View File
@@ -181,15 +181,15 @@ class VPCSVM(BaseVM):
"""
try:
script_file = os.path.join(self.working_dir, 'startup.vpc')
with open(script_file, "wb+") as f:
startup_script_path = os.path.join(self.working_dir, 'startup.vpc')
with open(startup_script_path, "w+", encoding='utf-8') as f:
if startup_script is None:
f.write(b'')
f.write('')
else:
startup_script = startup_script.replace("%h", self._name)
f.write(startup_script.encode("utf-8"))
f.write(startup_script)
except OSError as e:
raise VPCSError('Cannot write the startup script file "{}": {}'.format(self.script_file, e))
raise VPCSError('Cannot write the startup script file "{}": {}'.format(startup_script_path, e))
@asyncio.coroutine
def _check_vpcs_version(self):
@@ -254,7 +254,10 @@ class VPCSVM(BaseVM):
except asyncio.TimeoutError:
if self._process.returncode is None:
log.warn("VPCS process {} is still running... killing it".format(self._process.pid))
self._process.kill()
try:
self._process.kill()
except OSError as e:
raise VPCSError("Can not stop the VPCS process: {}".format(e))
self._process = None
self._started = False
@@ -390,6 +393,9 @@ class VPCSVM(BaseVM):
command = [self.vpcs_path]
command.extend(["-p", str(self._console)]) # listen to console port
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
nio = self._ethernet_adapter.get_nio(0)
if nio:
@@ -404,10 +410,6 @@ class VPCSVM(BaseVM):
command.extend(["-e"])
command.extend(["-d", nio.tap_device])
command.extend(["-m", str(self._manager.get_mac_id(self.id))]) # the unique ID is used to set the MAC address offset
command.extend(["-i", "1"]) # option to start only one VPC instance
command.extend(["-F"]) # option to avoid the daemonization of VPCS
if self.script_file:
command.extend([os.path.basename(self.script_file)])
return command
+11
View File
@@ -218,6 +218,16 @@ DEVICE_NIO_SCHEMA = {
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"NAT": {
"description": "NAT Network Input/Output",
"properties": {
"type": {
"enum": ["nio_nat"]
},
},
"required": ["type"],
"additionalProperties": False
},
"TAP": {
"description": "TAP Network Input/Output",
"properties": {
@@ -291,6 +301,7 @@ DEVICE_NIO_SCHEMA = {
{"$ref": "#/definitions/UDP"},
{"$ref": "#/definitions/Ethernet"},
{"$ref": "#/definitions/LinuxEthernet"},
{"$ref": "#/definitions/NAT"},
{"$ref": "#/definitions/TAP"},
{"$ref": "#/definitions/UNIX"},
{"$ref": "#/definitions/VDE"},
+12 -1
View File
@@ -120,6 +120,10 @@ VM_CREATE_SCHEMA = {
"description": "disk1 size in MB",
"type": "integer"
},
"auto_delete_disks": {
"description": "automatically delete nvram and disk files",
"type": "boolean"
},
"console": {
"description": "console TCP port",
"type": "integer",
@@ -337,6 +341,10 @@ VM_UPDATE_SCHEMA = {
"description": "disk1 size in MB",
"type": "integer"
},
"auto_delete_disks": {
"description": "automatically delete nvram and disk files",
"type": "boolean"
},
"console": {
"description": "console TCP port",
"type": "integer",
@@ -591,6 +599,10 @@ VM_OBJECT_SCHEMA = {
"description": "disk1 size in MB",
"type": "integer"
},
"auto_delete_disks": {
"description": "automatically delete nvram and disk files",
"type": "boolean"
},
"console": {
"description": "console TCP port",
"type": "integer",
@@ -745,5 +757,4 @@ VM_CONFIGS_SCHEMA = {
},
},
"additionalProperties": False,
"required": ["startup_config_content", "private_config_content"]
}
+11
View File
@@ -78,6 +78,16 @@ NIO_SCHEMA = {
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"NAT": {
"description": "NAT Network Input/Output",
"properties": {
"type": {
"enum": ["nio_nat"]
},
},
"required": ["type"],
"additionalProperties": False
},
"TAP": {
"description": "TAP Network Input/Output",
"properties": {
@@ -148,6 +158,7 @@ NIO_SCHEMA = {
{"$ref": "#/definitions/UDP"},
{"$ref": "#/definitions/Ethernet"},
{"$ref": "#/definitions/LinuxEthernet"},
{"$ref": "#/definitions/NAT"},
{"$ref": "#/definitions/TAP"},
{"$ref": "#/definitions/UNIX"},
{"$ref": "#/definitions/VDE"},
+28 -6
View File
@@ -27,6 +27,7 @@ import aiohttp
import functools
import types
import time
import atexit
from .web.route import Route
from .web.request_handler import RequestHandler
@@ -85,6 +86,7 @@ class Server:
if self._handler:
yield from self._handler.finish_connections()
self._handler = None
for module in MODULES:
log.debug("Unloading module {}".format(module.__name__))
@@ -173,6 +175,22 @@ class Server:
return
yield from embed(globals(), locals(), return_asyncio_coroutine=True, patch_stdout=True)
def _exit_handling(self):
"""
Makes sure the asyncio loop is closed.
"""
def close_asyncio_loop():
loop = None
try:
loop = asyncio.get_event_loop()
except AttributeError:
pass
if loop is not None:
loop.close()
atexit.register(close_asyncio_loop)
def run(self):
"""
Starts the server.
@@ -213,9 +231,12 @@ class Server:
log.info("Starting server on {}:{}".format(self._host, self._port))
self._handler = app.make_handler(handler=RequestHandler)
self._loop.run_until_complete(self._run_application(self._handler, ssl_context))
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"):
log.info("Code live reload is enabled, watching for file changes")
self._loop.call_later(1, self._reload_hook)
@@ -225,13 +246,14 @@ class Server:
try:
self._loop.run_forever()
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 TypeError as e:
# This is to ignore an asyncio.windows_events exception
# on Windows when the process gets the SIGBREAK signal
# TypeError: async() takes 1 positional argument but 3 were given
log.warning("TypeError exception in the loop {}".format(e))
finally:
if self._handler and self._loop.is_running():
self._loop.run_until_complete(self._handler.finish_connections())
server.close()
if self._loop.is_running():
self._loop.run_until_complete(app.finish())
+1 -1
View File
@@ -6,6 +6,6 @@
<ul>
<li><a href="http://community.gns3.com">Community</a></li>
<li><a href="http://api.gns3.net">API documentation</a></li>
<li><a href="/upload">Upload images</a></li>
<li><a href="/upload">Upload images & backup</a></li>
</ul>
{% endblock %}
+9
View File
@@ -4,6 +4,15 @@
<title>GNS3 Server</title>
</head>
<body>
<div>
<a href="/">Home</a>
|
<a href="/upload">Upload</a>
|
<a href="/backup/images.tar">Backup images</a>
|
<a href="/backup/projects.tar">Backup projects</a>
</div>
{% block body %}{% endblock %}
</body>
<small>
+2
View File
@@ -8,6 +8,8 @@
<option value="IOURC">IOU licence (iourc)</option>
<option value="IOS">IOS</option>
<option value="QEMU">Qemu</option>
<option value="IMAGES">GNS3 images backup (.tar)</option>
<option value="PROJECTS">GNS3 projects backup (.tar)</option>
</select>
<br />
<br />
+4 -1
View File
@@ -51,7 +51,10 @@ def subprocess_check_output(*args, cwd=None, env=None):
output = yield from proc.stdout.read()
if output is None:
return ""
return output.decode("utf-8")
# If we received garbage we ignore invalid characters
# it should happend only when user try to use another binary
# and the code of VPCS, dynamips... Will detect it's not the correct binary
return output.decode("utf-8", errors="ignore")
@asyncio.coroutine
+41
View File
@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import tempfile
import pkg_resources
import atexit
import logging
log = logging.getLogger(__name__)
try:
egg_cache_dir = tempfile.mkdtemp()
pkg_resources.set_extraction_path(egg_cache_dir)
except ValueError:
# If the path is already set the module throw an error
pass
@atexit.register
def clean_egg_cache():
try:
import shutil
log.debug("Clean egg cache %s", egg_cache_dir)
shutil.rmtree(egg_cache_dir)
except Exception:
# We don't care if we can not cleanup
pass
+2 -2
View File
@@ -23,5 +23,5 @@
# or negative for a release candidate or beta (after the base version
# number has been incremented)
__version__ = "1.3.2"
__version_info__ = (1, 3, 2, 0)
__version__ = "1.3.8"
__version_info__ = (1, 3, 8, 0)
+23 -2
View File
@@ -34,7 +34,7 @@ class ColouredFormatter(logging.Formatter):
message = super().format(record)
if not colour:
if not colour or sys.platform.startswith("win"):
return message.replace("#RESET#", "")
level_no = record.levelno
@@ -74,6 +74,27 @@ class ColouredStreamHandler(logging.StreamHandler):
stream.write(msg)
stream.write(self.terminator)
self.flush()
# On OSX when frozen flush raise a BrokenPipeError
except BrokenPipeError:
pass
except Exception:
self.handleError(record)
class WinStreamHandler(logging.StreamHandler):
def emit(self, record):
if sys.stdin.encoding != "utf-8":
record = record
stream = self.stream
try:
msg = self.formatter.format(record, stream.isatty())
stream.write(msg.encode(stream.encoding, errors="replace").decode(stream.encoding))
stream.write(self.terminator)
self.flush()
pass
except Exception:
self.handleError(record)
@@ -83,7 +104,7 @@ def init_logger(level, logfile=None, quiet=False):
stream_handler = logging.FileHandler(logfile)
stream_handler.formatter = ColouredFormatter("{asctime} {levelname} {filename}:{lineno} {message}", "%Y-%m-%d %H:%M:%S", "{")
elif sys.platform.startswith("win"):
stream_handler = logging.StreamHandler(sys.stdout)
stream_handler = WinStreamHandler(sys.stdout)
stream_handler.formatter = ColouredFormatter("{asctime} {levelname} {filename}:{lineno} {message}", "%Y-%m-%d %H:%M:%S", "{")
else:
stream_handler = ColouredStreamHandler(sys.stdout)
+37 -1
View File
@@ -83,6 +83,31 @@ class Route(object):
def delete(cls, path, *args, **kw):
return cls._route('DELETE', path, *args, **kw)
@classmethod
def authenticate(cls, request, route, server_config):
"""
Ask user for authentication
:returns: Response if you need to auth the user otherwise None
"""
if not server_config.getboolean("auth", False):
return
user = server_config.get("user", "").strip()
password = server_config.get("password", "").strip()
if len(user) == 0:
return
if "AUTHORIZATION" in request.headers:
if request.headers["AUTHORIZATION"] == aiohttp.helpers.BasicAuth(user, password).encode():
return
response = Response(request=request, route=route)
response.set_status(401)
response.headers["WWW-Authenticate"] = 'Basic realm="GNS3 server"'
return response
@classmethod
def _route(cls, method, path, *args, **kw):
# This block is executed only the first time
@@ -118,6 +143,13 @@ class Route(object):
def control_schema(request):
# This block is executed at each method call
server_config = Config.instance().get_section_config("Server")
# Authenticate
response = cls.authenticate(request, route, server_config)
if response:
return response
# Non API call
if api_version is None:
response = Response(request=request, route=route, output_schema=output_schema)
@@ -127,7 +159,6 @@ class Route(object):
# API call
try:
request = yield from parse_request(request, input_schema)
server_config = Config.instance().get_section_config("Server")
record_file = server_config.get("record")
if record_file:
try:
@@ -161,6 +192,11 @@ class Route(object):
response = Response(request=request, route=route)
response.set_status(408)
response.json({"message": "Client disconnected", "status": 408})
except ConnectionResetError:
log.error("Client connection reset")
response = Response(request=request, route=route)
response.set_status(408)
response.json({"message": "Connection reset", "status": 408})
except Exception as e:
log.error("Uncaught exception detected: {type}".format(type=type(e)), exc_info=1)
response = Response(request=request, route=route)
+1 -1
View File
@@ -6,7 +6,7 @@ stop on shutdown
script
echo $$ > /var/run/gns3.pid
exec start-stop-daemon --start -c gns3 --exec /usr/local/bin/gns3server --log /var/log/gns3.log
exec start-stop-daemon --start -c gns3 --exec /usr/local/bin/gns3server -- --log /var/log/gns3.log
end script
pre-start script
+1 -2
View File
@@ -1,6 +1,5 @@
netifaces==0.10.4
gns3-netifaces==0.10.4.1
jsonschema==2.4.0
python-dateutil==2.3
aiohttp==0.14.4
Jinja2==2.7.3
raven==5.2.0
+11 -5
View File
@@ -19,6 +19,10 @@ import sys
from setuptools import setup, find_packages
from setuptools.command.test import test as TestCommand
# we only support Python 3 version >= 3.3
if sys.version_info < (3, 3):
raise SystemExit("Python 3.3 or higher is required")
class PyTest(TestCommand):
@@ -34,12 +38,14 @@ class PyTest(TestCommand):
sys.exit(errcode)
dependencies = ["aiohttp>=0.14.4",
"jsonschema>=2.4.0",
"Jinja2>=2.7.3",
"raven>=5.2.0"]
dependencies = [
# "gns3-netifaces>=0.10.4.1",
"aiohttp>=0.14.4",
"jsonschema>=2.4.0",
"Jinja2>=2.7.3",
"raven>=5.2.0"]
#if not sys.platform.startswith("win"):
# if not sys.platform.startswith("win"):
# dependencies.append("netifaces==0.10.4")
if sys.version_info == (3, 3):
+1
View File
@@ -136,6 +136,7 @@ def run_around_tests(monkeypatch):
config = Config.instance()
config.clear()
config.set("Server", "project_directory", tmppath)
config.set("Server", "auth", False)
# Prevent exectuions of the VM if we forgot to mock something
config.set("VirtualBox", "vboxmanage_path", tmppath)
+4 -1
View File
@@ -88,7 +88,10 @@ class Query:
except ValueError:
response.json = None
else:
response.html = response.body.decode("utf-8")
try:
response.html = response.body.decode("utf-8")
except UnicodeDecodeError:
response.html = None
else:
response.json = {}
response.html = ""
+5
View File
@@ -15,6 +15,9 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import pytest
def test_udp_allocation(server, project):
response = server.post('/projects/{}/ports/udp'.format(project.id), {}, example=True)
@@ -22,6 +25,8 @@ def test_udp_allocation(server, project):
assert response.json == {'udp_port': 10000}
# Netfifaces is not available on Travis
@pytest.mark.skipif(os.environ.get("TRAVIS", False) is not False, reason="Not supported on Travis")
def test_interfaces(server):
response = server.get('/interfaces', example=True)
assert response.status == 200
+30 -4
View File
@@ -20,6 +20,7 @@ This test suite check /project endpoint
"""
import uuid
import os
from unittest.mock import patch
from tests.utils import asyncio_patch
@@ -85,18 +86,43 @@ def test_update_temporary_project(server):
assert response.json["temporary"] is False
def test_update_path_project(server, tmpdir):
def test_update_path_project_temporary(server, tmpdir):
os.makedirs(str(tmpdir / "a"))
os.makedirs(str(tmpdir / "b"))
with patch("gns3server.modules.project.Project.is_local", return_value=True):
response = server.post("/projects", {"name": "first_name"})
response = server.post("/projects", {"name": "first_name", "path": str(tmpdir / "a"), "temporary": True})
assert response.status == 201
assert response.json["name"] == "first_name"
query = {"name": "second_name", "path": str(tmpdir)}
query = {"name": "second_name", "path": str(tmpdir / "b")}
response = server.put("/projects/{project_id}".format(project_id=response.json["project_id"]), query, example=True)
assert response.status == 200
assert response.json["path"] == str(tmpdir)
assert response.json["path"] == str(tmpdir / "b")
assert response.json["name"] == "second_name"
assert not os.path.exists(str(tmpdir / "a"))
assert os.path.exists(str(tmpdir / "b"))
def test_update_path_project_non_temporary(server, tmpdir):
os.makedirs(str(tmpdir / "a"))
os.makedirs(str(tmpdir / "b"))
with patch("gns3server.modules.project.Project.is_local", return_value=True):
response = server.post("/projects", {"name": "first_name", "path": str(tmpdir / "a")})
assert response.status == 201
assert response.json["name"] == "first_name"
query = {"name": "second_name", "path": str(tmpdir / "b")}
response = server.put("/projects/{project_id}".format(project_id=response.json["project_id"]), query, example=True)
assert response.status == 200
assert response.json["path"] == str(tmpdir / "b")
assert response.json["name"] == "second_name"
assert os.path.exists(str(tmpdir / "a"))
assert os.path.exists(str(tmpdir / "b"))
def test_update_path_project_non_local(server, tmpdir):
+2 -2
View File
@@ -42,7 +42,7 @@ def test_vpcs_get(server, project, vm):
assert response.route == "/projects/{project_id}/vpcs/vms/{vm_id}"
assert response.json["name"] == "PC TEST 1"
assert response.json["project_id"] == project.id
assert response.json["startup_script_path"] == None
assert response.json["startup_script_path"] is None
def test_vpcs_create_startup_script(server, project):
@@ -51,7 +51,7 @@ def test_vpcs_create_startup_script(server, project):
assert response.route == "/projects/{project_id}/vpcs/vms"
assert response.json["name"] == "PC TEST 1"
assert response.json["project_id"] == project.id
assert response.json["startup_script"] == "ip 192.168.1.2\necho TEST"
assert response.json["startup_script"] == os.linesep.join(["ip 192.168.1.2", "echo TEST"])
assert response.json["startup_script_path"] == "startup.vpc"
+138 -2
View File
@@ -17,10 +17,15 @@
import aiohttp
import asyncio
import os
import tarfile
from unittest.mock import patch
from gns3server.config import Config
def test_index_upload(server):
response = server.get('/upload', api_version=None)
assert response.status == 200
@@ -37,10 +42,141 @@ def test_upload(server, tmpdir):
body.add_field("type", "QEMU")
body.add_field("file", open(str(tmpdir / "test"), "rb"), content_type="application/iou", filename="test2")
with patch("gns3server.config.Config.get_section_config", return_value={"images_path": str(tmpdir)}):
response = server.post('/upload', api_version=None, body=body, raw=True)
Config.instance().set("Server", "images_path", str(tmpdir))
response = server.post('/upload', api_version=None, body=body, raw=True)
with open(str(tmpdir / "QEMU" / "test2")) as f:
assert f.read() == "TEST"
assert "test2" in response.body.decode("utf-8")
def test_upload_images_backup(server, tmpdir):
Config.instance().set("Server", "images_path", str(tmpdir / 'images'))
os.makedirs(str(tmpdir / 'images' / 'IOU'))
# An old IOU image that we need to replace
with open(str(tmpdir / 'images' / 'IOU' / 'b.img'), 'w+') as f:
f.write('bad')
os.makedirs(str(tmpdir / 'old' / 'QEMU'))
with open(str(tmpdir / 'old' / 'QEMU' / 'a.img'), 'w+') as f:
f.write('hello')
os.makedirs(str(tmpdir / 'old' / 'IOU'))
with open(str(tmpdir / 'old' / 'IOU' / 'b.img'), 'w+') as f:
f.write('world')
os.chdir(str(tmpdir / 'old'))
with tarfile.open(str(tmpdir / 'test.tar'), 'w') as tar:
tar.add('.', recursive=True)
body = aiohttp.FormData()
body.add_field('type', 'IMAGES')
body.add_field('file', open(str(tmpdir / 'test.tar'), 'rb'), content_type='application/x-gtar', filename='test.tar')
response = server.post('/upload', api_version=None, body=body, raw=True)
assert response.status == 200
with open(str(tmpdir / 'images' / 'QEMU' / 'a.img')) as f:
assert f.read() == 'hello'
with open(str(tmpdir / 'images' / 'IOU' / 'b.img')) as f:
assert f.read() == 'world'
assert 'a.img' in response.body.decode('utf-8')
assert 'b.img' in response.body.decode('utf-8')
assert not os.path.exists(str(tmpdir / 'images' / 'archive.tar'))
def test_upload_projects_backup(server, tmpdir):
Config.instance().set("Server", "projects_path", str(tmpdir / 'projects'))
os.makedirs(str(tmpdir / 'projects' / 'b'))
# An old b image that we need to replace
with open(str(tmpdir / 'projects' / 'b' / 'b.img'), 'w+') as f:
f.write('bad')
os.makedirs(str(tmpdir / 'old' / 'a'))
with open(str(tmpdir / 'old' / 'a' / 'a.img'), 'w+') as f:
f.write('hello')
os.makedirs(str(tmpdir / 'old' / 'b'))
with open(str(tmpdir / 'old' / 'b' / 'b.img'), 'w+') as f:
f.write('world')
os.chdir(str(tmpdir / 'old'))
with tarfile.open(str(tmpdir / 'test.tar'), 'w') as tar:
tar.add('.', recursive=True)
body = aiohttp.FormData()
body.add_field('type', 'PROJECTS')
body.add_field('file', open(str(tmpdir / 'test.tar'), 'rb'), content_type='application/x-gtar', filename='test.tar')
response = server.post('/upload', api_version=None, body=body, raw=True)
assert response.status == 200
with open(str(tmpdir / 'projects' / 'a' / 'a.img')) as f:
assert f.read() == 'hello'
with open(str(tmpdir / 'projects' / 'b' / 'b.img')) as f:
assert f.read() == 'world'
assert 'a.img' not in response.body.decode('utf-8')
assert 'b.img' not in response.body.decode('utf-8')
assert not os.path.exists(str(tmpdir / 'projects' / 'archive.tar'))
def test_backup_images(server, tmpdir, loop):
Config.instance().set('Server', 'images_path', str(tmpdir))
os.makedirs(str(tmpdir / 'QEMU'))
with open(str(tmpdir / 'QEMU' / 'a.img'), 'w+') as f:
f.write('hello')
with open(str(tmpdir / 'QEMU' / 'b.img'), 'w+') as f:
f.write('world')
response = server.get('/backup/images.tar', api_version=None, raw=True)
assert response.status == 200
assert response.headers['CONTENT-TYPE'] == 'application/x-gtar'
with open(str(tmpdir / 'images.tar'), 'wb+') as f:
print(len(response.body))
f.write(response.body)
tar = tarfile.open(str(tmpdir / 'images.tar'), 'r')
os.makedirs(str(tmpdir / 'extract'))
os.chdir(str(tmpdir / 'extract'))
# Extract to current working directory
tar.extractall()
tar.close()
assert os.path.exists(os.path.join('QEMU', 'a.img'))
open(os.path.join('QEMU', 'a.img')).read() == 'hello'
assert os.path.exists(os.path.join('QEMU', 'b.img'))
open(os.path.join('QEMU', 'b.img')).read() == 'world'
def test_backup_projects(server, tmpdir, loop):
Config.instance().set('Server', 'projects_path', str(tmpdir))
os.makedirs(str(tmpdir / 'a'))
with open(str(tmpdir / 'a' / 'a.gns3'), 'w+') as f:
f.write('hello')
os.makedirs(str(tmpdir / 'b'))
with open(str(tmpdir / 'b' / 'b.gns3'), 'w+') as f:
f.write('world')
response = server.get('/backup/projects.tar', api_version=None, raw=True)
assert response.status == 200
assert response.headers['CONTENT-TYPE'] == 'application/x-gtar'
with open(str(tmpdir / 'projects.tar'), 'wb+') as f:
print(len(response.body))
f.write(response.body)
tar = tarfile.open(str(tmpdir / 'projects.tar'), 'r')
os.makedirs(str(tmpdir / 'extract'))
os.chdir(str(tmpdir / 'extract'))
# Extract to current working directory
tar.extractall()
tar.close()
assert os.path.exists(os.path.join('a', 'a.gns3'))
open(os.path.join('a', 'a.gns3')).read() == 'hello'
assert os.path.exists(os.path.join('b', 'b.gns3'))
open(os.path.join('b', 'b.gns3')).read() == 'world'
+20 -18
View File
@@ -260,24 +260,24 @@ def test_control_vm_expect_text(vm, loop, running_subprocess_mock):
def test_build_command(vm, loop, fake_qemu_binary, port_manager):
os.environ["DISPLAY"] = "0:0"
with patch("gns3server.modules.qemu.qemu_vm.QemuVM._get_random_mac", return_value="00:00:ab:7e:b5:00"):
with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()) as process:
cmd = loop.run_until_complete(asyncio.async(vm._build_command()))
assert cmd == [
fake_qemu_binary,
"-name",
"test",
"-m",
"256",
"-hda",
os.path.join(vm.working_dir, "flash.qcow2"),
"-serial",
"telnet:127.0.0.1:{},server,nowait".format(vm.console),
"-device",
"e1000,mac=00:00:ab:7e:b5:00,netdev=gns3-0",
"-netdev",
"user,id=gns3-0"
]
with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()) as process:
cmd = loop.run_until_complete(asyncio.async(vm._build_command()))
assert cmd == [
fake_qemu_binary,
"-name",
"test",
"-m",
"256",
"-hda",
os.path.join(vm.working_dir, "flash.qcow2"),
"-serial",
"telnet:127.0.0.1:{},server,nowait".format(vm.console),
"-net",
"none",
"-device",
"e1000,mac=00:00:ab:0e:0f:00"
]
@pytest.mark.skipif(sys.platform.startswith("win"), reason="Not supported on Windows")
def test_build_command_without_display(vm, loop, fake_qemu_binary):
@@ -314,6 +314,7 @@ def test_hdb_disk_image(vm, tmpdir):
vm.hdb_disk_image = "test"
assert vm.hdb_disk_image == str(tmpdir / "QEMU" / "test")
def test_hdc_disk_image(vm, tmpdir):
with patch("gns3server.config.Config.get_section_config", return_value={"images_path": str(tmpdir)}):
@@ -322,6 +323,7 @@ def test_hdc_disk_image(vm, tmpdir):
vm.hdc_disk_image = "test"
assert vm.hdc_disk_image == str(tmpdir / "QEMU" / "test")
def test_hdd_disk_image(vm, tmpdir):
with patch("gns3server.config.Config.get_section_config", return_value={"images_path": str(tmpdir)}):
+7
View File
@@ -103,10 +103,16 @@ def test_get_abs_image_path(qemu, tmpdir):
with patch("gns3server.config.Config.get_section_config", return_value={"images_path": str(tmpdir)}):
assert qemu.get_abs_image_path(path1) == path1
assert qemu.get_abs_image_path("test1.bin") == path1
assert qemu.get_abs_image_path(path2) == path2
assert qemu.get_abs_image_path("test2.bin") == path2
assert qemu.get_abs_image_path("../test1.bin") == path1
# We look at first in new location
path2 = str(tmpdir / "QEMU" / "test1.bin")
open(path2, 'w+').close()
assert qemu.get_abs_image_path("test1.bin") == path2
def test_get_relative_image_path(qemu, tmpdir):
os.makedirs(str(tmpdir / "QEMU"))
@@ -118,6 +124,7 @@ def test_get_relative_image_path(qemu, tmpdir):
with patch("gns3server.config.Config.get_section_config", return_value={"images_path": str(tmpdir)}):
assert qemu.get_relative_image_path(path1) == path1
assert qemu.get_relative_image_path("test1.bin") == path1
assert qemu.get_relative_image_path(path2) == "test2.bin"
assert qemu.get_relative_image_path("test2.bin") == "test2.bin"
assert qemu.get_relative_image_path("../test1.bin") == path1
+10
View File
@@ -45,3 +45,13 @@ def test_release_udp_port():
pm.reserve_udp_port(4242, project)
pm.release_udp_port(4242, project)
pm.reserve_udp_port(4242, project)
def test_find_unused_port():
p = PortManager().find_unused_port(1000, 10000)
assert p is not None
def test_find_unused_port_invalid_range():
with pytest.raises(aiohttp.web.HTTPConflict):
p = PortManager().find_unused_port(10000, 1000)
+1 -6
View File
@@ -69,15 +69,10 @@ def test_changing_path_temporary_flag(tmpdir):
with patch("gns3server.modules.project.Project.is_local", return_value=True):
p = Project(temporary=True)
assert os.path.exists(p.path)
original_path = p.path
assert os.path.exists(os.path.join(p.path, ".gns3_temporary"))
p.temporary = False
assert not os.path.exists(os.path.join(p.path, ".gns3_temporary"))
with open(str(tmpdir / ".gns3_temporary"), "w+") as f:
f.write("1")
p.path = str(tmpdir)
assert not os.path.exists(os.path.join(str(tmpdir), ".gns3_temporary"))
def test_temporary_path():
@@ -20,10 +20,14 @@ import pytest
import tempfile
import os
import stat
import asyncio
from unittest.mock import patch
from gns3server.modules.virtualbox import VirtualBox
from gns3server.modules.virtualbox.virtualbox_error import VirtualBoxError
from unittest.mock import patch
from tests.utils import asyncio_patch
@pytest.fixture(scope="module")
@@ -65,3 +69,31 @@ def test_vboxmanage_path(manager, tmpdir):
tmpfile = tempfile.NamedTemporaryFile()
with patch("gns3server.config.Config.get_section_config", return_value={"vboxmanage_path": path}):
assert manager.find_vboxmanage() == path
def test_get_list(manager, loop):
vm_list = ['"Windows 8.1" {27b4d095-ff5f-4ac4-bb9d-5f2c7861c1f1}',
'"Carriage',
'Return" {27b4d095-ff5f-4ac4-bb9d-5f2c7861c1f1}',
'',
'"<inaccessible>" {42b4d095-ff5f-4ac4-bb9d-5f2c7861c1f1}',
'"Linux Microcore 4.7.1" {ccd8c50b-c172-457d-99fa-dd69371ede0e}']
@asyncio.coroutine
def execute_mock(cmd, args):
if cmd == "list":
return vm_list
else:
if args[0] == "Windows 8.1":
return ["memory=512"]
elif args[0] == "Linux Microcore 4.7.1":
return ["memory=256"]
assert False, "Unknow {} {}".format(cmd, args)
with asyncio_patch("gns3server.modules.virtualbox.VirtualBox.execute") as mock:
mock.side_effect = execute_mock
vms = loop.run_until_complete(asyncio.async(manager.get_list()))
assert vms == [
{"vmname": "Windows 8.1", "ram": 512},
{"vmname": "Linux Microcore 4.7.1", "ram": 256}
]
@@ -54,3 +54,9 @@ def test_vm_invalid_virtualbox_api_version(loop, project, manager):
with pytest.raises(VirtualBoxError):
vm = VirtualBoxVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager, "test", False)
loop.run_until_complete(asyncio.async(vm.create()))
def test_vm_adapter_add_nio_binding_adapter_not_exist(loop, vm, manager, free_console_port):
nio = manager.create_nio(manager.vboxmanage_path, {"type": "nio_udp", "lport": free_console_port, "rport": free_console_port, "rhost": "192.168.1.2"})
with pytest.raises(VirtualBoxError):
loop.run_until_complete(asyncio.async(vm.adapter_add_nio_binding(15, nio)))
+2 -2
View File
@@ -190,9 +190,9 @@ def test_update_startup_script_h(vm):
def test_get_startup_script(vm):
content = "echo GNS3 VPCS\nip 192.168.1.2\n"
content = "echo GNS3 VPCS\nip 192.168.1.2"
vm.startup_script = content
assert vm.startup_script == content
assert vm.startup_script == os.linesep.join(["echo GNS3 VPCS","ip 192.168.1.2"])
def test_get_startup_script_using_default_script(vm):
+24
View File
@@ -0,0 +1,24 @@
# -*- 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 <http://www.gnu.org/licenses/>.
from gns3server.utils.interfaces import interfaces
def test_interfaces():
# This test should pass on all platforms without crash
assert isinstance(interfaces(), list)