Compare commits

..
340 Commits
Author SHA1 Message Date
Jeremy 701a1626f7 Bump version to 1.2.2 2015-01-15 17:44:09 -07:00
Jeremy e6e83ffa8a Update the VM name in VirtualBox for linked clones. 2015-01-14 16:48:32 -07:00
Jeremy 2bfe098866 Bump version to 1.2.2.dev2 2015-01-12 16:30:09 -07:00
Jeremy 2ef45781ad Checks connection failure to Qemu monitor. 2015-01-11 18:24:13 -07:00
Jeremy Grossmann 14cc6d414f Merge pull request #63 from Raizo62/master
Add snapshot named reset to linked cloned Vbox
2015-01-11 17:16:16 -07:00
Raizo62 cbbb89f69c Add snapshot named reset to linked cloned Vbox 2015-01-11 12:22:59 +00:00
Jeremy 29896151e7 Support spaces in the controller name of VirtualBox clones. 2015-01-06 15:31:50 -07:00
Jeremy 7f6f7022b0 Support to run VirtualBox as another user. 2015-01-05 16:18:33 -07:00
Jeremy Grossmann c77ebca036 Merge pull request #62 from shmygov/qemunio
Dynamically configure network connections of running QEMU VMs
2015-01-05 15:46:09 -07:00
Jeremy 881cc6f592 Fixes wrong IOS config paths. 2015-01-04 18:51:55 -07:00
Jeremy 5265818365 Support for IOURC file on the server side. 2015-01-04 15:59:00 -07:00
Jeremy d1e186317c Console switching from local/remote to remote/local while a VirtualBox VM is running. 2015-01-04 14:56:17 -07:00
Jeremy a6fd457cff Bump the maximum network adapters to 32 for Qemu. 2015-01-03 16:16:07 -07:00
Dmitry Shmygov 4eedc9c74c Dynamically configure network connections of running QEMU VMs (including new QEMU versions) 2015-01-03 05:04:52 +03:00
Dmitry Shmygov ccb4ccd612 Return QEMU monitor responses as Match Objects (more flexibility for future uses) 2014-12-29 01:59:45 +03:00
Dmitry Shmygov c775b05d33 Dynamically configure network connections of running QEMU VMs 2014-12-29 01:43:56 +03:00
Dmitry Shmygov 9cedb1cc99 Add sleep after write to QEMU monitor (to work with new QEMU versions) 2014-12-27 12:44:10 +03:00
Jeremy 1f615430ae Checks for broken symbolic links. 2014-12-24 17:19:42 -07:00
Jeremy 2de1a97076 Merge remote-tracking branch 'origin/master' 2014-12-24 15:47:07 -07:00
Jeremy 747c0ee7a3 More checks on minimum RAM for IOS routers and updates default values to match the latest IOS image requirements. 2014-12-24 15:46:59 -07:00
grossmj d9f44edcaf Fixes incompatibility for IOS startup-config and private-config paths created on Windows and loaded from a project on Linux/Mac OS X. 2014-12-23 15:29:27 -07:00
Jeremy Grossmann e9991affc3 Merge pull request #60 from shmygov/suspendqemu
QEMU VM suspend/resume and reload
2014-12-23 12:27:18 -07:00
Dmitry Shmygov 84511d7b39 QEMU VM suspend/resume and reload 2014-12-23 14:42:58 +03:00
Dmitry Shmygov 3590985c07 Add QEMU monitor port to control running QEMU VMs 2014-12-23 14:42:58 +03:00
Jeremy 7785c03eac Ignore Unicode errors when executing vboxmanage. 2014-12-19 15:47:12 -07:00
Jeremy 7f37f649da Catch OSError exception for subprocess calls. 2014-12-11 12:15:24 -07:00
Jeremy acb5103119 Merge remote-tracking branch 'origin/master' 2014-12-09 19:17:53 -07:00
Jeremy 823344bf44 Get Windows interface list from the registry if the COM service fails. 2014-12-09 19:17:25 -07:00
Jeremy Grossmann ecf7ebc26b Merge pull request #58 from dlintott/fix_gns3-gui_158
Remove extra line that was over-writing the auto Idle-PC value
2014-12-09 16:16:05 -07:00
Daniel Lintott 317a37147e Remove extra line that was over-writing the auto Idle-PC value
Fixes GNS3/gns3-gui#158
2014-12-09 19:39:22 +00:00
Jeremy Grossmann 0f238c5bb7 Merge pull request #54 from planctechnologies/dev
Qemu cloud improvements
2014-12-09 12:36:38 -07:00
Jeremy Grossmann 93934cf554 Merge pull request #55 from dlintott/fix_broken_test
Update test with correct hypervisor IP
2014-12-09 12:36:01 -07:00
Daniel Lintott 6a81390808 Update test with correct hypervisor IP 2014-12-09 13:49:45 +00:00
Jerry Seutter f1503ae655 Handle initrd and kernel paths in qemu 2014-12-08 20:14:55 -07:00
Jeremy 20592f72c0 Fixes UID bit test. 2014-12-08 14:54:06 -07:00
Jerry Seutter 00f49e337d Merge branch 'master' into dev 2014-12-08 14:22:31 -07:00
Jeremy fc04a94dce Bump version to 1.2.2.dev1 2014-12-05 13:53:30 -07:00
Jeremy ba357b0541 Bump version to 1.2.1 2014-12-04 12:49:40 -07:00
Jeremy f58c7960c9 Use bundled Qemu on Windows and OSX by default and checks if remote server are registered. 2014-12-04 12:25:49 -07:00
Jeremy 5a468888c8 Bump version to 1.2.1.dev2 2014-12-02 18:52:28 -07:00
Jeremy 8f53d51c05 Support for CPU throttling and process priority for Qemu. 2014-12-02 18:12:37 -07:00
Jeremy 1e01c85be9 Change search paths for Qemu on Windows. 2014-12-02 14:49:39 -07:00
grossmj fed02ee167 Adds default path for VBoxManage on Mac OS X. 2014-11-29 16:42:57 -07:00
grossmj 632134a02a Support for older Qemu versions like the 0.11.0 on Windows. 2014-11-29 14:11:51 -07:00
Jeremy 183a6aed44 Do not use universal_newlines in subprocess. 2014-11-26 15:07:15 -07:00
Jeremy d97ba11728 Fixes C7200 IO cards insert/remove issues and makes C7200-IO-FE the default. 2014-11-24 17:02:00 -07:00
Jerry Seutter 9ec1373d36 dms terminates instance if id matches 2014-11-24 16:49:01 -07:00
Jerry Seutter b220cffcf4 Updating dms to look for cloud_region instead of region 2014-11-24 16:40:55 -07:00
Jerry Seutter 7ef8dd794a Copy updated cloud/ module from gns3-gui 2014-11-24 16:24:57 -07:00
Jerry Seutter b9a4531544 Ensuring dead_time is an integer 2014-11-24 15:35:09 -07:00
Jerry Seutter cfdff773c4 deadtime -> dead_time 2014-11-24 15:08:58 -07:00
Jerry Seutter ef8c6b603c Add debugging to deadman process 2014-11-24 14:32:06 -07:00
Jeremy 4918675cd5 Fixes Qemu version detection. 2014-11-24 11:44:27 -07:00
Jeremy 6ef614103e Ignore inaccessible VirtualBox VMs. 2014-11-24 11:15:30 -07:00
Jeremy 09948a366f Use SubprocessError to catch Subprocess exceptions. 2014-11-22 17:45:04 -07:00
Jeremy 3bd88178a0 Bump to version 1.2.1.dev1 and fixes vboxmanage lookup on Windows. 2014-11-20 19:01:00 -07:00
Jeremy 95f5c73e33 Bump to version 1.2 2014-11-19 19:28:21 -07:00
grossmj fd92189d51 Restore dock widgets. 2014-11-19 10:22:09 -07:00
Jeremy cb913416ef Bump to version 1.2.dev3 2014-11-15 16:47:30 -07:00
Jeremy 5a7e482dac Linked clone support for VirtualBox (still problems with temporary projects). 2014-11-15 16:05:55 -07:00
Jeremy Grossmann 2509ee70e8 Merge pull request #51 from DimArmen/patch-1
Update setup.py
2014-11-15 10:21:49 -07:00
Dimitris Armenatzoglou a765bed3da Update setup.py
missing comma causes installation to fail...
2014-11-15 18:37:44 +02:00
Jeremy e2e4f4f38b Fixes remote server issue when creating a new project while already in a project. 2014-11-14 19:59:06 -07:00
Jeremy Grossmann e75dde3ebf Merge pull request #48 from planctechnologies/pr3
Add support for Qemu devices on cloud instances (server)
2014-11-12 21:19:15 -07:00
Jeremy Grossmann bba2c2b0d3 Merge pull request #47 from planctechnologies/pr2
Support IOU devices on cloud instances
2014-11-12 21:17:57 -07:00
Jeremy a9e924934a Fixes important issue when searching for a free port. 2014-11-12 19:49:02 -07:00
jseutter 387896fa69 Merge pull request #10 from planctechnologies/gns-129
Move image path manipulation to server side
2014-11-12 14:22:10 -07:00
jseutter 4d9d6ae5dd Merge pull request #9 from planctechnologies/gns-125
Add support for Qemu devices on cloud instances
2014-11-12 14:19:58 -07:00
Jeremy f6561bf684 Automatically extract IOS configs when a project is closed. 2014-11-10 13:50:17 -07:00
Jerry Seutter 5b73786653 Move image path manipulation to server side 2014-11-10 11:28:19 -07:00
Jeremy f44fbd1f16 Option to allow console connections to any local IP address when using the local server. 2014-11-09 23:01:13 -07:00
Jeremy 1982ff8100 Allows Qemu VM to have 0 interface. 2014-11-09 18:27:40 -07:00
Jeremy 7a6f27fed9 New VirtualBox guest property: ProjectDirInGNS3. 2014-11-09 16:10:30 -07:00
Jeremy 747ca7bb90 Base for VirtualBox linked clones (not completed yet). 2014-11-09 11:50:47 -07:00
Jerry Seutter faa3ef8cb4 Add support for Qemu devices on cloud instances 2014-11-07 20:42:08 -07:00
Nasrullah Taha f94a900b95 Merge pull request #8 from planctechnologies/gns-123
Support IOU devices on cloud instances
2014-11-06 16:18:27 -07:00
Jerry Seutter 0b0830976f Support IOU devices on cloud instances 2014-11-06 15:50:46 -07:00
grossmj 31db1a4e84 Merge remote-tracking branch 'origin/master'
Conflicts:
	gns3server/modules/virtualbox/virtualbox_vm.py
2014-11-06 13:59:05 -07:00
grossmj e07347a961 Rename "enable console" to "remote console". 2014-11-06 13:56:19 -07:00
Jeremy a4e20cd6f6 Add VirtualBox guest property "NameInGNS3". 2014-11-06 10:11:39 -07:00
Jeremy a98a8b1acc Change default VirtualBox adapter type. 2014-11-04 19:00:01 -07:00
Jeremy 7809160ea1 Add detection of qemu and qemu.exe binaries. 2014-11-03 17:36:14 -07:00
grossmj 410729c998 Check for duplicate node names in Preferences. 2014-11-03 15:06:07 -07:00
grossmj 3a85e2dba7 Fixes missing cloud settings on Windows. 2014-11-02 18:09:35 -07:00
grossmj 087f0e82de Fixes issues with VirtualBox Telnet server on Windows. 2014-11-02 18:06:15 -07:00
grossmj 393a312e7e New Telnet server for VirtualBox. 2014-11-02 15:47:44 -07:00
grossmj 4d23c5917c Add REUSE flag to socket when scanning for unused ports. 2014-11-01 15:44:18 -06:00
Jeremy Grossmann 89e80fd74b Merge pull request #43 from planctechnologies/dev
Download IOS images from Cloud Files to a cloud instance
2014-11-01 11:19:12 -06:00
Jeremy a48aff6ce5 Fixes some issues with VirtualBox support. 2014-10-31 17:41:12 -06:00
Jerry Seutter e5fa52fcb5 Adding back a line that was mistakenly removed. 2014-10-31 10:26:53 -06:00
Jerry Seutter ff02bb977a Merge branch 'master' into dev 2014-10-31 10:02:58 -06:00
grossmj 7b531cf094 Fixes issue when getting the VirtualBox VM list. 2014-10-30 21:10:14 -06:00
Jeremy dab72cf036 New VirtualBox support (under testing). 2014-10-30 18:53:17 -06:00
Nasrullah Taha bf0b6ee534 Merge pull request #7 from planctechnologies/gns-110
Support launching devices from cloud file images
2014-10-30 14:23:43 -06:00
grossmj 95a89ac91b Change find an unused port. 2014-10-29 10:15:22 -06:00
grossmj f5540ee147 Change find an unused port. 2014-10-28 21:03:51 -06:00
Jeremy Grossmann 8c47522a18 Merge pull request #39 from planctechnologies/dev
Improve logging, PEP8 cleanup
2014-10-28 16:39:58 -06:00
Jerry Seutter d2798a969e Cleanup 2014-10-28 11:27:41 -06:00
Jerry Seutter 148b99c553 Cleanup 2014-10-28 11:09:43 -06:00
Jerry Seutter 5f9554b86c Cleanup 2014-10-28 11:07:44 -06:00
Jerry Seutter 3a157b5e6d Handle a missing cloud server section in the config file 2014-10-28 11:01:17 -06:00
Jerry Seutter 7830bf8b1a Merge branch 'dev' into gns-110 2014-10-28 10:39:03 -06:00
Jerry Seutter ee1dbd6cd3 Merge branch 'master' into dev 2014-10-28 09:38:37 -06:00
Jerry Seutter c4afc33ea8 IOS devices can be deployed on cloud instances. 2014-10-27 18:12:56 -06:00
Jeremy f1f44078ba Update README. 2014-10-27 15:58:13 -06:00
grossmj 88b9d946da Fixes SecureCRT issue when disconnecting from an IOU device on Windows. 2014-10-25 18:03:24 -06:00
grossmj 20acca64b5 Bump version to 1.2.dev1 2014-10-25 18:01:14 -06:00
grossmj 440148aa0b Bump version to 1.1 2014-10-22 22:43:48 -06:00
grossmj f48c9117b0 Serial console for VirtualBox. 2014-10-22 21:59:11 -06:00
grossmj 666c8ea922 Pedantic: make sure Idle-PC is spelled that way. 2014-10-22 20:47:59 -06:00
Jerry Seutter 91894935bf Merge branch 'dev' into gns-110 2014-10-21 15:39:14 -06:00
grossmj 3b3c47c858 Bump version to 1.1.dev1. 2014-10-21 10:02:07 -06:00
grossmj f0c344939b Polish and bump up to version 1.0! 2014-10-20 18:40:05 -06:00
grossmj e261263aab Add path lookup directory for Qemu on OSX. 2014-10-20 11:56:30 -06:00
Jerry Seutter 6d80d3e70d Merge branch 'master' into dev 2014-10-20 11:22:04 -06:00
grossmj b88abb7c91 Remote servers and load-balancing (still things to improve). 2014-10-19 17:29:04 -06:00
Jerry Seutter c08e1011ed Make the server download images from cloud files 2014-10-15 15:51:00 -06:00
Jerry Seutter a833925497 Copied fresh cloud files from gns gui repo 2014-10-15 15:50:24 -06:00
grossmj 5f4b3c547b Bump to version 1.0.dev1. 2014-10-14 17:47:52 -06:00
grossmj f854752c84 Bump to version 1.0-beta4. 2014-10-14 17:20:28 -06:00
Jerry Seutter f287f5141a Update .gitignore file 2014-10-14 13:55:17 -06:00
grossmj 4195bdc7dd Auto idle-pc feature and improvements/bug fixes for GNS3 preferences. 2014-10-13 19:53:17 -06:00
Jerry Seutter c0fc093ab7 Merge branch 'master' into dev 2014-10-10 15:24:08 -06:00
grossmj b68c11e33e Bump version to 1.0-beta4.dev2 2014-10-09 21:26:07 -06:00
Jeremy Grossmann b3e86be182 Merge pull request #37 from planctechnologies/server_security2
Add secure communication between gui and server 2/2
2014-09-30 11:24:28 -06:00
Jeremy Grossmann 5802c2b9f5 Merge pull request #36 from planctechnologies/server_security
Add secure communication between gui and server 1/2
2014-09-30 11:23:04 -06:00
Jeremy Grossmann 83cef60c0f Merge pull request #35 from planctechnologies/deadman2
Deadman switch support 2/2
2014-09-30 11:20:39 -06:00
Jeremy Grossmann e39c93c91a Merge pull request #34 from planctechnologies/deadman
Deadman switch support 1/2
2014-09-30 11:20:01 -06:00
grossmj 1a96a150bc Fix shemas for QEMU. 2014-09-30 11:15:15 -06:00
Michael 65fdafda40 Merge pull request #6 from planctechnologies/gns-108
Add a --quiet mode to gns3server
2014-09-29 19:55:36 -06:00
Jerry Seutter c66fbbdb36 Merge improved ssl and auth support 2014-09-29 18:24:28 -06:00
Jerry Seutter 03fb75437b Add cert and auth support to gns3server. 2014-09-29 18:19:35 -06:00
Jerry Seutter 3833803244 Copy deadman switch code from old repository into gns3server. 2014-09-29 18:14:07 -06:00
Jerry Seutter 7c446796fe gns3server now controls the deadman switch. 2014-09-29 18:09:16 -06:00
Jerry Seutter ee88d6f808 Merge branch 'master' into dev 2014-09-29 17:24:52 -06:00
Jerry Seutter d4d774055a Remove unused parameter 2014-09-29 16:01:39 -06:00
Jerry Seutter efc80ff17a Revert version number change 2014-09-29 15:59:49 -06:00
Jerry Seutter 91fba4aca4 Use logging config to set destination of copyright info 2014-09-29 15:56:01 -06:00
grossmj 46495b9265 Merge remote-tracking branch 'origin/master' 2014-09-28 18:23:51 -06:00
grossmj a8193fa063 Split the PATH environment variable using os.pathsep 2014-09-28 18:23:27 -06:00
Jeremy Grossmann e3eecb6584 Merge pull request #33 from dlintott/fix_broken_test
Fix test for dynamips c7200 NPE (Default is now NPE-400)
2014-09-27 15:07:20 -06:00
Jeremy Grossmann 35f3434b2f Merge pull request #32 from noplay/osx_installation
Instruction for development on MacOS X
2014-09-27 13:56:28 -06:00
Daniel Lintott 20dc779fd8 Fix test for dynamips c7200 NPE (Default is now NPE-400) 2014-09-27 19:27:26 +01:00
Julien Duponchelle 04f670cb50 Instruction for development on MacOS X 2014-09-27 19:59:58 +02:00
Jerry Seutter 23686215fe Add a --quiet mode to gns3server 2014-09-25 14:42:37 -06:00
grossmj 6dce005594 Bump to version 1.0-beta1.dev1. 2014-09-24 11:14:28 -06:00
grossmj a49f107af2 Bump to version 1.0-beta3. 2014-09-24 11:01:33 -06:00
grossmj e7141685cc Tweaks to support Qemu on Windows. 2014-09-23 21:38:51 -06:00
grossmj aca9e0de56 Qemu integration stage 2, support for ASA and IDS. 2014-09-22 21:24:55 -06:00
Jerry Seutter 3b465890b6 Increase sleep to work around Rackspace slowness 2014-09-22 09:10:30 -06:00
Jerry Seutter cf59240bef Bugfixes with cloud server communication 2014-09-21 21:41:51 -06:00
grossmj d1715baae1 Base QEMU support. 2014-09-18 15:47:43 -06:00
Jerry Seutter b132c901c9 Disabling auth from version string 2014-09-18 20:39:12 +00:00
Michael a0e2fe551a Added web user and password to start_server output 2014-09-15 21:25:09 -06:00
jseutter 800d4d91f9 Merge pull request #5 from planctechnologies/gns3-87-bugfixes
Gns-87 bugfixes
2014-09-11 14:48:26 -06:00
Michael 6c6c9200e4 Add CN support to cert as command line arg 2014-09-08 22:07:33 -06:00
Michael 4fa87005bc Enabled HTTP Auth, SSL and DMS disabling based on cloud.conf availability 2014-09-08 21:51:56 -06:00
Michael 17e4b51d18 Testing out dummy config 2014-09-08 20:45:36 -06:00
Jerry Seutter 6421367259 Importing changeset from gns3dms repo 2014-09-08 15:35:22 +00:00
jseutter 6ff2c654d9 Merge pull request #4 from planctechnologies/gns3-87
Gns3 87 - add server security and startup scripts
2014-09-08 09:25:28 -06:00
Michael f876a862c4 GNS3 server will now create the heardbeat file durining initialization 2014-09-06 21:13:09 -06:00
Michael ef492d4690 Update gns3dms to support cloud.conf 2014-09-06 20:46:06 -06:00
Michael 36e539382c Added support for cloud.conf file and startup script 2014-09-06 00:51:43 -06:00
Michael 6f9e0f6d2e Moved certs to .config 2014-09-03 22:19:59 -06:00
Michael b84dda3c8e HTTP auth added to file_upload and jsonrpc 2014-09-03 22:12:34 -06:00
Jerry Seutter e2f3d2aca8 Pull from dev 2014-09-03 20:39:02 +00:00
Michael 382e693fc8 Added authentication handler for basic auth check 2014-09-03 00:05:06 -06:00
Michael a95cc678e9 Added server.py ssl mode dependant on cert existence 2014-09-02 22:33:45 -06:00
Michael bcf0aae531 Added HOME support and cert dir to create_cert script 2014-09-02 22:17:06 -06:00
grossmj b483f87c2f Bump version to 1.0-beta3.dev1. 2014-09-02 15:49:39 -06:00
grossmj 9d2e18328b Bump version to 1.0-beta2. 2014-09-02 13:06:26 -06:00
Michael 174013da80 Merge pull request #2 from planctechnologies/deadman
GNS-42 - Move deadman switch into gns3server codebase
2014-08-31 23:50:41 -06:00
Michael 99a8f5f21a Added create_cert.sh and ssl_options to enable SSL 2014-08-30 01:32:48 -06:00
Jerry Seutter 5e72fcbe14 GNS-42 - Move deadman switch into gns3server codebase 2014-08-29 18:05:56 +00:00
Michael e688d96c36 Add start, stop, restart and heartbeat handling to DeadMan module 2014-08-28 23:06:28 -06:00
Michael 3845cab84b Adding initial module 2014-08-28 22:09:38 -06:00
Michael 98e3a2e088 Updated requirements.txt 2014-08-28 22:01:23 -06:00
grossmj 76b357c1ce Do not activate sparse memory by default for c1700 and c2600 platforms.
https://github.com/GNS3/dynamips/issues/54
2014-08-26 17:07:48 -06:00
grossmj 80ab81190c Add "enable console" option to VirtualBox VMs (True by default).
Add "start at" option to VirtualBox VMs (adapter start index, 0 by default).
2014-08-26 15:27:43 -06:00
grossmj 934404cc90 Change default port ranges. 2014-08-25 15:40:04 -06:00
grossmj 6e39630b9b Required VirtualBox wrapper is >= 9.1 2014-08-22 17:39:57 -06:00
grossmj 569a68a486 VirtualBox support refactoring. 2014-08-22 17:36:12 -06:00
grossmj 77c583ca39 Check if the VirtualBox COM service is installed on Windows. 2014-08-21 18:13:41 -06:00
grossmj ea05744e1c Force to rebuild the COM cache on Windows (for VirtualBox support). 2014-08-17 15:15:07 -06:00
grossmj e0f0c98ffd Do not look for vboxwrapper on non Windows platforms. 2014-08-13 12:11:41 -06:00
grossmj a8d740ef21 Fix version from 1.0beta2-dev1 to 1.0beta2.dev1 2014-08-11 22:13:21 -06:00
Jeremy Grossmann 90c8c4312c Merge pull request #27 from dlintott/master
Override check_origin and fix test suite
2014-08-10 15:27:49 -06:00
Daniel Lintott e5642546f1 Remove commented line, not needed anymore 2014-08-09 12:26:24 +01:00
Daniel Lintott 4a33b2021c Further optimise the Travis testing and improve running tests for a user
+ Convert setup.py test to run py.test instead of tox
  + Tox should now run setup.py test
  + TravisCI will create a job for each TOX_ENV and then execute
    tox to run the tests for that TOX_ENV
2014-08-09 12:05:31 +01:00
Daniel Lintott a4bc96af28 revert not installing requirements outside of tox 2014-08-08 19:20:20 +01:00
Daniel Lintott d8f622d438 Streamline TravisCI build
+ As we use Tox there's no need to run seperate builds for
   python3.3 and python3.4
 + There's no need to install the requirements in main
   environment as all dependencies are handled in the Tox
   virtualenv's
2014-08-08 19:15:26 +01:00
Daniel Lintott ad287d3434 Remove un-needed imports 2014-08-08 19:14:36 +01:00
Daniel Lintott 4a4a57e1a3 Further test fixes
+ tests/dynamips/test_hypervisor.py: Increase sleep time to prevent
   random test failures
 + tests/iou/test_iou_device.py: Rework test skipping based on presence
   of IOU image rather than environment variable
2014-08-08 17:54:30 +01:00
Daniel Lintott 9b010d6388 Update test_hypervisor.py
+ test_stdout: use system dynamips
 + test_stdout: correct host address to start dynamips on
2014-08-08 15:00:44 +01:00
Daniel Lintott 8fc4667d2c Modify the TRAVIS environment check 2014-08-08 14:49:10 +01:00
Daniel Lintott 7cbce0f81b Fix test suite
+ Install VPCS and dynamips from GNS3 PPA
 + Drop netifaces-py3 from requirements.txt
 + Fix/update tests to use installed VPCS and dynamips
2014-08-08 14:32:32 +01:00
Daniel Lintott 578bb5741d Override check_origin from tornado.websocket 2014-08-06 22:43:37 +01:00
grossmj 0cfb446146 Bump version to beta1. 2014-07-18 22:50:22 -06:00
grossmj ae0feb8c9c Fixes packet capture when spaces are in the capture file path. 2014-07-18 22:43:01 -06:00
grossmj 7ff218a5fc Fixes issue when adding multiple VirtualBox VMs.
Remove early release dialog.
2014-07-18 21:10:46 -06:00
grossmj cdc778c159 Fixes issue with VirtualBox integration on Windows. 2014-07-18 18:04:21 -06:00
grossmj 19d24975c2 Remove privileged access checks for Dynamips. 2014-07-17 20:48:11 -06:00
grossmj 65d70bacfa Telnet to pipe support for VirtualBox. 2014-07-17 20:02:18 -06:00
grossmj 9ac2716826 Adds VirtualBox API for Python3 (not official). 2014-07-17 18:28:55 -06:00
grossmj 53ee3dc5cc Remove vboxapi_py3 package. 2014-07-17 18:24:21 -06:00
grossmj 64f78bb847 Fixes some inconsistencies when exporting configs. 2014-07-17 18:22:27 -06:00
grossmj f5cf7363da Work around for the c7200 reload bug in Dynamips <= 0.2.13. 2014-07-17 18:03:21 -06:00
grossmj 1fb4ab7e33 Minimal VirtualBox integration. 2014-07-17 15:28:02 -06:00
grossmj 0ef727ce4b Base for VirtualBox support. 2014-07-12 13:18:25 -06:00
grossmj 88e03ae312 Bump to version 1.0b1.dev1. 2014-07-11 16:44:42 -06:00
grossmj d6efd9ced0 Bump version to 1.0a8. 2014-07-09 17:08:22 -06:00
grossmj 6ffba35742 Use Windows interface names instead of their GUID (more user friendly).
Ask for an alternative interface if one cannot be found.
2014-07-08 14:01:45 -06:00
grossmj 9fc7650f3f Find an alternative IOS or IOU image if one cannot be found when importing a topology. 2014-07-06 16:34:38 -06:00
grossmj 551b98880b Default preferences for remote servers. 2014-07-04 15:18:13 -06:00
grossmj 7c99ee9de8 New feature: import/export device configs. 2014-07-03 18:56:37 -06:00
grossmj 9ac5b86431 Fixes IOU and VPCS renaming issues. 2014-07-03 09:02:25 -06:00
grossmj 329891fc83 Check for VPCS version >= 0.5b1 2014-07-02 15:04:39 -06:00
grossmj 15bc2221b9 Bump to version 1.0a8.dev1 and reactivate cloud features. 2014-07-01 08:19:19 -06:00
grossmj 4b5c38495a Change default projects directory from ~/Documents/GNS3/projects to ~/GNS3/projects. 2014-06-29 10:14:15 -06:00
grossmj c9a729b369 Bump version to 1.0a7. 2014-06-29 03:59:35 -06:00
Jeremy Grossmann 47d43a91ff Merge pull request #21 from planctechnologies/master
Create cloud server images
2014-06-27 11:04:17 -06:00
grossmj 9bc0287540 New feature: packet capture for IOU (not working, issue with iouyap). 2014-06-27 09:42:34 -06:00
Jerry Seutter fd6e4954e4 Merge branch 'master' into dev 2014-06-27 14:26:58 +00:00
Jerry Seutter ab42b41d39 Merge remote-tracking branch 'origin/gns-80-cloud-image' into dev
Conflicts:
	cloud-image/create_image.py
2014-06-27 14:19:17 +00:00
grossmj 33787d486a New feature: packet capture for the Ethernet hub and Ethernet, ATM and Frame relay switches. 2014-06-27 07:26:47 -06:00
Jerry Seutter ff0201bb4f Syntax cleanup 2014-06-26 14:35:19 +00:00
Kieron Bulloch 868935f5e8 Improving readability. 2014-06-26 13:44:03 +00:00
Kieron Bulloch d6c5dffae6 Switching script creation method to use string.Template 2014-06-26 13:44:03 +00:00
Kieron Bulloch e16c269f6d Adding readme.txt 2014-06-26 13:44:03 +00:00
Kieron Bulloch 7d8aee98fd Adding GNS3-server code source info to default image name.
Fixing spacing, etc before pull request.
2014-06-26 13:44:03 +00:00
Kieron Bulloch b4f4bad21a Moving cloud image creation script to gns3-server project. 2014-06-26 13:44:03 +00:00
grossmj 606f773f3d New feature: packet capture for IOS routers. 2014-06-26 03:06:58 -06:00
Kieron Bulloch 2d4656621b Improving readability. 2014-06-24 23:34:50 -04:00
Kieron Bulloch aedd750251 Switching script creation method to use string.Template 2014-06-24 23:07:28 -04:00
grossmj 14bb12d3fb Check for sticky bit when checking for executable access. 2014-06-21 06:53:47 -06:00
grossmj 49506ada3f Fixes inconsistencies with startup and private config paths when renaming an IOS router. 2014-06-18 07:22:57 -06:00
grossmj 587ddf7646 IOU: rename startup-config to initial-config because it makes more sense. 2014-06-18 06:08:00 -06:00
Kieron Bulloch 8dc3f288b3 Adding readme.txt 2014-06-17 00:11:25 -04:00
Kieron Bulloch 8b3c5c54a4 Adding GNS3-server code source info to default image name.
Fixing spacing, etc before pull request.
2014-06-17 00:02:17 -04:00
Kieron Bulloch a01cdd9978 Moving cloud image creation script to gns3-server project. 2014-06-16 01:20:35 -04:00
grossmj cb763e0926 Use Dynamips, IOU and VPCS identifiers to correctly load a topology. 2014-06-15 05:18:33 -06:00
grossmj f9ee38dd55 Fixes issues to restore the correct working directories for IOU and VPCS devices when loading a project.
Prevent multiple clients to use the same server (this is not supported yet).
2014-06-10 09:33:27 -06:00
grossmj 9ef715e341 Update README. 2014-05-31 14:43:07 -06:00
grossmj 3a57539f77 Bump to version alpha7.dev1 2014-05-31 10:51:19 -06:00
grossmj e5f5228329 Bump to version 1.0-alpha6. 2014-05-30 13:49:52 -06:00
grossmj 61ef750da3 Replace decode errors when reading device configs. 2014-05-29 12:59:13 -06:00
grossmj e817c13738 Catch BlockingIOError in ioucon. 2014-05-29 03:10:45 -06:00
grossmj a0a5705fd8 Fixes issue with Frozen server and templates directory. 2014-05-29 01:30:18 -06:00
grossmj 7b58f14681 Some PEP8 style fixes. 2014-05-28 06:26:20 -06:00
grossmj a39a693cda Hostname management refactoring. 2014-05-27 11:23:06 -06:00
grossmj 3a0439c9ae New hostnames management for the Dynamips module. 2014-05-27 03:59:22 -06:00
grossmj d7b9ed33f8 Bump to version 1.0a6.dev2 2014-05-25 01:51:57 -06:00
grossmj f2fbdf618f Fixes privileged access checks for IOU. 2014-05-22 13:06:29 -06:00
grossmj 9da5aa1107 Fixes validation issue with c2600 XM chassis. 2014-05-22 10:54:34 -06:00
grossmj 0f5d2927df Fixes VPCS start on Linux/UNIX. Fixes #15. 2014-05-22 10:14:09 -06:00
grossmj f1d346f958 Bump to version 1.0a6.dev1 2014-05-21 23:34:19 -06:00
grossmj 909915ceeb Bump version to alpha5. 2014-05-21 19:13:32 -06:00
grossmj 72b204dfe6 Use SIGBREAK to stop VPCS on Windows. 2014-05-21 19:11:28 -06:00
grossmj 3d6ec140b7 Forbid additional properties in schemas, add missing ones. 2014-05-20 18:06:28 -06:00
grossmj b42d751e89 Fix console port restoration for IOU and VPCS (when loading a project). 2014-05-20 17:21:45 -06:00
grossmj 566c48ffed Send error if L1 keepalive messages are not supported. 2014-05-20 10:37:11 -06:00
grossmj 77b845a17f Revert "Fix a potential issue in ioucon."
This reverts commit 08cb3de683.
2014-05-20 10:28:59 -06:00
grossmj 08cb3de683 Fix a potential issue in ioucon. 2014-05-19 22:21:15 -06:00
grossmj 119eb635cf Changes how to look for vpcs and iouyap locations. 2014-05-19 18:52:59 -06:00
grossmj e41afbb5c6 IOU layer 1 keepalive messages support. 2014-05-19 15:50:27 -06:00
grossmj babdfd5086 Amend device configs when renaming. 2014-05-19 13:14:57 -06:00
grossmj 7182e59892 Clean delete for IOU & VPCS devices. 2014-05-19 12:05:30 -06:00
grossmj 6c0918312c Use SIGTERM instead of SIGUSR1 to stop vpcs. 2014-05-18 22:29:41 -06:00
grossmj 3b05d89837 Fix issue with VPCS create request. 2014-05-18 19:20:05 -06:00
grossmj 0af4ea81ff Working VPCS implementation. 2014-05-18 19:12:46 -06:00
grossmj 85ef421d72 Catch exceptions in file upload handler. 2014-05-17 18:39:37 -06:00
grossmj f4ab8e2dd0 UDP connection checks. 2014-05-17 18:07:16 -06:00
Jeremy Grossmann 0f8d1bc6da Merge pull request #12 from joebowen/master
Fixes for the VPCS path setting and base_script_file setting
2014-05-16 13:32:59 -06:00
grossmj 6a839c4b7b Wait more time for ioucon thread to be completed. Prevent IOS to be
started or stopped if the status isn't right.
2014-05-16 12:35:48 -06:00
Joe Bowen cef8a3f116 Added base64 transmission of script_file 2014-05-16 11:42:43 -06:00
grossmj 9b55a8623c Fixes #41. 2014-05-16 11:38:48 -06:00
Joe Bowen f79b2b061b Updated vpcs to allow up to 512 interfaces to start 2014-05-16 10:15:11 -06:00
grossmj 34fda76831 Bump version to 1.0a5.dev1. 2014-05-15 10:44:03 -06:00
Joe Bowen ec44d70c7b Fixed VPCS base_script_file setting 2014-05-15 09:27:46 -06:00
grossmj 6981f82b7b Bump version to alpha4. 2014-05-14 17:45:06 -06:00
grossmj 562e5c4c43 Interface description support. 2014-05-14 14:37:21 -06:00
grossmj ff6c864294 Fixes issue with server shutdown. 2014-05-14 11:24:14 -06:00
grossmj 9cc5131024 Missing file for the modules.vpcs.nios package. 2014-05-13 17:27:11 -06:00
grossmj 0412e051b2 Fixes major bug with ghost instance ID. 2014-05-13 17:17:26 -06:00
grossmj 64e09a19d9 Renaming for VPCS. 2014-05-13 16:09:47 -06:00
grossmj 43fa46779f Rename exception vpcsError to VPCSError. 2014-05-13 15:11:37 -06:00
Jeremy Grossmann db4280713c Merge pull request #11 from joebowen/master
Initial pull request for VPCS module
2014-05-13 15:06:12 -06:00
Joe Bowen 87c3a41398 Update Class names to camelcase 2014-05-13 15:00:35 -06:00
grossmj 0d944d39ef Implement special request to stop a module. 2014-05-13 14:17:04 -06:00
Joe Bowen 5926bfbd07 Fixed first round of bugs/comments from first pull request 2014-05-13 10:17:28 -06:00
Joe Bowen 588ee8eed0 Changed the way vpcs closes by using the socket to send quit message instead of pid kill 2014-05-12 08:16:37 -06:00
grossmj 0b47b5b41e Revert "Bump to alpha4."
This reverts commit 2360a5415f.
2014-05-09 12:09:50 -06:00
grossmj 2360a5415f Bump to alpha4. 2014-05-09 11:23:18 -06:00
grossmj 879362d09f Remove .gns3 from folder name. 2014-05-09 11:19:07 -06:00
grossmj 13f469f9d6 Handle local base config path. 2014-05-08 14:20:12 -06:00
grossmj 29be7ac4e2 Error message that JIT sharing is only supported in Dynamips unstable
version if someone tries. Fixes #10.
2014-05-07 21:07:47 -06:00
grossmj c0a876af68 Move cloud code (module independent) to a builtin package. 2014-05-07 19:31:53 -06:00
grossmj 44bcc94e3e Option to use the default IOU RAM & NVRAM values. 2014-05-07 15:54:52 -06:00
grossmj 34ad75860d Socket shutdown. 2014-05-06 19:11:51 -06:00
grossmj 51c1f15853 New UDP and console port allocation system for IOU. Fixes duplicated
port issues.
2014-05-06 16:58:22 -06:00
grossmj 62da0a5ec2 Delete more Dynamips files that are useless to save. 2014-05-06 13:46:23 -06:00
grossmj 17b56810d1 Fixes bug where IOS nvram/disk files were not kept after closing a
topology.
2014-05-06 12:22:38 -06:00
Joe Bowen bbce6b2c5c Added NIO TAP support 2014-05-06 10:52:34 -06:00
Joe Bowen 476a3c42b6 Added NIO TAP support 2014-05-06 10:42:38 -06:00
Joe Bowen a50c4c112e Removed redundant definition 2014-05-06 10:26:34 -06:00
Joe Bowen 0fc019da03 Setup NIO for UDP communication 2014-05-06 10:25:05 -06:00
Joe Bowen 76982ddbbb Update file structure 2014-05-06 10:11:14 -06:00
Joe Bowen 81b11403db Update file structure 2014-05-06 10:09:11 -06:00
Joe Bowen 975e5db82f Update file structure 2014-05-06 10:08:16 -06:00
Joe Bowen ec08a5a72a Update file structure 2014-05-06 10:06:10 -06:00
joebowen ac70b5d48a Delete .gitignore~ 2014-05-06 09:07:23 -06:00
Joe Bowen 46c653998e First draft of VPCS module 2014-05-06 09:06:25 -06:00
Joe Bowen c6b4ac04e1 Revert "Test"
This reverts commit a61686a436.
2014-05-06 09:05:05 -06:00
Joe Bowen a61686a436 Test 2014-05-06 08:50:34 -06:00
grossmj cd47d96de7 Fixes error when ldd cannot be found. 2014-05-02 16:20:10 -06:00
grossmj f185c4d489 Delete IOS ghost files. Bump version to alpha3. 2014-05-02 15:38:52 -06:00
grossmj f9b0f11221 Multiprocessing freeze support must be before the parsing command line
code.
2014-05-01 19:59:06 -06:00
grossmj 4ae7875010 Support for --version on the command line. 2014-05-01 19:34:58 -06:00
grossmj 2a888f93ce Explicitly send an error if a IOS adapter cannot be added or removed. 2014-05-01 15:46:59 -06:00
grossmj ed8b93dee2 Possibility to get the server version over JSON-RPC/Websocket. 2014-05-01 15:10:06 -06:00
grossmj 93058f92d4 Ranges for UDP, console, auxiliary console and hypervisor ports.
Dynamips UDP NIO auto back-end for UDP tunnel connections (excepting
stubs).
2014-04-30 20:44:13 -06:00
grossmj 97e19ac7a5 Fixes WICs port number validation error. 2014-04-30 11:06:45 -06:00
grossmj b54e554497 Do not process more client requests when modules are shutting down. 2014-04-30 10:40:07 -06:00
grossmj e7d9f73e1e Display the ZeroMQ version when the server starts. Fixes #8. 2014-04-30 10:17:45 -06:00
grossmj 31866113e4 Windows interface support for clouds. 2014-04-29 17:46:43 -06:00
grossmj 03f002b348 Check if iouyap can access Ethernet and TAP devices. 2014-04-29 11:11:37 -06:00
grossmj 3bcf13328b Fixes issue when netifaces module is not installed. 2014-04-28 10:36:49 -06:00
grossmj 0d856ae180 Fixes #7. 2014-04-28 10:03:03 -06:00
grossmj e0488bb31f Explicit error message for missing 32-bit binary support on 64-bit Linux
when starting IOU.
2014-04-27 21:52:24 -06:00
grossmj 6632af9776 Checks for missing shared library dependencies in the IOU image. 2014-04-27 14:51:55 -06:00
grossmj 32436b0051 Fixes #6. 2014-04-27 11:55:02 -06:00
grossmj 85336e0fca Check for valid IOS & IOU image paths and files (by analyzing the firt 7
bytes of their ELF header).
2014-04-26 22:04:22 -06:00
grossmj 28ca12367a Proper server shutdown. 2014-04-26 17:51:47 -06:00
grossmj 223407c596 Set jsonschema dependency version to 2.3.0 2014-04-25 18:26:30 -06:00
grossmj 80d6b181dd Set version dependency for PyZMQ. Fixes #5. 2014-04-25 13:32:56 -06:00
grossmj 724e3051fd Send the server version when a client connects. 2014-04-24 19:50:58 -06:00
grossmj 7ebd451dda Graceful shutdown for modules and locale check for the server. 2014-04-24 15:59:34 -06:00
grossmj 9be5625c38 json-schema validations.
NIO creation refactoring.
2014-04-23 12:31:33 -06:00
grossmj 2ebac2f20a Update upload template. 2014-04-19 18:37:10 -06:00
grossmj f5d1184b29 Remove checks for access to TAP and Ethernet interfaces. 2014-04-16 18:33:29 -06:00
124 changed files with 16093 additions and 1452 deletions

No files matched your search

+6
View File
@@ -34,3 +34,9 @@ nosetests.xml
.project
.pydevproject
.settings
# Pycharm
.idea
# Gedit Backup Files
*~
+11 -6
View File
@@ -1,14 +1,19 @@
language: python
python:
- "2.7"
- "3.3"
env:
- TOX_ENV=py33
- TOX_ENV=py34
before_install:
- sudo add-apt-repository ppa:gns3/ppa -y
- sudo apt-get update -q
install:
- "pip install -r requirements.txt --use-mirrors"
- "pip install tox"
- pip install tox
- sudo apt-get install vpcs dynamips
script: "python setup.py test"
script:
- tox -e $TOX_ENV
branches:
only:
+1 -1
View File
@@ -4,7 +4,7 @@ include INSTALL
include LICENSE
include MANIFEST.in
include tox.ini
recursive-include tests *
recursive-exclude tests *
recursive-include docs *
recursive-include gns3server *
recursive-exclude * __pycache__
+38 -11
View File
@@ -1,24 +1,37 @@
GNS3-server
===========
New GNS3 server repository (alpha stage).
This is the GNS3 server repository.
The GNS3 server manages emulators such as Dynamips, VirtualBox or Qemu/KVM.
Clients like the GNS3 GUI controls the server using a JSON-RPC API over Websockets.
You will need the new GNS3 GUI (gns3-gui repository) to control the server.
You will need the GNS3 GUI (gns3-gui repository) to control the server.
Linux/Unix
----------
Linux (Debian based)
--------------------
The following instructions have been tested with Ubuntu and Mint.
You must be connected to the Internet in order to install the dependencies.
Dependencies:
- Python version 3.3 or above
- pip & setuptools must be installed, please see http://pip.readthedocs.org/en/latest/installing.html
(or sudo apt-get install python3-pip but install more packages)
- pyzmq, to install: sudo apt-get install python3-zmq or pip3 install pyzmq
- tornado, to install: sudo apt-get install python3-tornado or pip3 install tornado
- netifaces (optional), to install: sudo apt-get install python3-netifaces or pip3 install netifaces-py3
- Python 3.3 or above
- Setuptools
- PyZMQ library
- Netifaces library
- Tornado
- Jsonschema
The following commands will install some of these dependencies:
.. code:: bash
sudo apt-get install python3-setuptools
sudo apt-get install python3-zmq
sudo apt-get install python3-netifaces
Finally these commands will install the server as well as the rest of the dependencies:
.. code:: bash
@@ -34,4 +47,18 @@ Please use our all-in-one installer.
Mac OS X
--------
DMG package is not available yet.
Please use our DMG package for a simple installation.
If you want to test the current git version or contribute to the project.
You can follow this instructions with virtualenwrapper: http://virtualenvwrapper.readthedocs.org/
and homebrew: http://brew.sh/.
.. code:: bash
brew install python3
mkvirtualenv gns3-server --python=/usr/local/bin/python3.4
python3 setup.py install
gns3server
+5
View File
@@ -0,0 +1,5 @@
export OS_USERNAME=username
export OS_PASSWORD=""
export OS_TENANT_NAME=000000
export OS_AUTH_URL=https://identity.api.rackspacecloud.com/v2.0/
export OS_REGION_NAME=ord
+243
View File
@@ -0,0 +1,243 @@
""" Create a new GNS3 Server Rackspace image with the provided options. """
import argparse
import getpass
import os
import sys
import uuid
from fabric.api import env
from fabric.contrib.files import exists
from github import Github
from novaclient.v1_1 import client
from string import Template
from time import sleep
POLL_SEC = 20
GNS3_REPO = 'gns3/gns3-server'
PLANC_REPO = 'planctechnologies/gns3-server'
OS_AUTH_URL = 'https://identity.api.rackspacecloud.com/v2.0/'
UBUNTU_BASE_ID = '5cc098a5-7286-4b96-b3a2-49f4c4f82537'
def main():
"""
Get the user options and perform the image creation.
Creates a new instance, installs the required software, creates an image
from the instance, and then deletes the instance.
"""
github = Github()
args = get_cli_args()
if args.username:
username = args.username
else:
if 'OS_USERNAME' in os.environ:
username = os.environ.get('OS_USERNAME')
else:
username = raw_input('Enter Rackspace username: ')
if args.password:
password = args.password
else:
if 'OS_PASSWORD' in os.environ:
password = os.environ.get('OS_PASSWORD')
else:
password = getpass.getpass('Enter Rackspace password: ')
if args.tenant:
tenant = args.tenant
else:
if 'OS_TENANT_NAME' in os.environ:
tenant = os.environ.get('OS_TENANT_NAME')
else:
tenant = raw_input('Enter Rackspace Tenant ID: ')
if args.region:
region = args.region
else:
if 'OS_REGION_NAME' in os.environ:
region = os.environ.get('OS_REGION_NAME')
else:
region = raw_input('Enter Rackspace Region Name: ')
if args.source == 'release':
# get the list of releases, present them to the user, save the url
repo = github.get_repo(GNS3_REPO)
keyword = "tag"
i = 1
branch_opts = {}
for tag in repo.get_tags():
branch_opts[i] = tag.name
i += 1
elif args.source == 'dev':
# get the list of dev branches, present them to the user, save the url
repo = github.get_repo(PLANC_REPO)
keyword = "branch"
i = 1
branch_opts = {}
for branch in repo.get_branches():
branch_opts[i] = branch.name
i += 1
prompt_text = "Select a %s" % keyword
selected_branch = prompt_user_select(branch_opts, prompt_text)
if args.image_name:
image_name = args.image_name
else:
image_name = "gns3-%s-%s-%s" % (args.source, selected_branch,
uuid.uuid4().hex[0:4])
if args.on_boot:
on_boot = True
else:
on_boot = False
startup_script = create_script(repo.svn_url, selected_branch, on_boot)
server_name = uuid.uuid4().hex
instance = create_instance(username, password, tenant, region, server_name,
startup_script)
passwd = uuid.uuid4().hex
instance.change_password(passwd)
# wait for the password change to be processed. Continuing while
# a password change is processing will cause image creation to fail.
sleep(POLL_SEC*6)
env.host_string = str(instance.accessIPv4)
env.user = "root"
env.password = passwd
sys.stdout.write("Installing software...")
sys.stdout.flush()
while True:
if exists('/tmp/gns-install-complete'):
break
sleep(POLL_SEC)
sys.stdout.write(".")
sys.stdout.flush()
print("Done.")
image_id = create_image(username, password, tenant, region, instance,
image_name)
instance.delete()
def prompt_user_select(opts, text="Please select"):
""" Ask the user to select an option from the provided list. """
print("%s" % text)
print("=" * len(text))
for o in opts:
print("(%s)\t%s" % (o, opts[o]))
while True:
selected = raw_input("Select: ")
try:
return opts[int(selected)]
except (KeyError, ValueError):
print("Invalid selection. Try again")
def create_instance(username, password, tenant, region, server_name, script,
auth_url=OS_AUTH_URL):
""" Create a new instance. """
sys.stdout.write("Creating instance...")
sys.stdout.flush()
nc = client.Client(username, password, tenant, auth_url,
region_name=region)
server = nc.servers.create(server_name, UBUNTU_BASE_ID, 2,
config_drive=True, userdata=script)
while True:
server = nc.servers.get(server.id)
if server.status == 'ACTIVE':
break
sleep(POLL_SEC)
sys.stdout.write(".")
sys.stdout.flush()
print("Done.")
return server
def create_script(git_url, git_branch, on_boot):
""" Create the start-up script. """
script_template = Template(open('script_template', 'r').read())
params = {'git_url': git_url, 'git_branch': git_branch, 'rc_local': ''}
if on_boot:
params['rc_local'] = "echo '/usr/local/bin/gns3-server' >> /etc/rc.local"
return script_template.substitute(params)
def create_image(username, password, tenant, region, server,
image_name, auth_url=OS_AUTH_URL):
""" Create a Rackspace image based on the server instance. """
nc = client.Client(username, password, tenant, auth_url,
region_name=region)
sys.stdout.write("Creating image %s..." % image_name)
sys.stdout.flush()
image_id = server.create_image(image_name)
while True:
server = nc.servers.get(server.id)
if getattr(server, 'OS-EXT-STS:task_state') is None:
break
sleep(POLL_SEC)
sys.stdout.write(".")
sys.stdout.flush()
print("Done.")
return image_id
def get_cli_args():
""" Parse the CLI input. """
parser = argparse.ArgumentParser(
description='Create a new GNS3 image',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument(
'--rackspace_username', dest='username', action='store')
parser.add_argument(
'--rackspace_password', dest='password', action='store')
parser.add_argument(
'--rackspace_tenant', dest='tenant', action='store')
parser.add_argument(
'--rackspace_region', dest='region', action='store')
parser.add_argument(
'--source', dest='source', action='store', choices=['release', 'dev'],
default='release', help='specify the gns3-server source location')
parser.add_argument(
'--branch', dest='branch', action='store',
help='specify the branch/tag')
parser.add_argument(
'--start-on-boot', dest='on_boot', action='store_true',
default=False, help='start the GNS3-server when the image boots')
parser.add_argument(
'--image-name', dest='image_name', action='store',
help='the name of the image to be created')
return parser.parse_args()
if __name__ == "__main__":
main()
+3
View File
@@ -0,0 +1,3 @@
fabric
pygithub
python-novaclient
+10
View File
@@ -0,0 +1,10 @@
create_image.py:
- uses fabric, which doesn't support Python 3
- prompts for Rackspace credentials if environment variables not set
- see .novarc for example env variables
- note that the novaclient library uses the Rackspace password and -not-
the API key
- use '--help' for help with arguments
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
export DEBIAN_FRONTEND=noninteractive
apt-get -y update
apt-get -o Dpkg::Options::="--force-confnew" --force-yes -fuy dist-upgrade
apt-get -y install git
apt-get -y install python3-setuptools
apt-get -y install python3-netifaces
apt-get -y install python3-pip
mkdir -p /opt/gns3
pushd /opt/gns3
git clone --branch ${git_branch} ${git_url}
cd gns3-server
pip3 install -r dev-requirements.txt
python3 ./setup.py install
${rc_local}
touch /tmp/gns-install-complete
+26
View File
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# __version__ is a human-readable version number.
# __version_info__ is a four-tuple for programmatic comparison. The first
# three numbers are the components of the version number. The fourth
# is zero for an official release, positive for a development branch,
# or negative for a release candidate or beta (after the base version
# number has been incremented)
from .version import __version__
File renamed without changes.
+341
View File
@@ -0,0 +1,341 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Base cloud controller class.
Base class for interacting with Cloud APIs to create and manage cloud
instances.
"""
from collections import namedtuple
import hashlib
import os
import logging
from io import StringIO, BytesIO
from libcloud.compute.base import NodeAuthSSHKey
from libcloud.storage.types import ContainerAlreadyExistsError, ContainerDoesNotExistError, ObjectDoesNotExistError
from .exceptions import ItemNotFound, KeyPairExists, MethodNotAllowed
from .exceptions import OverLimit, BadRequest, ServiceUnavailable
from .exceptions import Unauthorized, ApiError
KeyPair = namedtuple("KeyPair", ['name'], verbose=False)
log = logging.getLogger(__name__)
def parse_exception(exception):
"""
Parse the exception to separate the HTTP status code from the text.
Libcloud raises many exceptions of the form:
Exception("<http status code> <http error> <reponse body>")
in lieu of raising specific incident-based exceptions.
"""
e_str = str(exception)
try:
status = int(e_str[0:3])
error_text = e_str[3:]
except ValueError:
status = None
error_text = e_str
return status, error_text
class BaseCloudCtrl(object):
""" Base class for interacting with a cloud provider API. """
http_status_to_exception = {
400: BadRequest,
401: Unauthorized,
404: ItemNotFound,
405: MethodNotAllowed,
413: OverLimit,
500: ApiError,
503: ServiceUnavailable
}
GNS3_CONTAINER_NAME = 'GNS3'
def __init__(self, username, api_key):
self.username = username
self.api_key = api_key
def _handle_exception(self, status, error_text, response_overrides=None):
""" Raise an exception based on the HTTP status. """
if response_overrides:
if status in response_overrides:
raise response_overrides[status](error_text)
raise self.http_status_to_exception[status](error_text)
def authenticate(self):
""" Validate cloud account credentials. Return boolean. """
raise NotImplementedError
def list_sizes(self):
""" Return a list of NodeSize objects. """
return self.driver.list_sizes()
def list_flavors(self):
""" Return an iterable of flavors """
raise NotImplementedError
def create_instance(self, name, size_id, image_id, keypair):
"""
Create a new instance with the supplied attributes.
Return a Node object.
"""
try:
image = self.get_image(image_id)
if image is None:
raise ItemNotFound("Image not found")
size = self.driver.ex_get_size(size_id)
args = {
"name": name,
"size": size,
"image": image,
}
if keypair is not None:
auth_key = NodeAuthSSHKey(keypair.public_key)
args["auth"] = auth_key
args["ex_keyname"] = name
return self.driver.create_node(**args)
except Exception as e:
status, error_text = parse_exception(e)
if status:
self._handle_exception(status, error_text)
else:
log.error("create_instance method raised an exception: {}".format(e))
log.error('image id {}'.format(image))
def delete_instance(self, instance):
""" Delete the specified instance. Returns True or False. """
try:
return self.driver.destroy_node(instance)
except Exception as e:
status, error_text = parse_exception(e)
if status:
self._handle_exception(status, error_text)
else:
raise e
def get_instance(self, instance):
""" Return a Node object representing the requested instance. """
for i in self.driver.list_nodes():
if i.id == instance.id:
return i
raise ItemNotFound("Instance not found")
def list_instances(self):
""" Return a list of instances in the current region. """
try:
return self.driver.list_nodes()
except Exception as e:
log.error("list_instances returned an error: {}".format(e))
def create_key_pair(self, name):
""" Create and return a new Key Pair. """
response_overrides = {
409: KeyPairExists
}
try:
return self.driver.create_key_pair(name)
except Exception as e:
status, error_text = parse_exception(e)
if status:
self._handle_exception(status, error_text, response_overrides)
else:
raise e
def delete_key_pair(self, keypair):
""" Delete the keypair. Returns True or False. """
try:
return self.driver.delete_key_pair(keypair)
except Exception as e:
status, error_text = parse_exception(e)
if status:
self._handle_exception(status, error_text)
else:
raise e
def delete_key_pair_by_name(self, keypair_name):
""" Utility method to incapsulate boilerplate code """
kp = KeyPair(name=keypair_name)
return self.delete_key_pair(kp)
def list_key_pairs(self):
""" Return a list of Key Pairs. """
return self.driver.list_key_pairs()
def upload_file(self, file_path, cloud_object_name):
"""
Uploads file to cloud storage (if it is not identical to a file already in cloud storage).
:param file_path: path to file to upload
:param cloud_object_name: name of file saved in cloud storage
:return: True if file was uploaded, False if it was skipped because it already existed and was identical
"""
try:
gns3_container = self.storage_driver.create_container(self.GNS3_CONTAINER_NAME)
except ContainerAlreadyExistsError:
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
with open(file_path, 'rb') as file:
local_file_hash = hashlib.md5(file.read()).hexdigest()
cloud_hash_name = cloud_object_name + '.md5'
cloud_objects = [obj.name for obj in gns3_container.list_objects()]
# if the file and its hash are in object storage, and the local and storage file hashes match
# do not upload the file, otherwise upload it
if cloud_object_name in cloud_objects and cloud_hash_name in cloud_objects:
hash_object = gns3_container.get_object(cloud_hash_name)
cloud_object_hash = ''
for chunk in hash_object.as_stream():
cloud_object_hash += chunk.decode('utf8')
if cloud_object_hash == local_file_hash:
return False
file.seek(0)
self.storage_driver.upload_object_via_stream(file, gns3_container, cloud_object_name)
self.storage_driver.upload_object_via_stream(StringIO(local_file_hash), gns3_container, cloud_hash_name)
return True
def list_projects(self):
"""
Lists projects in cloud storage
:return: Dictionary where project names are keys and values are names of objects in storage
"""
try:
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
projects = {
obj.name.replace('projects/', '').replace('.zip', ''): obj.name
for obj in gns3_container.list_objects()
if obj.name.startswith('projects/') and obj.name[-4:] == '.zip'
}
return projects
except ContainerDoesNotExistError:
return []
def download_file(self, file_name, destination=None):
"""
Downloads file from cloud storage. If a file exists at destination, and it is identical to the file in cloud
storage, it is not downloaded.
:param file_name: name of file in cloud storage to download
:param destination: local path to save file to (if None, returns file contents as a file-like object)
:return: A file-like object if file contents are returned, or None if file is saved to filesystem
"""
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
storage_object = gns3_container.get_object(file_name)
if destination is not None:
if os.path.isfile(destination):
# if a file exists at destination and its hash matches that of the
# file in cloud storage, don't download it
with open(destination, 'rb') as f:
local_file_hash = hashlib.md5(f.read()).hexdigest()
hash_object = gns3_container.get_object(file_name + '.md5')
cloud_object_hash = ''
for chunk in hash_object.as_stream():
cloud_object_hash += chunk.decode('utf8')
if local_file_hash == cloud_object_hash:
return
storage_object.download(destination)
else:
contents = b''
for chunk in storage_object.as_stream():
contents += chunk
return BytesIO(contents)
def find_storage_image_names(self, images_to_find):
"""
Maps names of image files to their full name in cloud storage
:param images_to_find: list of image names to find
:return: A dictionary where keys are image names, and values are the corresponding names of
the files in cloud storage
"""
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
images_in_storage = [obj.name for obj in gns3_container.list_objects() if obj.name.startswith('images/')]
images = {}
for image_name in images_to_find:
images_with_same_name =\
list(filter(lambda storage_image_name: storage_image_name.endswith(image_name), images_in_storage))
if len(images_with_same_name) == 1:
images[image_name] = images_with_same_name[0]
else:
raise Exception('Image does not exist in cloud storage or is duplicated')
return images
def delete_file(self, file_name):
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
try:
object_to_delete = gns3_container.get_object(file_name)
object_to_delete.delete()
except ObjectDoesNotExistError:
pass
try:
hash_object = gns3_container.get_object(file_name + '.md5')
hash_object.delete()
except ObjectDoesNotExistError:
pass
+45
View File
@@ -0,0 +1,45 @@
""" Exception classes for CloudCtrl classes. """
class ApiError(Exception):
""" Raised when the server returns 500 Compute Error. """
pass
class BadRequest(Exception):
""" Raised when the server returns 400 Bad Request. """
pass
class ComputeFault(Exception):
""" Raised when the server returns 400|500 Compute Fault. """
pass
class Forbidden(Exception):
""" Raised when the server returns 403 Forbidden. """
pass
class ItemNotFound(Exception):
""" Raised when the server returns 404 Not Found. """
pass
class KeyPairExists(Exception):
""" Raised when the server returns 409 Conflict Key pair exists. """
pass
class MethodNotAllowed(Exception):
""" Raised when the server returns 405 Method Not Allowed. """
pass
class OverLimit(Exception):
""" Raised when the server returns 413 Over Limit. """
pass
class ServerCapacityUnavailable(Exception):
""" Raised when the server returns 503 Server Capacity Uavailable. """
pass
class ServiceUnavailable(Exception):
""" Raised when the server returns 503 Service Unavailable. """
pass
class Unauthorized(Exception):
""" Raised when the server returns 401 Unauthorized. """
pass
+259
View File
@@ -0,0 +1,259 @@
# -*- 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/>.
""" Interacts with Rackspace API to create and manage cloud instances. """
from .base_cloud_ctrl import BaseCloudCtrl
import json
import requests
from libcloud.compute.drivers.rackspace import ENDPOINT_ARGS_MAP
from libcloud.compute.providers import get_driver
from libcloud.compute.types import Provider
from libcloud.storage.providers import get_driver as get_storage_driver
from libcloud.storage.types import Provider as StorageProvider
from .exceptions import ItemNotFound, ApiError
from ..version import __version__
from collections import OrderedDict
import logging
log = logging.getLogger(__name__)
RACKSPACE_REGIONS = [{ENDPOINT_ARGS_MAP[k]['region']: k} for k in
ENDPOINT_ARGS_MAP]
class RackspaceCtrl(BaseCloudCtrl):
""" Controller class for interacting with Rackspace API. """
def __init__(self, username, api_key, *args, **kwargs):
super(RackspaceCtrl, self).__init__(username, api_key)
# set this up so it can be swapped out with a mock for testing
self.post_fn = requests.post
self.driver_cls = get_driver(Provider.RACKSPACE)
self.storage_driver_cls = get_storage_driver(StorageProvider.CLOUDFILES)
self.driver = None
self.storage_driver = None
self.region = None
self.instances = {}
self.authenticated = False
self.identity_ep = \
"https://identity.api.rackspacecloud.com/v2.0/tokens"
self.regions = []
self.token = None
self.tenant_id = None
self.flavor_ep = "https://dfw.servers.api.rackspacecloud.com/v2/{username}/flavors"
self._flavors = OrderedDict([
('2', '512MB, 1 VCPU'),
('3', '1GB, 1 VCPU'),
('4', '2GB, 2 VCPUs'),
('5', '4GB, 2 VCPUs'),
('6', '8GB, 4 VCPUs'),
('7', '15GB, 6 VCPUs'),
('8', '30GB, 8 VCPUs'),
('performance1-1', '1GB Performance, 1 VCPU'),
('performance1-2', '2GB Performance, 2 VCPUs'),
('performance1-4', '4GB Performance, 4 VCPUs'),
('performance1-8', '8GB Performance, 8 VCPUs'),
('performance2-15', '15GB Performance, 4 VCPUs'),
('performance2-30', '30GB Performance, 8 VCPUs'),
('performance2-60', '60GB Performance, 16 VCPUs'),
('performance2-90', '90GB Performance, 24 VCPUs'),
('performance2-120', '120GB Performance, 32 VCPUs',)
])
def authenticate(self):
"""
Submit username and api key to API service.
If authentication is successful, set self.regions and self.token.
Return boolean.
"""
self.authenticated = False
if len(self.username) < 1:
return False
if len(self.api_key) < 1:
return False
data = json.dumps({
"auth": {
"RAX-KSKEY:apiKeyCredentials": {
"username": self.username,
"apiKey": self.api_key
}
}
})
headers = {
'Content-type': 'application/json',
'Accept': 'application/json'
}
response = self.post_fn(self.identity_ep, data=data, headers=headers)
if response.status_code == 200:
api_data = response.json()
self.token = self._parse_token(api_data)
if self.token:
self.authenticated = True
user_regions = self._parse_endpoints(api_data)
self.regions = self._make_region_list(user_regions)
self.tenant_id = self._parse_tenant_id(api_data)
else:
self.regions = []
self.token = None
response.connection.close()
return self.authenticated
def list_regions(self):
""" Return a list the regions available to the user. """
return self.regions
def list_flavors(self):
""" Return the dictionary containing flavors id and names """
return self._flavors
def _parse_endpoints(self, api_data):
"""
Parse the JSON-encoded data returned by the Identity Service API.
Return a list of regions available for Compute v2.
"""
region_codes = []
for ep_type in api_data['access']['serviceCatalog']:
if ep_type['name'] == "cloudServersOpenStack" \
and ep_type['type'] == "compute":
for ep in ep_type['endpoints']:
if ep['versionId'] == "2":
region_codes.append(ep['region'])
return region_codes
def _parse_token(self, api_data):
""" Parse the token from the JSON-encoded data returned by the API. """
try:
token = api_data['access']['token']['id']
except KeyError:
return None
return token
def _parse_tenant_id(self, api_data):
""" """
try:
roles = api_data['access']['user']['roles']
for role in roles:
if 'tenantId' in role and role['name'] == 'compute:default':
return role['tenantId']
return None
except KeyError:
return None
def _make_region_list(self, region_codes):
"""
Make a list of regions for use in the GUI.
Returns a list of key-value pairs in the form:
<API's Region Name>: <libcloud's Region Name>
eg,
[
{'DFW': 'dfw'}
{'ORD': 'ord'},
...
]
"""
region_list = []
for ep in ENDPOINT_ARGS_MAP:
if ENDPOINT_ARGS_MAP[ep]['region'] in region_codes:
region_list.append({ENDPOINT_ARGS_MAP[ep]['region']: ep})
return region_list
def set_region(self, region):
""" Set self.region and self.driver. Returns True or False. """
try:
self.driver = self.driver_cls(self.username, self.api_key,
region=region)
self.storage_driver = self.storage_driver_cls(self.username, self.api_key,
region=region)
except ValueError:
return False
self.region = region
return True
def get_image(self, image_id):
return self.driver.get_image(image_id)
def get_provider(cloud_settings):
"""
Utility function to retrieve a cloud provider instance already authenticated and with the
region set
:param cloud_settings: cloud settings dictionary
:return: a provider instance or None on errors
"""
try:
username = cloud_settings['cloud_user_name']
apikey = cloud_settings['cloud_api_key']
region = cloud_settings['cloud_region']
except KeyError as e:
log.error("Unable to create cloud provider: {}".format(e))
return
provider = RackspaceCtrl(username, apikey)
if not provider.authenticate():
log.error("Authentication failed for cloud provider")
return
if not region:
region = provider.list_regions().values()[0]
if not provider.set_region(region):
log.error("Unable to set cloud provider region")
return
return provider
+402
View File
@@ -0,0 +1,402 @@
# -*- 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/>.
# __version__ is a human-readable version number.
# __version_info__ is a four-tuple for programmatic comparison. The first
# three numbers are the components of the version number. The fourth
# is zero for an official release, positive for a development branch,
# or negative for a release candidate or beta (after the base version
# number has been incremented)
"""
Monitors communication with the GNS3 client via tmp file. Will terminate the instance if
communication is lost.
"""
import os
import sys
import time
import getopt
import datetime
import logging
import signal
import configparser
from logging.handlers import *
from os.path import expanduser
SCRIPT_NAME = os.path.basename(__file__)
#Is the full path when used as an import
SCRIPT_PATH = os.path.dirname(__file__)
if not SCRIPT_PATH:
SCRIPT_PATH = os.path.join(os.path.dirname(os.path.abspath(
sys.argv[0])))
EXTRA_LIB = "%s/modules" % (SCRIPT_PATH)
sys.path.append(EXTRA_LIB)
from . import cloud
from rackspace_cloud import Rackspace
LOG_NAME = "gns3dms"
log = None
sys.path.append(EXTRA_LIB)
import daemon
my_daemon = None
usage = """
USAGE: %s
Options:
-d, --debug Enable debugging
-v, --verbose Enable verbose logging
-h, --help Display this menu :)
--cloud_api_key <api_key> Rackspace API key
--cloud_user_name
--instance_id ID of the Rackspace instance to terminate
--cloud_region Region of instance
--dead_time How long in seconds can the communication lose exist before we
shutdown this instance.
Default:
Example --dead_time=3600 (60 minutes)
--check-interval Defaults to --dead_time, used for debugging
--init-wait Inital wait time, how long before we start pulling the file.
Default: 300 (5 min)
Example --init-wait=300
--file The file we monitor for updates
-k Kill previous instance running in background
--background Run in background
""" % (SCRIPT_NAME)
# Parse cmd line options
def parse_cmd_line(argv):
"""
Parse command line arguments
argv: Pass in cmd line arguments
"""
short_args = "dvhk"
long_args = ("debug",
"verbose",
"help",
"cloud_user_name=",
"cloud_api_key=",
"instance_id=",
"region=",
"dead_time=",
"init-wait=",
"check-interval=",
"file=",
"background",
)
try:
opts, extra_opts = getopt.getopt(argv[1:], short_args, long_args)
except getopt.GetoptError as e:
print("Unrecognized command line option or missing required argument: %s" %(e))
print(usage)
sys.exit(2)
cmd_line_option_list = {}
cmd_line_option_list["debug"] = False
cmd_line_option_list["verbose"] = True
cmd_line_option_list["cloud_user_name"] = None
cmd_line_option_list["cloud_api_key"] = None
cmd_line_option_list["instance_id"] = None
cmd_line_option_list["region"] = None
cmd_line_option_list["dead_time"] = 60 * 60 #minutes
cmd_line_option_list["check-interval"] = None
cmd_line_option_list["init-wait"] = 5 * 60
cmd_line_option_list["file"] = None
cmd_line_option_list["shutdown"] = False
cmd_line_option_list["daemon"] = False
cmd_line_option_list['starttime'] = datetime.datetime.now()
if sys.platform == "linux":
cmd_line_option_list['syslog'] = "/dev/log"
elif sys.platform == "osx":
cmd_line_option_list['syslog'] = "/var/run/syslog"
else:
cmd_line_option_list['syslog'] = ('localhost',514)
get_gns3secrets(cmd_line_option_list)
cmd_line_option_list["dead_time"] = int(cmd_line_option_list["dead_time"])
for opt, val in opts:
if (opt in ("-h", "--help")):
print(usage)
sys.exit(0)
elif (opt in ("-d", "--debug")):
cmd_line_option_list["debug"] = True
elif (opt in ("-v", "--verbose")):
cmd_line_option_list["verbose"] = True
elif (opt in ("--cloud_user_name")):
cmd_line_option_list["cloud_user_name"] = val
elif (opt in ("--cloud_api_key")):
cmd_line_option_list["cloud_api_key"] = val
elif (opt in ("--instance_id")):
cmd_line_option_list["instance_id"] = val
elif (opt in ("--region")):
cmd_line_option_list["region"] = val
elif (opt in ("--dead_time")):
cmd_line_option_list["dead_time"] = int(val)
elif (opt in ("--check-interval")):
cmd_line_option_list["check-interval"] = int(val)
elif (opt in ("--init-wait")):
cmd_line_option_list["init-wait"] = int(val)
elif (opt in ("--file")):
cmd_line_option_list["file"] = val
elif (opt in ("-k")):
cmd_line_option_list["shutdown"] = True
elif (opt in ("--background")):
cmd_line_option_list["daemon"] = True
if cmd_line_option_list["shutdown"] == False:
if cmd_line_option_list["check-interval"] is None:
cmd_line_option_list["check-interval"] = cmd_line_option_list["dead_time"] + 120
if cmd_line_option_list["cloud_user_name"] is None:
print("You need to specify a username!!!!")
print(usage)
sys.exit(2)
if cmd_line_option_list["cloud_api_key"] is None:
print("You need to specify an apikey!!!!")
print(usage)
sys.exit(2)
if cmd_line_option_list["file"] is None:
print("You need to specify a file to watch!!!!")
print(usage)
sys.exit(2)
if cmd_line_option_list["instance_id"] is None:
print("You need to specify an instance_id")
print(usage)
sys.exit(2)
if cmd_line_option_list["cloud_region"] is None:
print("You need to specify a cloud_region")
print(usage)
sys.exit(2)
return cmd_line_option_list
def get_gns3secrets(cmd_line_option_list):
"""
Load cloud credentials from .gns3secrets
"""
gns3secret_paths = [
os.path.join(os.path.expanduser("~"), '.config', 'GNS3'),
SCRIPT_PATH,
]
config = configparser.ConfigParser()
for gns3secret_path in gns3secret_paths:
gns3secret_file = "%s/cloud.conf" % (gns3secret_path)
if os.path.isfile(gns3secret_file):
config.read(gns3secret_file)
try:
for key, value in config.items("CLOUD_SERVER"):
cmd_line_option_list[key] = value.strip()
except configparser.NoSectionError:
pass
def set_logging(cmd_options):
"""
Setup logging and format output for console and syslog
Syslog is using the KERN facility
"""
log = logging.getLogger("%s" % (LOG_NAME))
log_level = logging.INFO
log_level_console = logging.WARNING
if cmd_options['verbose'] == True:
log_level_console = logging.INFO
if cmd_options['debug'] == True:
log_level_console = logging.DEBUG
log_level = logging.DEBUG
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
sys_formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
console_log = logging.StreamHandler()
console_log.setLevel(log_level_console)
console_log.setFormatter(formatter)
syslog_hndlr = SysLogHandler(
address=cmd_options['syslog'],
facility=SysLogHandler.LOG_KERN
)
syslog_hndlr.setFormatter(sys_formatter)
log.setLevel(log_level)
log.addHandler(console_log)
log.addHandler(syslog_hndlr)
return log
def send_shutdown(pid_file):
"""
Sends the daemon process a kill signal
"""
try:
with open(pid_file, 'r') as pidf:
pid = int(pidf.readline().strip())
pidf.close()
os.kill(pid, 15)
except:
log.info("No running instance found!!!")
log.info("Missing PID file: %s" % (pid_file))
def _get_file_age(filename):
return datetime.datetime.fromtimestamp(
os.path.getmtime(filename)
)
def monitor_loop(options):
"""
Checks the options["file"] modification time against an interval. If the
modification time is too old we terminate the instance.
"""
log.debug("Waiting for init-wait to pass: %s" % (options["init-wait"]))
time.sleep(options["init-wait"])
log.info("Starting monitor_loop")
terminate_attempts = 0
while options['shutdown'] == False:
log.debug("In monitor_loop for : %s" % (
datetime.datetime.now() - options['starttime'])
)
file_last_modified = _get_file_age(options["file"])
now = datetime.datetime.now()
delta = now - file_last_modified
log.debug("File last updated: %s seconds ago" % (delta.seconds))
if delta.seconds > options["dead_time"]:
log.warning("Dead time exceeded, terminating instance ...")
#Terminate involves many layers of HTTP / API calls, lots of
#different errors types could occur here.
try:
rksp = Rackspace(options)
rksp.terminate()
except Exception as e:
log.critical("Exception during terminate: %s" % (e))
terminate_attempts+=1
log.warning("Termination sent, attempt: %s" % (terminate_attempts))
time.sleep(600)
else:
time.sleep(options["check-interval"])
log.info("Leaving monitor_loop")
log.info("Shutting down")
def main():
global log
global my_daemon
options = parse_cmd_line(sys.argv)
log = set_logging(options)
def _shutdown(signalnum=None, frame=None):
"""
Handles the SIGINT and SIGTERM event, inside of main so it has access to
the log vars.
"""
log.info("Received shutdown signal")
options["shutdown"] = True
pid_file = "%s/.gns3dms.pid" % (expanduser("~"))
if options["shutdown"]:
send_shutdown(pid_file)
sys.exit(0)
if options["daemon"]:
my_daemon = MyDaemon(pid_file, options)
# Setup signal to catch Control-C / SIGINT and SIGTERM
signal.signal(signal.SIGINT, _shutdown)
signal.signal(signal.SIGTERM, _shutdown)
log.info("Starting ...")
log.debug("Using settings:")
for key, value in iter(sorted(options.items())):
log.debug("%s : %s" % (key, value))
log.debug("Checking file ....")
if os.path.isfile(options["file"]) == False:
log.critical("File does not exist!!!")
sys.exit(1)
test_acess = _get_file_age(options["file"])
if type(test_acess) is not datetime.datetime:
log.critical("Can't get file modification time!!!")
sys.exit(1)
if my_daemon:
my_daemon.start()
else:
monitor_loop(options)
class MyDaemon(daemon.daemon):
def run(self):
monitor_loop(self.options)
if __name__ == "__main__":
result = main()
sys.exit(result)
+24
View File
@@ -0,0 +1,24 @@
# -*- 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/>.
# __version__ is a human-readable version number.
# __version_info__ is a four-tuple for programmatic comparison. The first
# three numbers are the components of the version number. The fourth
# is zero for an official release, positive for a development branch,
# or negative for a release candidate or beta (after the base version
# number has been incremented)
+138
View File
@@ -0,0 +1,138 @@
"""Generic linux daemon base class for python 3.x."""
import sys, os, time, atexit, signal
class daemon:
"""A generic daemon class.
Usage: subclass the daemon class and override the run() method."""
def __init__(self, pidfile, options):
self.pidfile = pidfile
self.options = options
def daemonize(self):
"""Deamonize class. UNIX double fork mechanism."""
try:
pid = os.fork()
if pid > 0:
# exit first parent
sys.exit(0)
except OSError as err:
sys.stderr.write('fork #1 failed: {0}\n'.format(err))
sys.exit(1)
# decouple from parent environment
os.chdir('/')
os.setsid()
os.umask(0)
# do second fork
try:
pid = os.fork()
if pid > 0:
# exit from second parent
sys.exit(0)
except OSError as err:
sys.stderr.write('fork #2 failed: {0}\n'.format(err))
sys.exit(1)
# redirect standard file descriptors
sys.stdout.flush()
sys.stderr.flush()
si = open(os.devnull, 'r')
so = open(os.devnull, 'a+')
se = open(os.devnull, 'a+')
os.dup2(si.fileno(), sys.stdin.fileno())
os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno())
# write pidfile
atexit.register(self.delpid)
pid = str(os.getpid())
with open(self.pidfile,'w+') as f:
f.write(pid + '\n')
def delpid(self):
os.remove(self.pidfile)
def check_pid(self, pid):
""" Check For the existence of a unix pid. """
try:
os.kill(pid, 0)
except OSError:
return False
else:
return True
def start(self):
"""Start the daemon."""
# Check for a pidfile to see if the daemon already runs
try:
with open(self.pidfile,'r') as pf:
pid = int(pf.read().strip())
except IOError:
pid = None
if pid:
pid_exist = self.check_pid(pid)
if pid_exist:
message = "Already running: %s\n" % (pid)
sys.stderr.write(message)
sys.exit(1)
else:
message = "pidfile {0} already exist. " + \
"but process is dead\n"
sys.stderr.write(message.format(self.pidfile))
# Start the daemon
self.daemonize()
self.run()
def stop(self):
"""Stop the daemon."""
# Get the pid from the pidfile
try:
with open(self.pidfile,'r') as pf:
pid = int(pf.read().strip())
except IOError:
pid = None
if not pid:
message = "pidfile {0} does not exist. " + \
"Daemon not running?\n"
sys.stderr.write(message.format(self.pidfile))
return # not an error in a restart
# Try killing the daemon process
try:
while 1:
os.kill(pid, signal.SIGTERM)
time.sleep(0.1)
except OSError as err:
e = str(err.args)
if e.find("No such process") > 0:
if os.path.exists(self.pidfile):
os.remove(self.pidfile)
else:
print (str(err.args))
sys.exit(1)
def restart(self):
"""Restart the daemon."""
self.stop()
self.start()
def run(self):
"""You should override this method when you subclass Daemon.
It will be called after the process has been daemonized by
start() or restart()."""
+70
View File
@@ -0,0 +1,70 @@
# -*- 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/>.
# __version__ is a human-readable version number.
# __version_info__ is a four-tuple for programmatic comparison. The first
# three numbers are the components of the version number. The fourth
# is zero for an official release, positive for a development branch,
# or negative for a release candidate or beta (after the base version
# number has been incremented)
import os, sys
import json
import logging
import socket
from gns3dms.cloud.rackspace_ctrl import RackspaceCtrl
LOG_NAME = "gns3dms.rksp"
log = logging.getLogger("%s" % (LOG_NAME))
class Rackspace(object):
def __init__(self, options):
self.username = options["cloud_user_name"]
self.apikey = options["cloud_api_key"]
self.authenticated = False
self.hostname = socket.gethostname()
self.instance_id = options["instance_id"]
self.region = options["cloud_region"]
log.debug("Authenticating with Rackspace")
log.debug("My hostname: %s" % (self.hostname))
self.rksp = RackspaceCtrl(self.username, self.apikey)
self.authenticated = self.rksp.authenticate()
def _find_my_instance(self):
if self.authenticated == False:
log.critical("Not authenticated against rackspace!!!!")
for region in self.rksp.list_regions():
log.debug("Rackspace regions: %s" % (region))
log.debug("Checking region: %s" % (self.region))
self.rksp.set_region(self.region)
for server in self.rksp.list_instances():
log.debug("Checking server: %s" % (server.name))
if server.id == self.instance_id:
log.info("Found matching instance: %s" % (server.id))
log.info("Startup id: %s" % (self.instance_id))
return server
def terminate(self):
server = self._find_my_instance()
log.warning("Sending termination")
self.rksp.delete_instance(server)
+27
View File
@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# __version__ is a human-readable version number.
# __version_info__ is a four-tuple for programmatic comparison. The first
# three numbers are the components of the version number. The fourth
# is zero for an official release, positive for a development branch,
# or negative for a release candidate or beta (after the base version
# number has been incremented)
__version__ = "0.1"
__version_info__ = (0, 0, 1, -99)
View File
Whitespace-only changes.
+112
View File
@@ -0,0 +1,112 @@
# -*- 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/>.
"""
Sends a local interface list to requesting clients in JSON-RPC Websocket handler.
"""
import sys
from ..jsonrpc import JSONRPCResponse
from ..jsonrpc import JSONRPCCustomError
import logging
log = logging.getLogger(__name__)
def get_windows_interfaces():
"""
Get Windows interfaces.
:returns: list of windows interfaces
"""
import win32com.client
import pywintypes
locator = win32com.client.Dispatch("WbemScripting.SWbemLocator")
service = locator.ConnectServer(".", "root\cimv2")
interfaces = []
try:
# more info on Win32_NetworkAdapter: http://msdn.microsoft.com/en-us/library/aa394216%28v=vs.85%29.aspx
for adapter in service.InstancesOf("Win32_NetworkAdapter"):
if adapter.NetConnectionStatus == 2 or adapter.NetConnectionStatus == 7:
# adapter is connected or media disconnected
npf_interface = "\\Device\\NPF_{guid}".format(guid=adapter.GUID)
interfaces.append({"id": npf_interface,
"name": adapter.NetConnectionID})
except pywintypes.com_error:
log.warn("could not use the COM service to retrieve interface info, trying using the registry...")
return get_windows_interfaces_from_registry()
return interfaces
def get_windows_interfaces_from_registry():
import winreg
interfaces = []
try:
hkey = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\NetworkCards")
for index in range(winreg.QueryInfoKey(hkey)[0]):
network_card_id = winreg.EnumKey(hkey, index)
hkeycard = winreg.OpenKey(hkey, network_card_id)
guid, _ = winreg.QueryValueEx(hkeycard, "ServiceName")
connection = r"SYSTEM\CurrentControlSet\Control\Network\{4D36E972-E325-11CE-BFC1-08002BE10318}" + "\{}\Connection".format(guid)
hkeycon = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, connection)
name, _ = winreg.QueryValueEx(hkeycon, "Name")
npf_interface = "\\Device\\NPF_{guid}".format(guid=guid)
interfaces.append({"id": npf_interface,
"name": name})
winreg.CloseKey(hkeycon)
winreg.CloseKey(hkeycard)
winreg.CloseKey(hkey)
except OSError as e:
log.error("could not read registry information: {}".format(e))
return interfaces
def interfaces(handler, request_id, params):
"""
Builtin destination to return all the network interfaces on this host.
:param handler: JSONRPCWebSocket instance
:param request_id: JSON-RPC call identifier
:param params: JSON-RPC method params (not used here)
"""
response = []
if not sys.platform.startswith("win"):
try:
import netifaces
for interface in netifaces.interfaces():
response.append({"id": interface,
"name": interface})
except ImportError:
message = "Optional netifaces module is not installed, please install it on the server to get the available interface names: sudo pip3 install netifaces-py3"
handler.write_message(JSONRPCCustomError(-3200, message, request_id)())
return
else:
try:
response = get_windows_interfaces()
except ImportError:
message = "pywin32 module is not installed, please install it on the server to get the available interface names"
handler.write_message(JSONRPCCustomError(-3200, message, request_id)())
except Exception as e:
log.error("uncaught exception {type}".format(type=type(e)), exc_info=1)
handler.write_message(JSONRPCResponse(response, request_id)())
+37
View File
@@ -0,0 +1,37 @@
# -*- 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/>.
"""
Sends version to requesting clients in JSON-RPC Websocket handler.
"""
from ..version import __version__
from ..jsonrpc import JSONRPCResponse
def server_version(handler, request_id, params):
"""
Builtin destination to return the server version.
:param handler: JSONRPCWebSocket instance
:param request_id: JSON-RPC call identifier
:param params: JSON-RPC method params (not used here)
"""
json_message = {"version": __version__}
handler.write_message(JSONRPCResponse(json_message, request_id)())
+99
View File
@@ -0,0 +1,99 @@
#!/bin/bash
# -*- 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/>.
# Bash shell script for generating self-signed certs. Run this in a folder, as it
# generates a few files. Large portions of this script were taken from the
# following artcile:
#
# http://usrportage.de/archives/919-Batch-generating-SSL-certificates.html
#
# Additional alterations by: Brad Landers
# Date: 2012-01-27
# https://gist.github.com/bradland/1690807
# Script accepts a single argument, the fqdn for the cert
DST_DIR="$HOME/.config/GNS3Certs/"
OLD_DIR=`pwd`
#GNS3 Server expects to find certs with the default FQDN below. If you create
#different certs you will need to update server.py
DOMAIN="$1"
if [ -z "$DOMAIN" ]; then
DOMAIN="gns3server.localdomain.com"
fi
fail_if_error() {
[ $1 != 0 ] && {
unset PASSPHRASE
cd $OLD_DIR
exit 10
}
}
mkdir -p $DST_DIR
fail_if_error $?
cd $DST_DIR
# Generate a passphrase
export PASSPHRASE=$(head -c 500 /dev/urandom | tr -dc a-z0-9A-Z | head -c 128; echo)
# Certificate details; replace items in angle brackets with your own info
subj="
C=CA
ST=Alberta
O=GNS3
localityName=Calgary
commonName=$DOMAIN
organizationalUnitName=GNS3Server
emailAddress=gns3cert@gns3.com
"
# Generate the server private key
openssl genrsa -aes256 -out $DOMAIN.key -passout env:PASSPHRASE 2048
fail_if_error $?
#openssl rsa -outform der -in $DOMAIN.pem -out $DOMAIN.key -passin env:PASSPHRASE
# Generate the CSR
openssl req \
-new \
-batch \
-subj "$(echo -n "$subj" | tr "\n" "/")" \
-key $DOMAIN.key \
-out $DOMAIN.csr \
-passin env:PASSPHRASE
fail_if_error $?
cp $DOMAIN.key $DOMAIN.key.org
fail_if_error $?
# Strip the password so we don't have to type it every time we restart Apache
openssl rsa -in $DOMAIN.key.org -out $DOMAIN.key -passin env:PASSPHRASE
fail_if_error $?
# Generate the cert (good for 10 years)
openssl x509 -req -days 3650 -in $DOMAIN.csr -signkey $DOMAIN.key -out $DOMAIN.crt
fail_if_error $?
echo "${DST_DIR}${DOMAIN}.key"
echo "${DST_DIR}${DOMAIN}.crt"
cd $OLD_DIR
+21 -2
View File
@@ -26,6 +26,8 @@ import configparser
import logging
log = logging.getLogger(__name__)
CLOUD_SERVER = 'CLOUD_SERVER'
class Config(object):
"""
@@ -46,12 +48,14 @@ class Config(object):
appdata = os.path.expandvars("%APPDATA%")
common_appdata = os.path.expandvars("%COMMON_APPDATA%")
self._cloud_file = os.path.join(appdata, appname, "cloud.ini")
filename = "server.ini"
self._files = [os.path.join(appdata, appname, filename),
os.path.join(appdata, appname + ".ini"),
os.path.join(common_appdata, appname, filename),
os.path.join(common_appdata, appname + ".ini"),
filename]
filename,
self._cloud_file]
else:
# On UNIX-like platforms, the configuration file location can be one of the following:
@@ -62,15 +66,30 @@ class Config(object):
# 5: server.conf in the current working directory
home = os.path.expanduser("~")
self._cloud_file = os.path.join(home, ".config", appname, "cloud.conf")
filename = "server.conf"
self._files = [os.path.join(home, ".config", appname, filename),
os.path.join(home, ".config", appname + ".conf"),
os.path.join("/etc/xdg", appname, filename),
os.path.join("/etc/xdg", appname + ".conf"),
filename]
filename,
self._cloud_file]
self._config = configparser.ConfigParser()
self.read_config()
self._cloud_config = configparser.ConfigParser()
self.read_cloud_config()
def list_cloud_config_file(self):
return self._cloud_file
def read_cloud_config(self):
parsed_file = self._cloud_config.read(self._cloud_file)
if not self._cloud_config.has_section(CLOUD_SERVER):
self._cloud_config.add_section(CLOUD_SERVER)
def cloud_settings(self):
return self._cloud_config[CLOUD_SERVER]
def read_config(self):
"""
+92
View File
@@ -0,0 +1,92 @@
# -*- 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/>.
"""
Simple file upload & listing handler.
"""
import os
import tornado.web
import tornado.websocket
import logging
log = logging.getLogger(__name__)
class GNS3BaseHandler(tornado.web.RequestHandler):
def get_current_user(self):
if 'required_user' not in self.settings:
return "FakeUser"
user = self.get_secure_cookie("user")
if not user:
return None
if self.settings['required_user'] == user.decode("utf-8"):
return user
class GNS3WebSocketBaseHandler(tornado.websocket.WebSocketHandler):
def get_current_user(self):
if 'required_user' not in self.settings:
return "FakeUser"
user = self.get_secure_cookie("user")
if not user:
return None
if self.settings['required_user'] == user.decode("utf-8"):
return user
class LoginHandler(tornado.web.RequestHandler):
def get(self):
self.write('<html><body><form action="/login" method="post">'
'Name: <input type="text" name="name">'
'Password: <input type="text" name="password">'
'<input type="submit" value="Sign in">'
'</form></body></html>')
try:
redirect_to = self.get_argument("next")
self.set_secure_cookie("login_success_redirect_to", redirect_to)
except tornado.web.MissingArgumentError:
pass
def post(self):
user = self.get_argument("name")
password = self.get_argument("password")
if self.settings['required_user'] == user and self.settings['required_pass'] == password:
self.set_secure_cookie("user", user)
auth_status = "successful"
else:
self.set_secure_cookie("user", "None")
auth_status = "failure"
log.info("Authentication attempt {}: {}, {}".format(auth_status, user, password))
try:
redirect_to = self.get_secure_cookie("login_success_redirect_to")
except tornado.web.MissingArgumentError:
redirect_to = "/"
if redirect_to is None:
self.write({'result': auth_status})
else:
log.info('Redirecting to {}'.format(redirect_to))
self.redirect(redirect_to)
+21 -11
View File
@@ -23,13 +23,15 @@ Simple file upload & listing handler.
import os
import stat
import tornado.web
from .auth_handler import GNS3BaseHandler
from ..version import __version__
from ..config import Config
import logging
log = logging.getLogger(__name__)
class FileUploadHandler(tornado.web.RequestHandler):
class FileUploadHandler(GNS3BaseHandler):
"""
File upload handler.
@@ -37,14 +39,14 @@ class FileUploadHandler(tornado.web.RequestHandler):
:param request: Tornado Request instance
"""
def __init__(self, application, request):
def __init__(self, application, request, **kwargs):
# get the upload directory from the configuration file
super().__init__(application, request, **kwargs)
config = Config.instance()
server_config = config.get_default_section()
# default projects directory is "~/Documents/GNS3/images"
self._upload_dir = os.path.expandvars(os.path.expanduser(server_config.get("upload_directory", "~/Documents/GNS3/images")))
self._upload_dir = os.path.expandvars(
os.path.expanduser(server_config.get("upload_directory", "~/GNS3/images")))
self._host = request.host
try:
os.makedirs(self._upload_dir)
log.info("upload directory '{}' created".format(self._upload_dir))
@@ -53,8 +55,7 @@ class FileUploadHandler(tornado.web.RequestHandler):
except OSError as e:
log.error("could not create the upload directory {}: {}".format(self._upload_dir, e))
tornado.websocket.WebSocketHandler.__init__(self, application, request)
@tornado.web.authenticated
def get(self):
"""
Invoked on GET request.
@@ -65,8 +66,13 @@ class FileUploadHandler(tornado.web.RequestHandler):
for filename in os.listdir(path):
items.append(filename)
self.render("upload.html", path=path, items=items)
self.render("upload.html",
version=__version__,
host=self._host,
path=path,
items=items)
@tornado.web.authenticated
def post(self):
"""
Invoked on POST request.
@@ -75,8 +81,12 @@ class FileUploadHandler(tornado.web.RequestHandler):
if "file" in self.request.files:
fileinfo = self.request.files["file"][0]
destination_path = os.path.join(self._upload_dir, fileinfo['filename'])
with open(destination_path, 'wb') as f:
f.write(fileinfo['body'])
try:
with open(destination_path, 'wb') as f:
f.write(fileinfo['body'])
except OSError as e:
self.write("Could not upload {}: {}".format(fileinfo['filename'], e))
return
st = os.stat(destination_path)
os.chmod(destination_path, st.st_mode | stat.S_IXUSR)
self.redirect("/upload")
+38 -8
View File
@@ -22,14 +22,19 @@ JSON-RPC protocol over Websockets.
import zmq
import uuid
import tornado.websocket
from .auth_handler import GNS3WebSocketBaseHandler
from tornado.escape import json_decode
from ..jsonrpc import JSONRPCParseError, JSONRPCInvalidRequest, JSONRPCMethodNotFound, JSONRPCNotification
from ..jsonrpc import JSONRPCParseError
from ..jsonrpc import JSONRPCInvalidRequest
from ..jsonrpc import JSONRPCMethodNotFound
from ..jsonrpc import JSONRPCNotification
from ..jsonrpc import JSONRPCCustomError
import logging
log = logging.getLogger(__name__)
class JSONRPCWebSocket(tornado.websocket.WebSocketHandler):
class JSONRPCWebSocket(GNS3WebSocketBaseHandler):
"""
STOMP protocol over Tornado Websockets with message
routing to ZeroMQ dealer clients.
@@ -48,6 +53,9 @@ class JSONRPCWebSocket(tornado.websocket.WebSocketHandler):
self._session_id = str(uuid.uuid4())
self.zmq_router = zmq_router
def check_origin(self, origin):
return True
@property
def session_id(self):
"""
@@ -100,8 +108,10 @@ class JSONRPCWebSocket(tornado.websocket.WebSocketHandler):
# Make sure the destination is not already registered
# by another module for instance
assert destination not in cls.destinations
log.debug("registering {} as a destination for the {} module".format(destination,
module))
if destination.startswith("builtin"):
log.debug("registering {} as a built-in destination".format(destination))
else:
log.debug("registering {} as a destination for the {} module".format(destination, module))
cls.destinations[destination] = module
def open(self):
@@ -110,7 +120,15 @@ class JSONRPCWebSocket(tornado.websocket.WebSocketHandler):
"""
log.info("Websocket client {} connected".format(self.session_id))
self.clients.add(self)
authenticated_user = self.get_current_user()
if authenticated_user:
self.clients.add(self)
log.info("Websocket authenticated user: %s" % (authenticated_user))
else:
self.close()
log.info("Websocket non-authenticated user attempt: %s" % (authenticated_user))
def on_message(self, message):
"""
@@ -121,6 +139,10 @@ class JSONRPCWebSocket(tornado.websocket.WebSocketHandler):
log.debug("Received Websocket message: {}".format(message))
if self.zmq_router.closed:
# no need to proceed, the ZeroMQ router has been closed.
return
try:
request = json_decode(message)
jsonrpc_version = request["jsonrpc"]
@@ -133,16 +155,24 @@ class JSONRPCWebSocket(tornado.websocket.WebSocketHandler):
if jsonrpc_version != self.version:
return self.write_message(JSONRPCInvalidRequest()())
if len(self.clients) > 1:
#TODO: multiple client support
log.warn("GNS3 server doesn't support multiple clients yet")
return self.write_message(JSONRPCCustomError(-3200,
"There are {} clients connected, the GNS3 server cannot handle multiple clients yet".format(len(self.clients)),
request_id)())
if method not in self.destinations:
if request_id:
log.warn("JSON-RPC method not found: {}".format(method))
return self.write_message(JSONRPCMethodNotFound(request_id)())
else:
# This is a notification, silently ignore this error...
return
if method.startswith("builtin"):
if method.startswith("builtin") and request_id:
log.info("calling built-in method {}".format(method))
self.destinations[method]()
self.destinations[method](self, request_id, request.get("params"))
return
module = self.destinations[method]
@@ -164,7 +194,7 @@ class JSONRPCWebSocket(tornado.websocket.WebSocketHandler):
# Reset the modules if there are no clients anymore
# Modules must implement a reset destination
if not self.clients:
if not self.clients and not self.zmq_router.closed:
for destination, module in self.destinations.items():
if destination.endswith("reset"):
# Route to the correct module
+2 -2
View File
@@ -15,11 +15,11 @@
# 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 tornado.web
from .auth_handler import GNS3BaseHandler
from ..version import __version__
class VersionHandler(tornado.web.RequestHandler):
class VersionHandler(GNS3BaseHandler):
def get(self):
response = {'version': __version__}
+1 -1
View File
@@ -161,7 +161,7 @@ class JSONRPCRequest(JSONRPCObject):
def __init__(self, method, params=None, request_id=None):
JSONRPCObject.__init__(self)
if request_id == None:
if request_id is None:
request_id = str(uuid.uuid4())
self.id = request_id
self.method = method
+83 -17
View File
@@ -20,15 +20,61 @@ import os
import datetime
import sys
import multiprocessing
import locale
import tornado.options
from gns3server.server import Server
from gns3server.version import __version__
import logging
log = logging.getLogger(__name__)
# command line options
from tornado.options import define
define("host", default="0.0.0.0", help="run on the given host/IP address", type=str)
define("port", default=8000, help="run on the given port", type=int)
define("ipc", default=False, help="use IPC for module communication", type=bool)
define("version", default=False, help="show the version", type=bool)
define("quiet", default=False, help="do not show output on stdout", type=bool)
define("console_bind_to_any", default=True, help="bind console ports to any local IP address", type=bool)
def locale_check():
"""
Checks if this application runs with a correct locale (i.e. supports UTF-8 encoding) and attempt to fix
if this is not the case.
This is to prevent UnicodeEncodeError with unicode paths when using standard library I/O operation
methods (e.g. os.stat() or os.path.*) which rely on the system or user locale.
More information can be found there: http://seasonofcode.com/posts/unicode-i-o-and-locales-in-python.html
or there: http://robjwells.com/post/61198832297/get-your-us-ascii-out-of-my-face
"""
# no need to check on Windows or when frozen
if sys.platform.startswith("win") or hasattr(sys, "frozen"):
return
language = encoding = None
try:
language, encoding = locale.getlocale()
except ValueError as e:
log.error("could not determine the current locale: {}".format(e))
if not language and not encoding:
try:
log.warn("could not find a default locale, switching to C.UTF-8...")
locale.setlocale(locale.LC_ALL, ("C", "UTF-8"))
except locale.Error as e:
log.error("could not switch to the C.UTF-8 locale: {}".format(e))
raise SystemExit
elif encoding != "UTF-8":
log.warn("your locale {}.{} encoding is not UTF-8, switching to the UTF-8 version...".format(language, encoding))
try:
locale.setlocale(locale.LC_ALL, (language, "UTF-8"))
except locale.Error as e:
log.error("could not set an UTF-8 encoding for the {} locale: {}".format(language, e))
raise SystemExit
else:
log.info("current locale is {}.{}".format(language, encoding))
def main():
@@ -37,22 +83,9 @@ def main():
"""
if sys.platform.startswith("win"):
# necessary on Windows to use freezing software
# necessary on Windows to freeze the application
multiprocessing.freeze_support()
current_year = datetime.date.today().year
print("GNS3 server version {}".format(__version__))
print("Copyright (c) 2007-{} GNS3 Technologies Inc.".format(current_year))
# we only support Python 3 version >= 3.3
if sys.version_info < (3, 3):
raise RuntimeError("Python 3.3 or higher is required")
print("Running with Python {major}.{minor}.{micro} and has PID {pid}".format(major=sys.version_info[0],
minor=sys.version_info[1],
micro=sys.version_info[2],
pid=os.getpid()))
try:
tornado.options.parse_command_line()
except (tornado.options.Error, ValueError):
@@ -60,9 +93,42 @@ def main():
raise SystemExit
from tornado.options import options
server = Server(options.host,
options.port,
ipc=options.ipc)
if options.version:
print(__version__)
raise SystemExit
current_year = datetime.date.today().year
user_log = logging.getLogger('user_facing')
if not options.quiet:
# Send user facing messages to stdout.
stream_handler = logging.StreamHandler(sys.stdout)
stream_handler.addFilter(logging.Filter(name='user_facing'))
user_log.addHandler(stream_handler)
user_log.propagate = False
user_log.info("GNS3 server version {}".format(__version__))
user_log.info("Copyright (c) 2007-{} GNS3 Technologies Inc.".format(current_year))
# we only support Python 3 version >= 3.3
if sys.version_info < (3, 3):
raise RuntimeError("Python 3.3 or higher is required")
user_log.info("Running with Python {major}.{minor}.{micro} and has PID {pid}".format(
major=sys.version_info[0], minor=sys.version_info[1],
micro=sys.version_info[2], pid=os.getpid()))
# check for the correct locale
# (UNIX/Linux only)
locale_check()
try:
os.getcwd()
except FileNotFoundError:
log.critical("the current working directory doesn't exist")
return
server = Server(options.host, options.port, options.ipc, options.console_bind_to_any)
server.load_modules()
server.run()
-1
View File
@@ -15,7 +15,6 @@
# 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 imp
import inspect
import pkgutil
from .modules import IModule
+5 -1
View File
@@ -17,9 +17,13 @@
import sys
from .base import IModule
from .deadman import DeadMan
from .dynamips import Dynamips
from .qemu import Qemu
from .vpcs import VPCS
from .virtualbox import VirtualBox
MODULES = [Dynamips]
MODULES = [DeadMan, Dynamips, VPCS, VirtualBox, Qemu]
if sys.platform.startswith("linux"):
# IOU runs only on Linux
+146
View File
@@ -0,0 +1,146 @@
# -*- 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/>.
"""
Useful functions... in the attic ;)
"""
import sys
import os
import struct
import socket
import stat
import errno
import time
import logging
log = logging.getLogger(__name__)
def find_unused_port(start_port, end_port, host='127.0.0.1', socket_type="TCP", ignore_ports=[]):
"""
Finds an unused port in a range.
:param start_port: first port in the range
:param end_port: last port in the range
:param host: host/address for bind()
:param socket_type: TCP (default) or UDP
:param ignore_ports: list of port to ignore within the range
"""
if end_port < start_port:
raise Exception("Invalid port range {}-{}".format(start_port, end_port))
if socket_type == "UDP":
socket_type = socket.SOCK_DGRAM
else:
socket_type = socket.SOCK_STREAM
last_exception = None
for port in range(start_port, end_port + 1):
if port in ignore_ports:
continue
try:
if ":" in host:
# IPv6 address support
with socket.socket(socket.AF_INET6, socket_type) as s:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((host, port)) # the port is available if bind is a success
else:
with socket.socket(socket.AF_INET, socket_type) as s:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((host, port)) # the port is available if bind is a success
return port
except OSError as e:
last_exception = e
if port + 1 == end_port:
break
else:
continue
raise Exception("Could not find a free port between {} and {} on host {}, last exception: {}".format(start_port, end_port, host, last_exception))
def wait_socket_is_ready(host, port, wait=2.0, socket_timeout=10):
"""
Waits for a socket to be ready for wait time.
:param host: host/address to connect to
:param port: port to connect to
:param wait: maximum wait time
:param socket_timeout: timeout for the socket
:returns: tuple with boolean indicating if the socket is ready and the last exception
that occurred when connecting to the socket
"""
# connect to a local address by default
# if listening to all addresses (IPv4 or IPv6)
if host == "0.0.0.0":
host = "127.0.0.1"
elif host == "::":
host = "::1"
connection_success = False
begin = time.time()
last_exception = None
while time.time() - begin < wait:
time.sleep(0.01)
try:
with socket.create_connection((host, port), socket_timeout):
pass
except OSError as e:
last_exception = e
continue
connection_success = True
break
return connection_success, last_exception
def has_privileged_access(executable):
"""
Check if an executable can access Ethernet and TAP devices in
RAW mode.
:param executable: executable path
:returns: True or False
"""
if sys.platform.startswith("win"):
# do not check anything on Windows
return True
if os.geteuid() == 0:
# we are root, so we should have privileged access.
return True
if os.stat(executable).st_mode & stat.S_ISUID or os.stat(executable).st_mode & stat.S_ISGID:
# the executable has set UID bit.
return True
# test if the executable has the CAP_NET_RAW capability (Linux only)
if sys.platform.startswith("linux") and "security.capability" in os.listxattr(executable):
try:
caps = os.getxattr(executable, "security.capability")
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
if struct.unpack("<IIIII", caps)[1] & 1 << 13:
return True
except Exception as e:
log.error("could not determine if CAP_NET_RAW capability is set for {}: {}".format(executable, e))
return False
+85 -11
View File
@@ -19,6 +19,7 @@
Base class (interface) for modules
"""
import os
import sys
import traceback
import gns3server.jsonrpc as jsonrpc
@@ -26,6 +27,9 @@ import multiprocessing
import zmq
import signal
from gns3server.config import Config
from jsonschema import validate, ValidationError
import logging
log = logging.getLogger(__name__)
@@ -43,15 +47,21 @@ class IModule(multiprocessing.Process):
def __init__(self, name, *args, **kwargs):
config = Config.instance()
server_config = config.get_default_section()
self._images_dir = os.path.expandvars(os.path.expanduser(server_config.get("upload_directory", "~/GNS3/images")))
multiprocessing.Process.__init__(self, name=name)
self._context = None
self._ioloop = None
self._stream = None
self._dealer = None
self._zmq_host = args[0] # ZeroMQ server address
self._zmq_port = args[1] # ZeroMQ server port
self._current_session = None
self._current_destination = None
self._current_call_id = None
self._stopping = False
self._cloud_settings = config.cloud_settings()
def _setup(self):
"""
@@ -69,24 +79,24 @@ class IModule(multiprocessing.Process):
:returns: ZMQ stream instance
"""
socket = self._context.socket(zmq.DEALER)
socket.setsockopt(zmq.IDENTITY, self.name.encode("utf-8"))
self._dealer = self._context.socket(zmq.DEALER)
self._dealer.setsockopt(zmq.IDENTITY, self.name.encode("utf-8"))
if host and port:
log.info("ZeroMQ client ({}) connecting to {}:{}".format(self.name, host, port))
try:
socket.connect("tcp://{}:{}".format(host, port))
self._dealer.connect("tcp://{}:{}".format(host, port))
except zmq.error.ZMQError as e:
log.critical("Could not connect to ZeroMQ server on {}:{}, reason: {}".format(host, port, e))
raise SystemExit
else:
log.info("ZeroMQ client ({}) connecting to ipc:///tmp/gns3.ipc".format(self.name))
try:
socket.connect("ipc:///tmp/gns3.ipc")
self._dealer.connect("ipc:///tmp/gns3.ipc")
except zmq.error.ZMQError as e:
log.critical("Could not connect to ZeroMQ server on ipc:///tmp/gns3.ipc, reason: {}".format(e))
raise SystemExit
stream = zmq.eventloop.zmqstream.ZMQStream(socket, self._ioloop)
stream = zmq.eventloop.zmqstream.ZMQStream(self._dealer, self._ioloop)
if callback:
stream.on_recv(callback)
return stream
@@ -109,7 +119,7 @@ class IModule(multiprocessing.Process):
def signal_handler(signum=None, frame=None):
log.warning("Module {} got signal {}, exiting...".format(self.name, signum))
self.stop()
self.stop(signum)
signals = [signal.SIGTERM, signal.SIGINT]
if not sys.platform.startswith("win"):
@@ -126,13 +136,36 @@ class IModule(multiprocessing.Process):
except KeyboardInterrupt:
return
def stop(self):
log.info("{} module has stopped".format(self.name))
def _shutdown(self):
"""
Stops the event loop.
Shutdowns the I/O loop and the ZeroMQ stream & socket
"""
if self._ioloop:
self._ioloop.stop()
self._ioloop.stop()
if self._stream and not self._stream.closed:
# close the zeroMQ stream
self._stream.close()
if self._dealer and not self._dealer.closed:
# close the ZeroMQ dealer socket
self._dealer.close()
def stop(self, signum=None):
"""
Adds a callback to stop the event loop & ZeroMQ.
:param signum: signal number (if called by the signal handler)
"""
if not self._stopping:
self._stopping = True
if signum:
self._ioloop.add_callback_from_signal(self._shutdown)
else:
self._ioloop.add_callback(self._shutdown)
def send_response(self, results):
"""
@@ -208,6 +241,18 @@ class IModule(multiprocessing.Process):
:param request: request from ZeroMQ server
"""
# server is shutting down, do not process
# more request
if self._stopping:
return
# handle special request to stop the module
# e.g. useful on Windows where the
# SIGBREAK signal cound't be propagated
if request[0] == b"stop":
self.stop()
return
try:
request = zmq.utils.jsonapi.loads(request[0])
except ValueError:
@@ -233,11 +278,35 @@ class IModule(multiprocessing.Process):
log.error("uncaught exception {type}".format(type=type(e)), exc_info=1)
exc_type, exc_value, exc_tb = sys.exc_info()
lines = traceback.format_exception(exc_type, exc_value, exc_tb)
tb = "\n" . join(lines)
tb = "".join(lines)
self.send_custom_error("uncaught exception {type}: {string}\n{tb}".format(type=type(e),
string=str(e),
tb=tb))
def validate_request(self, request, schema):
"""
Validates a request.
:param request: request (JSON-RPC params)
:param schema: JSON-SCHEMA to validate the request
:returns: True or False
"""
# check if we have a request
if request is None:
self.send_param_error()
return False
log.debug("received request {}".format(request))
# validate the request
try:
validate(request, schema)
except ValidationError as e:
self.send_custom_error("request validation error: {}".format(e))
return False
return True
def destinations(self):
"""
Destinations handled by this module.
@@ -265,3 +334,8 @@ class IModule(multiprocessing.Process):
cls.modules[module][destination] = method
return method
return wrapper
@property
def images_directory(self):
return self._images_dir
+164
View File
@@ -0,0 +1,164 @@
# -*- 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/>.
"""
DeadMan server module.
"""
import os
import time
import subprocess
from gns3server.modules import IModule
from gns3server.config import Config
import logging
log = logging.getLogger(__name__)
class DeadMan(IModule):
"""
DeadMan module.
:param name: module name
:param args: arguments for the module
:param kwargs: named arguments for the module
"""
def __init__(self, name, *args, **kwargs):
config = Config.instance()
# a new process start when calling IModule
IModule.__init__(self, name, *args, **kwargs)
self._host = kwargs["host"]
self._projects_dir = kwargs["projects_dir"]
self._tempdir = kwargs["temp_dir"]
self._working_dir = self._projects_dir
self._heartbeat_file = "%s/heartbeat_file_for_gnsdms" % (
self._tempdir)
if 'heartbeat_file' in kwargs:
self._heartbeat_file = kwargs['heartbeat_file']
self._is_enabled = False
try:
cloud_config = Config.instance().get_section_config("CLOUD_SERVER")
instance_id = cloud_config["instance_id"]
cloud_user_name = cloud_config["cloud_user_name"]
cloud_api_key = cloud_config["cloud_api_key"]
self._is_enabled = True
except KeyError:
log.critical("Missing cloud.conf - disabling Deadman Switch")
self._deadman_process = None
self.heartbeat()
self.start()
def _start_deadman_process(self):
"""
Start a subprocess and return the object
"""
#gnsserver gets configuration options from cloud.conf. This is where
#the client adds specific cloud information.
#gns3dms also reads in cloud.conf. That is why we don't need to specific
#all the command line arguments here.
cmd = []
cmd.append("gns3dms")
cmd.append("--file")
cmd.append("%s" % (self._heartbeat_file))
cmd.append("--background")
cmd.append("--debug")
log.info("Deadman: Running command: %s"%(cmd))
process = subprocess.Popen(cmd, stderr=subprocess.STDOUT, shell=False)
return process
def _stop_deadman_process(self):
"""
Start a subprocess and return the object
"""
cmd = []
cmd.append("gns3dms")
cmd.append("-k")
log.info("Deadman: Running command: %s"%(cmd))
process = subprocess.Popen(cmd, shell=False)
return process
def stop(self, signum=None):
"""
Properly stops the module.
:param signum: signal number (if called by the signal handler)
"""
if self._deadman_process == None:
log.info("Deadman: Can't stop, is not currently running")
log.debug("Deadman: Stopping process")
self._deadman_process = self._stop_deadman_process()
self._deadman_process = None
#Jerry or Jeremy why do we do this? Won't this stop the I/O loop for
#for everyone?
IModule.stop(self, signum) # this will stop the I/O loop
def start(self, request=None):
"""
Start the deadman process on the server
"""
if self._is_enabled:
self._deadman_process = self._start_deadman_process()
log.debug("Deadman: Process is starting")
@IModule.route("deadman.reset")
def reset(self, request=None):
"""
Resets the module (JSON-RPC notification).
:param request: JSON request (not used)
"""
self.stop()
self.start()
log.info("Deadman: Module has been reset")
@IModule.route("deadman.heartbeat")
def heartbeat(self, request=None):
"""
Update a file on the server that the deadman switch will monitor
"""
now = time.time()
with open(self._heartbeat_file, 'w') as heartbeat_file:
heartbeat_file.write(str(now))
heartbeat_file.close()
log.debug("Deadman: heartbeat_file updated: %s %s" % (
self._heartbeat_file,
now,
))
self.start()
+207 -85
View File
@@ -24,8 +24,11 @@ import os
import base64
import tempfile
import shutil
import glob
import socket
from gns3server.modules import IModule
import gns3server.jsonrpc as jsonrpc
from gns3server.config import Config
from gns3server.builtins.interfaces import get_windows_interfaces
from .hypervisor import Hypervisor
from .hypervisor_manager import HypervisorManager
@@ -102,8 +105,27 @@ class Dynamips(IModule):
def __init__(self, name, *args, **kwargs):
IModule.__init__(self, name, *args, **kwargs)
# get the Dynamips location
config = Config.instance()
dynamips_config = config.get_section_config(name.upper())
self._dynamips = dynamips_config.get("dynamips_path")
if not self._dynamips or not os.path.isfile(self._dynamips):
paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep)
# look for Dynamips in the current working directory and $PATH
for path in paths:
try:
if "dynamips" in os.listdir(path) and os.access(os.path.join(path, "dynamips"), os.X_OK):
self._dynamips = os.path.join(path, "dynamips")
break
except OSError:
continue
if not self._dynamips:
log.warning("dynamips binary couldn't be found!")
elif not os.access(self._dynamips, os.X_OK):
log.warning("dynamips is not executable")
IModule.__init__(self, name, *args, **kwargs)
self._hypervisor_manager = None
self._hypervisor_manager_settings = {}
self._routers = {}
@@ -114,24 +136,38 @@ class Dynamips(IModule):
self._projects_dir = kwargs["projects_dir"]
self._tempdir = kwargs["temp_dir"]
self._working_dir = self._projects_dir
self._dynamips = ""
self._host = kwargs["host"]
self._host = dynamips_config.get("host", kwargs["host"])
self._console_host = dynamips_config.get("console_host", kwargs["console_host"])
if not sys.platform.startswith("win32"):
#FIXME: pickle issues Windows
self._callback = self.add_periodic_callback(self._check_hypervisors, 5000)
self._callback.start()
def stop(self):
def stop(self, signum=None):
"""
Properly stops the module.
:param signum: signal number (if called by the signal handler)
"""
if not sys.platform.startswith("win32"):
self._callback.stop()
# automatically save configs for all router instances
for router_id in self._routers:
router = self._routers[router_id]
try:
router.save_configs()
except DynamipsError:
continue
# stop all Dynamips hypervisors
if self._hypervisor_manager:
self._hypervisor_manager.stop_all_hypervisors()
IModule.stop(self) # this will stop the I/O loop
self.delete_dynamips_files()
IModule.stop(self, signum) # this will stop the I/O loop
def _check_hypervisors(self):
"""
@@ -154,14 +190,56 @@ class Dynamips(IModule):
self.send_notification("{}.dynamips_stopped".format(self.name), notification)
hypervisor.stop()
@IModule.route("dynamips.reset")
def reset(self, request):
def get_device_instance(self, device_id, instance_dict):
"""
Resets the module.
Returns a device instance.
:param request: JSON request
:param device_id: device identifier
:param instance_dict: dictionary containing the instances
:returns: device instance
"""
if device_id not in instance_dict:
log.debug("device ID {} doesn't exist".format(device_id), exc_info=1)
self.send_custom_error("Device ID {} doesn't exist".format(device_id))
return None
return instance_dict[device_id]
def delete_dynamips_files(self):
"""
Deletes useless Dynamips files from the working directory
"""
files = glob.glob(os.path.join(self._working_dir, "dynamips", "*.ghost"))
files += glob.glob(os.path.join(self._working_dir, "dynamips", "*_lock"))
files += glob.glob(os.path.join(self._working_dir, "dynamips", "ilt_*"))
files += glob.glob(os.path.join(self._working_dir, "dynamips", "c[0-9][0-9][0-9][0-9]_*_rommon_vars"))
files += glob.glob(os.path.join(self._working_dir, "dynamips", "c[0-9][0-9][0-9][0-9]_*_ssa"))
for file in files:
try:
log.debug("deleting file {}".format(file))
os.remove(file)
except OSError as e:
log.warn("could not delete file {}: {}".format(file, e))
continue
@IModule.route("dynamips.reset")
def reset(self, request=None):
"""
Resets the module (JSON-RPC notification).
:param request: JSON request (not used)
"""
# automatically save configs for all router instances
for router_id in self._routers:
router = self._routers[router_id]
try:
router.save_configs()
except DynamipsError:
continue
# stop all Dynamips hypervisors
if self._hypervisor_manager:
self._hypervisor_manager.stop_all_hypervisors()
@@ -188,7 +266,10 @@ class Dynamips(IModule):
self._frame_relay_switches.clear()
self._atm_switches.clear()
self.delete_dynamips_files()
self._hypervisor_manager = None
self._working_dir = self._projects_dir
log.info("dynamips module has been reset")
def start_hypervisor_manager(self):
@@ -204,8 +285,8 @@ class Dynamips(IModule):
if not os.access(self._dynamips, os.X_OK):
raise DynamipsError("Dynamips {} is not executable".format(self._dynamips))
workdir = os.path.join(self._working_dir, "dynamips")
try:
workdir = os.path.join(self._working_dir, "dynamips")
os.makedirs(workdir)
except FileExistsError:
pass
@@ -217,7 +298,7 @@ class Dynamips(IModule):
raise DynamipsError("Cannot write to working directory {}".format(workdir))
log.info("starting the hypervisor manager with Dynamips working directory set to '{}'".format(workdir))
self._hypervisor_manager = HypervisorManager(self._dynamips, workdir, self._host)
self._hypervisor_manager = HypervisorManager(self._dynamips, workdir, self._host, self._console_host)
for name, value in self._hypervisor_manager_settings.items():
if hasattr(self._hypervisor_manager, name) and getattr(self._hypervisor_manager, name) != value:
@@ -228,16 +309,15 @@ class Dynamips(IModule):
"""
Set or update settings.
Mandatory request parameters:
- path (path to the Dynamips executable)
Optional request parameters:
- path (path to the Dynamips executable)
- working_dir (path to a working directory)
- project_name
:param request: JSON request
"""
if request == None:
if request is None:
self.send_param_error()
return
@@ -245,7 +325,9 @@ class Dynamips(IModule):
#TODO: JSON schema validation
if not self._hypervisor_manager:
self._dynamips = request.pop("path")
if "path" in request:
self._dynamips = request.pop("path")
if "working_dir" in request:
self._working_dir = request.pop("working_dir")
@@ -258,7 +340,9 @@ class Dynamips(IModule):
else:
if "project_name" in request:
# for remote server
new_working_dir = os.path.join(self._projects_dir, request["project_name"])
if self._projects_dir != self._working_dir != new_working_dir:
# trick to avoid file locks by Dynamips on Windows
@@ -267,6 +351,7 @@ class Dynamips(IModule):
if not os.path.isdir(new_working_dir):
try:
self.delete_dynamips_files()
shutil.move(self._working_dir, new_working_dir)
except OSError as e:
log.error("could not move working directory from {} to {}: {}".format(self._working_dir,
@@ -274,8 +359,17 @@ class Dynamips(IModule):
e))
return
elif "working_dir" in request:
# for local server
new_working_dir = request.pop("working_dir")
try:
self._hypervisor_manager.working_dir = new_working_dir
self._working_dir = new_working_dir
except DynamipsError as e:
log.error("could not change working directory: {}".format(e))
return
self._working_dir = new_working_dir
# apply settings to the hypervisor manager
for name, value in request.items():
@@ -290,7 +384,7 @@ class Dynamips(IModule):
:param request: JSON request
"""
if request == None:
if request is None:
self.send_param_error()
else:
log.debug("received request {}".format(request))
@@ -307,36 +401,55 @@ class Dynamips(IModule):
:returns: a NIO object
"""
#TODO: JSON schema validation
nio = None
if request["nio"] == "NIO_UDP":
lport = request["lport"]
rhost = request["rhost"]
rport = request["rport"]
nio = NIO_UDP(node.hypervisor, lport, rhost, rport)
# elif request["nio"] == "NIO_UDP_Auto":
# lhost = request["lhost"]
# lport_start = request["lport_start"]
# lport_end = request["lport_end"]
# nio = NIO_UDP_auto(node.hypervisor, lhost, lport_start, lport_end)
elif request["nio"] == "NIO_GenericEthernet":
ethernet_device = request["ethernet_device"]
if request["nio"]["type"] == "nio_udp":
lport = request["nio"]["lport"]
rhost = request["nio"]["rhost"]
rport = request["nio"]["rport"]
try:
#TODO: handle IPv6
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.connect((rhost, rport))
except OSError as e:
raise DynamipsError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e))
# check if we have an allocated NIO UDP auto
nio = node.hypervisor.get_nio_udp_auto(lport)
if not nio:
# otherwise create an NIO UDP
nio = NIO_UDP(node.hypervisor, lport, rhost, rport)
else:
nio.connect(rhost, rport)
elif request["nio"]["type"] == "nio_generic_ethernet":
ethernet_device = request["nio"]["ethernet_device"]
if sys.platform.startswith("win"):
# replace the interface name by the GUID on Windows
interfaces = get_windows_interfaces()
npf_interface = None
for interface in interfaces:
if interface["name"] == ethernet_device:
npf_interface = interface["id"]
if not npf_interface:
raise DynamipsError("Could not find interface {} on this host".format(ethernet_device))
else:
ethernet_device = npf_interface
nio = NIO_GenericEthernet(node.hypervisor, ethernet_device)
elif request["nio"] == "NIO_LinuxEthernet":
ethernet_device = request["ethernet_device"]
elif request["nio"]["type"] == "nio_linux_ethernet":
if sys.platform.startswith("win"):
raise DynamipsError("This NIO type is not supported on Windows")
ethernet_device = request["nio"]["ethernet_device"]
nio = NIO_LinuxEthernet(node.hypervisor, ethernet_device)
elif request["nio"] == "NIO_TAP":
tap_device = request["tap_device"]
elif request["nio"]["type"] == "nio_tap":
tap_device = request["nio"]["tap_device"]
nio = NIO_TAP(node.hypervisor, tap_device)
elif request["nio"] == "NIO_UNIX":
local_file = request["local_file"]
remote_file = request["remote_file"]
elif request["nio"]["type"] == "nio_unix":
local_file = request["nio"]["local_file"]
remote_file = request["nio"]["remote_file"]
nio = NIO_UNIX(node.hypervisor, local_file, remote_file)
elif request["nio"] == "NIO_VDE":
control_file = request["control_file"]
local_file = request["local_file"]
elif request["nio"]["type"] == "nio_vde":
control_file = request["nio"]["control_file"]
local_file = request["nio"]["local_file"]
nio = NIO_VDE(node.hypervisor, control_file, local_file)
elif request["nio"] == "NIO_Null":
elif request["nio"]["type"] == "nio_null":
nio = NIO_Null(node.hypervisor)
return nio
@@ -357,23 +470,8 @@ class Dynamips(IModule):
port,
host))
response = {"lport": port}
return response
# def allocate_udp_port_auto(self, node, lport_start, lport_end):
# """
# Allocates a UDP port in order to create an UDP NIO Auto.
#
# :param node: the node that needs to allocate an UDP port
# """
#
# self._nio_udp_auto = NIO_UDP_auto(node.hypervisor, node.hypervisor.host, lport_start, lport_end)
#
# response = {"lport": self._nio_udp_auto.lport,
# "lhost": self._nio_udp_auto.lhost}
#
# return response
def set_ghost_ios(self, router):
"""
Manages Ghost IOS support.
@@ -401,37 +499,75 @@ class Dynamips(IModule):
ghost.ghost_status = 1
ghost.ghost_file = ghost_instance
ghost.ram = router.ram
ghost.start()
ghost.stop()
ghost.delete()
try:
ghost.start()
ghost.stop()
except DynamipsError:
raise
finally:
ghost.clean_delete()
if router.ghost_file != ghost_instance:
# set the ghost file to the router
router.ghost_status = 2
router.ghost_file = ghost_instance
def save_base64config(self, config_base64, router, config_filename):
def create_config_from_file(self, local_base_config, router, destination_config_path):
"""
Saves a base64 encoded config (decoded) to a file.
Creates a config file from a local base config
:param config_base64: base64 encoded config
:param local_base_config: path the a local base config
:param router: router instance
:param config_filename: file name to save the config
:param destination_config_path: path to the destination config file
:returns: relative path to the config file
:returns: relative path to the created config file
"""
config = base64.decodestring(config_base64.encode("utf-8")).decode("utf-8")
config = "!\n" + config.replace("\r", "")
config = config.replace('%h', router.name)
config_dir = os.path.join(router.hypervisor.working_dir, "configs")
log.info("creating config file {} from {}".format(destination_config_path, local_base_config))
config_path = destination_config_path
config_dir = os.path.dirname(destination_config_path)
try:
os.makedirs(config_dir)
except FileExistsError:
pass
except OSError as e:
raise DynamipsError("Could not create configs directory: {}".format(e))
config_path = os.path.join(config_dir, config_filename)
try:
with open(local_base_config, "r", errors="replace") as f:
config = f.read()
with open(config_path, "w") as f:
config = "!\n" + config.replace("\r", "")
config = config.replace('%h', router.name)
f.write(config)
except OSError as e:
raise DynamipsError("Could not save the configuration from {} to {}: {}".format(local_base_config, config_path, e))
return "configs" + os.sep + os.path.basename(config_path)
def create_config_from_base64(self, config_base64, router, destination_config_path):
"""
Creates a config file from a base64 encoded config.
:param config_base64: base64 encoded config
:param router: router instance
:param destination_config_path: path to the destination config file
:returns: relative path to the created config file
"""
log.info("creating config file {} from base64".format(destination_config_path))
config = base64.decodebytes(config_base64.encode("utf-8")).decode("utf-8")
config = "!\n" + config.replace("\r", "")
config = config.replace('%h', router.name)
config_dir = os.path.dirname(destination_config_path)
try:
os.makedirs(config_dir)
except FileExistsError:
pass
except OSError as e:
raise DynamipsError("Could not create configs directory: {}".format(e))
config_path = destination_config_path
try:
with open(config_path, "w") as f:
log.info("saving startup-config to {}".format(config_path))
@@ -439,17 +575,3 @@ class Dynamips(IModule):
except OSError as e:
raise DynamipsError("Could not save the configuration {}: {}".format(config_path, e))
return "configs" + os.sep + os.path.basename(config_path)
@IModule.route("dynamips.nio.get_interfaces")
def nio_get_interfaces(self, request):
"""
Get all the network interfaces on this host.
:param request: JSON request
"""
try:
import netifaces
except ImportError:
raise DynamipsError("The netifaces module is not installed")
self.send_response(netifaces.interfaces())
@@ -63,7 +63,7 @@ class Adapter(object):
False otherwise.
"""
if self._wics[wic_slot_id] == None:
if self._wics[wic_slot_id] is None:
return True
return False
+139 -63
View File
@@ -16,10 +16,20 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import re
import os
from gns3server.modules import IModule
from ..nodes.atm_switch import ATMSwitch
from ..dynamips_error import DynamipsError
from ..schemas.atmsw import ATMSW_CREATE_SCHEMA
from ..schemas.atmsw import ATMSW_DELETE_SCHEMA
from ..schemas.atmsw import ATMSW_UPDATE_SCHEMA
from ..schemas.atmsw import ATMSW_ALLOCATE_UDP_PORT_SCHEMA
from ..schemas.atmsw import ATMSW_ADD_NIO_SCHEMA
from ..schemas.atmsw import ATMSW_DELETE_NIO_SCHEMA
from ..schemas.atmsw import ATMSW_START_CAPTURE_SCHEMA
from ..schemas.atmsw import ATMSW_STOP_CAPTURE_SCHEMA
import logging
log = logging.getLogger(__name__)
@@ -31,7 +41,7 @@ class ATMSW(object):
"""
Creates a new ATM switch.
Optional request parameters:
Mandatory request parameters:
- name (switch name)
Response parameters:
@@ -41,11 +51,11 @@ class ATMSW(object):
:param request: JSON request
"""
#TODO: JSON schema validation for the request
name = None
if request and "name" in request:
name = request["name"]
# validate the request
if not self.validate_request(request, ATMSW_CREATE_SCHEMA):
return
name = request["name"]
try:
if not self._hypervisor_manager:
self.start_hypervisor_manager()
@@ -71,22 +81,20 @@ class ATMSW(object):
- id (switch identifier)
Response parameters:
- same as original request
- True on success
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, ATMSW_DELETE_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
# get the ATM switch instance
atmsw_id = request["id"]
if atmsw_id not in self._atm_switches:
self.send_custom_error("ATM switch id {} doesn't exist".format(atmsw_id))
atmsw = self.get_device_instance(atmsw_id, self._atm_switches)
if not atmsw:
return
atmsw = self._atm_switches[atmsw_id]
try:
atmsw.delete()
@@ -95,7 +103,7 @@ class ATMSW(object):
except DynamipsError as e:
self.send_custom_error(str(e))
return
self.send_response(request)
self.send_response(True)
@IModule.route("dynamips.atmsw.update")
def atmsw_update(self, request):
@@ -109,32 +117,31 @@ class ATMSW(object):
- name (new switch name)
Response parameters:
- same as original request
- name if changed
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, ATMSW_UPDATE_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
atmsw_id = request["id"]
if atmsw_id not in self._atm_switches:
self.send_custom_error("ATM switch id {} doesn't exist".format(atmsw_id))
# get the ATM switch instance
atmsw = self.get_device_instance(request["id"], self._atm_switches)
if not atmsw:
return
atmsw = self._atm_switches[atmsw_id]
response = {}
# rename the switch if requested
if "name" in request and atmsw.name != request["name"]:
try:
atmsw.name = request["name"]
response["name"] = atmsw.name
except DynamipsError as e:
self.send_custom_error(str(e))
return
self.send_response(request)
self.send_response(response)
@IModule.route("dynamips.atmsw.allocate_udp_port")
def atmsw_allocate_udp_port(self, request):
@@ -153,17 +160,14 @@ class ATMSW(object):
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, ATMSW_ALLOCATE_UDP_PORT_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
atmsw_id = request["id"]
if atmsw_id not in self._atm_switches:
self.send_custom_error("ATM switch id {} doesn't exist".format(atmsw_id))
# get the ATM switch instance
atmsw = self.get_device_instance(request["id"], self._atm_switches)
if not atmsw:
return
atmsw = self._atm_switches[atmsw_id]
try:
# allocate a new UDP port
@@ -185,42 +189,39 @@ class ATMSW(object):
- port (port identifier)
- port_id (port identifier)
- mappings (VCs/VPs mapped to the port)
- nio (nio type, one of the following)
- "NIO_UDP"
- nio (one of the following)
- type "nio_udp"
- lport (local port)
- rhost (remote host)
- rport (remote port)
- "NIO_GenericEthernet"
- type "nio_generic_ethernet"
- ethernet_device (Ethernet device name e.g. eth0)
- "NIO_LinuxEthernet"
- type "nio_linux_ethernet"
- ethernet_device (Ethernet device name e.g. eth0)
- "NIO_TAP"
- type "nio_tap"
- tap_device (TAP device name e.g. tap0)
- "NIO_UNIX"
- type "nio_unix"
- local_file (path to UNIX socket file)
- remote_file (path to UNIX socket file)
- "NIO_VDE"
- type "nio_vde"
- control_file (path to VDE control file)
- local_file (path to VDE local file)
- "NIO_Null"
- type "nio_null"
Response parameters:
- same as original request
- port_id (unique port identifier)
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, ATMSW_ADD_NIO_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
atmsw_id = request["id"]
if atmsw_id not in self._atm_switches:
self.send_custom_error("ATM switch id {} doesn't exist".format(atmsw_id))
# get the ATM switch instance
atmsw = self.get_device_instance(request["id"], self._atm_switches)
if not atmsw:
return
atmsw = self._atm_switches[atmsw_id]
port = request["port"]
mappings = request["mappings"]
@@ -261,8 +262,7 @@ class ATMSW(object):
self.send_custom_error(str(e))
return
# for now send back the original request
self.send_response(request)
self.send_response({"port_id": request["port_id"]})
@IModule.route("dynamips.atmsw.delete_nio")
def atmsw_delete_nio(self, request):
@@ -274,24 +274,21 @@ class ATMSW(object):
- port (port identifier)
Response parameters:
- same as original request
- True on success
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, ATMSW_DELETE_NIO_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
atmsw_id = request["id"]
if atmsw_id not in self._atm_switches:
self.send_custom_error("ATM switch id {} doesn't exist".format(atmsw_id))
# get the ATM switch instance
atmsw = self.get_device_instance(request["id"], self._atm_switches)
if not atmsw:
return
atmsw = self._atm_switches[atmsw_id]
port = request["port"]
try:
for source, destination in atmsw.mapping.copy().items():
if len(source) == 3 and len(destination) == 3:
@@ -315,5 +312,84 @@ class ATMSW(object):
self.send_custom_error(str(e))
return
# for now send back the original request
self.send_response(request)
self.send_response(True)
@IModule.route("dynamips.atmsw.start_capture")
def atmsw_start_capture(self, request):
"""
Starts a packet capture.
Mandatory request parameters:
- id (vm identifier)
- port (port identifier)
- port_id (port identifier)
- capture_file_name
Optional request parameters:
- data_link_type (PCAP DLT_* value)
Response parameters:
- port_id (port identifier)
- capture_file_path (path to the capture file)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, ATMSW_START_CAPTURE_SCHEMA):
return
# get the ATM switch instance
atmsw = self.get_device_instance(request["id"], self._atm_switches)
if not atmsw:
return
port = request["port"]
capture_file_name = request["capture_file_name"]
data_link_type = request.get("data_link_type")
try:
capture_file_path = os.path.join(atmsw.hypervisor.working_dir, "captures", capture_file_name)
atmsw.start_capture(port, capture_file_path, data_link_type)
except DynamipsError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"],
"capture_file_path": capture_file_path}
self.send_response(response)
@IModule.route("dynamips.atmsw.stop_capture")
def atmsw_stop_capture(self, request):
"""
Stops a packet capture.
Mandatory request parameters:
- id (vm identifier)
- port_id (port identifier)
- port (port number)
Response parameters:
- port_id (port identifier)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, ATMSW_STOP_CAPTURE_SCHEMA):
return
# get the ATM switch instance
atmsw = self.get_device_instance(request["id"], self._atm_switches)
if not atmsw:
return
port = request["port"]
try:
atmsw.stop_capture(port)
except DynamipsError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"]}
self.send_response(response)
+139 -63
View File
@@ -15,10 +15,20 @@
# 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
from gns3server.modules import IModule
from ..nodes.hub import Hub
from ..dynamips_error import DynamipsError
from ..schemas.ethhub import ETHHUB_CREATE_SCHEMA
from ..schemas.ethhub import ETHHUB_DELETE_SCHEMA
from ..schemas.ethhub import ETHHUB_UPDATE_SCHEMA
from ..schemas.ethhub import ETHHUB_ALLOCATE_UDP_PORT_SCHEMA
from ..schemas.ethhub import ETHHUB_ADD_NIO_SCHEMA
from ..schemas.ethhub import ETHHUB_DELETE_NIO_SCHEMA
from ..schemas.ethhub import ETHHUB_START_CAPTURE_SCHEMA
from ..schemas.ethhub import ETHHUB_STOP_CAPTURE_SCHEMA
import logging
log = logging.getLogger(__name__)
@@ -30,7 +40,7 @@ class ETHHUB(object):
"""
Creates a new Ethernet hub.
Optional request parameters:
Mandatory request parameters:
- name (hub name)
Response parameters:
@@ -40,11 +50,11 @@ class ETHHUB(object):
:param request: JSON request
"""
#TODO: JSON schema validation for the request
name = None
if request and "name" in request:
name = request["name"]
# validate the request
if not self.validate_request(request, ETHHUB_CREATE_SCHEMA):
return
name = request["name"]
try:
if not self._hypervisor_manager:
self.start_hypervisor_manager()
@@ -70,22 +80,20 @@ class ETHHUB(object):
- id (hub identifier)
Response parameters:
- same as original request
- True on success
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, ETHHUB_DELETE_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
# get the Ethernet hub instance
ethhub_id = request["id"]
if ethhub_id not in self._ethernet_hubs:
self.send_custom_error("Ethernet hub id {} doesn't exist".format(ethhub_id))
ethhub = self.get_device_instance(ethhub_id, self._ethernet_hubs)
if not ethhub:
return
ethhub = self._ethernet_hubs[ethhub_id]
try:
ethhub.delete()
@@ -108,27 +116,26 @@ class ETHHUB(object):
- name (new hub name)
Response parameters:
- same as original request
- name if changed
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, ETHHUB_UPDATE_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
ethhub_id = request["id"]
if ethhub_id not in self._ethernet_hubs:
self.send_custom_error("Ethernet hub id {} doesn't exist".format(ethhub_id))
# get the Ethernet hub instance
ethhub = self.get_device_instance(request["id"], self._ethernet_hubs)
if not ethhub:
return
ethhub = self._ethernet_hubs[ethhub_id]
response = {}
# rename the hub if requested
if "name" in request and ethhub.name != request["name"]:
try:
ethhub.name = request["name"]
response["name"] = ethhub.name
except DynamipsError as e:
self.send_custom_error(str(e))
return
@@ -152,17 +159,14 @@ class ETHHUB(object):
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, ETHHUB_ALLOCATE_UDP_PORT_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
ethhub_id = request["id"]
if ethhub_id not in self._ethernet_hubs:
self.send_custom_error("Ethernet hub id {} doesn't exist".format(ethhub_id))
# get the Ethernet hub instance
ethhub = self.get_device_instance(request["id"], self._ethernet_hubs)
if not ethhub:
return
ethhub = self._ethernet_hubs[ethhub_id]
try:
# allocate a new UDP port
@@ -183,44 +187,41 @@ class ETHHUB(object):
- id (hub identifier)
- port (port identifier)
- port_id (port identifier)
- nio (nio type, one of the following)
- "NIO_UDP"
- nio (one of the following)
- type "nio_udp"
- lport (local port)
- rhost (remote host)
- rport (remote port)
- "NIO_GenericEthernet"
- type "nio_generic_ethernet"
- ethernet_device (Ethernet device name e.g. eth0)
- "NIO_LinuxEthernet"
- type "nio_linux_ethernet"
- ethernet_device (Ethernet device name e.g. eth0)
- "NIO_TAP"
- type "nio_tap"
- tap_device (TAP device name e.g. tap0)
- "NIO_UNIX"
- type "nio_unix"
- local_file (path to UNIX socket file)
- remote_file (path to UNIX socket file)
- "NIO_VDE"
- type "nio_vde"
- control_file (path to VDE control file)
- local_file (path to VDE local file)
- "NIO_Null"
- type "nio_null"
Response parameters:
- same as original request
- port_id (unique port identifier)
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, ETHHUB_ADD_NIO_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
ethhub_id = request["id"]
if ethhub_id not in self._ethernet_hubs:
self.send_custom_error("Ethernet hub id {} doesn't exist".format(ethhub_id))
# get the Ethernet hub instance
ethhub = self.get_device_instance(request["id"], self._ethernet_hubs)
if not ethhub:
return
ethhub = self._ethernet_hubs[ethhub_id]
port = request["port"]
try:
nio = self.create_nio(ethhub, request)
if not nio:
@@ -235,11 +236,10 @@ class ETHHUB(object):
self.send_custom_error(str(e))
return
# for now send back the original request
self.send_response(request)
self.send_response({"port_id": request["port_id"]})
@IModule.route("dynamips.ethhub.delete_nio")
def ethsw_delete_nio(self, request):
def ethhub_delete_nio(self, request):
"""
Deletes an NIO (Network Input/Output).
@@ -248,24 +248,21 @@ class ETHHUB(object):
- port (port identifier)
Response parameters:
- same as original request
- True on success
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, ETHHUB_DELETE_NIO_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
ethhub_id = request["id"]
if ethhub_id not in self._ethernet_hubs:
self.send_custom_error("Ethernet hub id {} doesn't exist".format(ethhub_id))
# get the Ethernet hub instance
ethhub = self.get_device_instance(request["id"], self._ethernet_hubs)
if not ethhub:
return
ethhub = self._ethernet_hubs[ethhub_id]
port = request["port"]
try:
nio = ethhub.remove_nio(port)
nio.delete()
@@ -273,5 +270,84 @@ class ETHHUB(object):
self.send_custom_error(str(e))
return
# for now send back the original request
self.send_response(request)
self.send_response(True)
@IModule.route("dynamips.ethhub.start_capture")
def ethhub_start_capture(self, request):
"""
Starts a packet capture.
Mandatory request parameters:
- id (vm identifier)
- port (port identifier)
- port_id (port identifier)
- capture_file_name
Optional request parameters:
- data_link_type (PCAP DLT_* value)
Response parameters:
- port_id (port identifier)
- capture_file_path (path to the capture file)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, ETHHUB_START_CAPTURE_SCHEMA):
return
# get the Ethernet hub instance
ethhub = self.get_device_instance(request["id"], self._ethernet_hubs)
if not ethhub:
return
port = request["port"]
capture_file_name = request["capture_file_name"]
data_link_type = request.get("data_link_type")
try:
capture_file_path = os.path.join(ethhub.hypervisor.working_dir, "captures", capture_file_name)
ethhub.start_capture(port, capture_file_path, data_link_type)
except DynamipsError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"],
"capture_file_path": capture_file_path}
self.send_response(response)
@IModule.route("dynamips.ethhub.stop_capture")
def ethhub_stop_capture(self, request):
"""
Stops a packet capture.
Mandatory request parameters:
- id (vm identifier)
- port_id (port identifier)
- port (port number)
Response parameters:
- port_id (port identifier)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, ETHHUB_STOP_CAPTURE_SCHEMA):
return
# get the Ethernet hub instance
ethhub = self.get_device_instance(request["id"], self._ethernet_hubs)
if not ethhub:
return
port = request["port"]
try:
ethhub.stop_capture(port)
except DynamipsError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"]}
self.send_response(response)
+139 -63
View File
@@ -15,10 +15,20 @@
# 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
from gns3server.modules import IModule
from ..nodes.ethernet_switch import EthernetSwitch
from ..dynamips_error import DynamipsError
from ..schemas.ethsw import ETHSW_CREATE_SCHEMA
from ..schemas.ethsw import ETHSW_DELETE_SCHEMA
from ..schemas.ethsw import ETHSW_UPDATE_SCHEMA
from ..schemas.ethsw import ETHSW_ALLOCATE_UDP_PORT_SCHEMA
from ..schemas.ethsw import ETHSW_ADD_NIO_SCHEMA
from ..schemas.ethsw import ETHSW_DELETE_NIO_SCHEMA
from ..schemas.ethsw import ETHSW_START_CAPTURE_SCHEMA
from ..schemas.ethsw import ETHSW_STOP_CAPTURE_SCHEMA
import logging
log = logging.getLogger(__name__)
@@ -30,7 +40,7 @@ class ETHSW(object):
"""
Creates a new Ethernet switch.
Optional request parameters:
Mandatory request parameters:
- name (switch name)
Response parameters:
@@ -40,11 +50,11 @@ class ETHSW(object):
:param request: JSON request
"""
#TODO: JSON schema validation for the request
name = None
if request and "name" in request:
name = request["name"]
# validate the request
if not self.validate_request(request, ETHSW_CREATE_SCHEMA):
return
name = request["name"]
try:
if not self._hypervisor_manager:
self.start_hypervisor_manager()
@@ -70,22 +80,20 @@ class ETHSW(object):
- id (switch identifier)
Response parameters:
- same as original request
- True on success
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, ETHSW_DELETE_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
# get the Ethernet switch instance
ethsw_id = request["id"]
if ethsw_id not in self._ethernet_switches:
self.send_custom_error("Ethernet switch id {} doesn't exist".format(ethsw_id))
ethsw = self.get_device_instance(ethsw_id, self._ethernet_switches)
if not ethsw:
return
ethsw = self._ethernet_switches[ethsw_id]
try:
ethsw.delete()
@@ -94,7 +102,7 @@ class ETHSW(object):
except DynamipsError as e:
self.send_custom_error(str(e))
return
self.send_response(request)
self.send_response(True)
@IModule.route("dynamips.ethsw.update")
def ethsw_update(self, request):
@@ -109,22 +117,19 @@ class ETHSW(object):
- ports (ports settings)
Response parameters:
- same as original request
- name if changed
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, ETHSW_UPDATE_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
ethsw_id = request["id"]
if ethsw_id not in self._ethernet_switches:
self.send_custom_error("Ethernet switch id {} doesn't exist".format(ethsw_id))
# get the Ethernet switch instance
ethsw = self.get_device_instance(request["id"], self._ethernet_switches)
if not ethsw:
return
ethsw = self._ethernet_switches[ethsw_id]
if "ports" in request:
ports = request["ports"]
@@ -144,15 +149,17 @@ class ETHSW(object):
self.send_custom_error(str(e))
return
response = {}
# rename the switch if requested
if "name" in request and ethsw.name != request["name"]:
try:
ethsw.name = request["name"]
response["name"] = ethsw.name
except DynamipsError as e:
self.send_custom_error(str(e))
return
self.send_response(request)
self.send_response(response)
@IModule.route("dynamips.ethsw.allocate_udp_port")
def ethsw_allocate_udp_port(self, request):
@@ -171,17 +178,14 @@ class ETHSW(object):
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, ETHSW_ALLOCATE_UDP_PORT_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
ethsw_id = request["id"]
if ethsw_id not in self._ethernet_switches:
self.send_custom_error("Ethernet switch id {} doesn't exist".format(ethsw_id))
# get the Ethernet switch instance
ethsw = self.get_device_instance(request["id"], self._ethernet_switches)
if not ethsw:
return
ethsw = self._ethernet_switches[ethsw_id]
try:
# allocate a new UDP port
@@ -204,42 +208,39 @@ class ETHSW(object):
- port_id (port identifier)
- vlan (vlan identifier)
- port_type ("access", "dot1q" or "qinq")
- nio (nio type, one of the following)
- "NIO_UDP"
- nio (one of the following)
- type "nio_udp"
- lport (local port)
- rhost (remote host)
- rport (remote port)
- "NIO_GenericEthernet"
- type "nio_generic_ethernet"
- ethernet_device (Ethernet device name e.g. eth0)
- "NIO_LinuxEthernet"
- type "nio_linux_ethernet"
- ethernet_device (Ethernet device name e.g. eth0)
- "NIO_TAP"
- type "nio_tap"
- tap_device (TAP device name e.g. tap0)
- "NIO_UNIX"
- type "nio_unix"
- local_file (path to UNIX socket file)
- remote_file (path to UNIX socket file)
- "NIO_VDE"
- type "nio_vde"
- control_file (path to VDE control file)
- local_file (path to VDE local file)
- "NIO_Null"
- type "nio_null"
Response parameters:
- same as original request
- port_id (unique port identifier)
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, ETHSW_ADD_NIO_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
ethsw_id = request["id"]
if ethsw_id not in self._ethernet_switches:
self.send_custom_error("Ethernet switch id {} doesn't exist".format(ethsw_id))
# get the Ethernet switch instance
ethsw = self.get_device_instance(request["id"], self._ethernet_switches)
if not ethsw:
return
ethsw = self._ethernet_switches[ethsw_id]
port = request["port"]
vlan = request["vlan"]
@@ -264,8 +265,7 @@ class ETHSW(object):
self.send_custom_error(str(e))
return
# for now send back the original request
self.send_response(request)
self.send_response({"port_id": request["port_id"]})
@IModule.route("dynamips.ethsw.delete_nio")
def ethsw_delete_nio(self, request):
@@ -277,24 +277,21 @@ class ETHSW(object):
- port (port identifier)
Response parameters:
- same as original request
- True on success
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, ETHSW_DELETE_NIO_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
ethsw_id = request["id"]
if ethsw_id not in self._ethernet_switches:
self.send_custom_error("Ethernet switch id {} doesn't exist".format(ethsw_id))
# get the Ethernet switch instance
ethsw = self.get_device_instance(request["id"], self._ethernet_switches)
if not ethsw:
return
ethsw = self._ethernet_switches[ethsw_id]
port = request["port"]
try:
nio = ethsw.remove_nio(port)
nio.delete()
@@ -302,5 +299,84 @@ class ETHSW(object):
self.send_custom_error(str(e))
return
# for now send back the original request
self.send_response(request)
self.send_response(True)
@IModule.route("dynamips.ethsw.start_capture")
def ethsw_start_capture(self, request):
"""
Starts a packet capture.
Mandatory request parameters:
- id (vm identifier)
- port (port identifier)
- port_id (port identifier)
- capture_file_name
Optional request parameters:
- data_link_type (PCAP DLT_* value)
Response parameters:
- port_id (port identifier)
- capture_file_path (path to the capture file)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, ETHSW_START_CAPTURE_SCHEMA):
return
# get the Ethernet switch instance
ethsw = self.get_device_instance(request["id"], self._ethernet_switches)
if not ethsw:
return
port = request["port"]
capture_file_name = request["capture_file_name"]
data_link_type = request.get("data_link_type")
try:
capture_file_path = os.path.join(ethsw.hypervisor.working_dir, "captures", capture_file_name)
ethsw.start_capture(port, capture_file_path, data_link_type)
except DynamipsError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"],
"capture_file_path": capture_file_path}
self.send_response(response)
@IModule.route("dynamips.ethsw.stop_capture")
def ethsw_stop_capture(self, request):
"""
Stops a packet capture.
Mandatory request parameters:
- id (vm identifier)
- port_id (port identifier)
- port (port number)
Response parameters:
- port_id (port identifier)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, ETHSW_STOP_CAPTURE_SCHEMA):
return
# get the Ethernet switch instance
ethsw = self.get_device_instance(request["id"], self._ethernet_switches)
if not ethsw:
return
port = request["port"]
try:
ethsw.stop_capture(port)
except DynamipsError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"]}
self.send_response(response)
+140 -62
View File
@@ -15,10 +15,20 @@
# 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
from gns3server.modules import IModule
from ..nodes.frame_relay_switch import FrameRelaySwitch
from ..dynamips_error import DynamipsError
from ..schemas.frsw import FRSW_CREATE_SCHEMA
from ..schemas.frsw import FRSW_DELETE_SCHEMA
from ..schemas.frsw import FRSW_UPDATE_SCHEMA
from ..schemas.frsw import FRSW_ALLOCATE_UDP_PORT_SCHEMA
from ..schemas.frsw import FRSW_ADD_NIO_SCHEMA
from ..schemas.frsw import FRSW_DELETE_NIO_SCHEMA
from ..schemas.frsw import FRSW_START_CAPTURE_SCHEMA
from ..schemas.frsw import FRSW_STOP_CAPTURE_SCHEMA
import logging
log = logging.getLogger(__name__)
@@ -30,7 +40,7 @@ class FRSW(object):
"""
Creates a new Frame-Relay switch.
Optional request parameters:
Mandatory request parameters:
- name (switch name)
Response parameters:
@@ -40,11 +50,11 @@ class FRSW(object):
:param request: JSON request
"""
#TODO: JSON schema validation for the request
name = None
if request and "name" in request:
name = request["name"]
# validate the request
if not self.validate_request(request, FRSW_CREATE_SCHEMA):
return
name = request["name"]
try:
if not self._hypervisor_manager:
self.start_hypervisor_manager()
@@ -70,22 +80,20 @@ class FRSW(object):
- id (switch identifier)
Response parameters:
- same as original request
- True on success
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, FRSW_DELETE_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
# get the Frame relay switch instance
frsw_id = request["id"]
if frsw_id not in self._frame_relay_switches:
self.send_custom_error("Frame relay switch id {} doesn't exist".format(frsw_id))
frsw = self.get_device_instance(frsw_id, self._frame_relay_switches)
if not frsw:
return
frsw = self._frame_relay_switches[frsw_id]
try:
frsw.delete()
@@ -94,7 +102,8 @@ class FRSW(object):
except DynamipsError as e:
self.send_custom_error(str(e))
return
self.send_response(request)
self.send_response(True)
@IModule.route("dynamips.frsw.update")
def frsw_update(self, request):
@@ -108,30 +117,30 @@ class FRSW(object):
- name (new switch name)
Response parameters:
- same as original request
- name if updated
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, FRSW_UPDATE_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
frsw_id = request["id"]
if frsw_id not in self._frame_relay_switches:
self.send_custom_error("Frame relay switch id {} doesn't exist".format(frsw_id))
# get the Frame relay switch instance
frsw = self.get_device_instance(request["id"], self._frame_relay_switches)
if not frsw:
return
frsw = self._frame_relay_switches[frsw_id]
response = {}
# rename the switch if requested
if "name" in request and frsw.name != request["name"]:
try:
frsw.name = request["name"]
response["name"] = frsw.name
except DynamipsError as e:
self.send_custom_error(str(e))
return
self.send_response(request)
@IModule.route("dynamips.frsw.allocate_udp_port")
@@ -151,17 +160,14 @@ class FRSW(object):
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, FRSW_ALLOCATE_UDP_PORT_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
frsw_id = request["id"]
if frsw_id not in self._frame_relay_switches:
self.send_custom_error("Frame relay switch id {} doesn't exist".format(frsw_id))
# get the Frame relay switch instance
frsw = self.get_device_instance(request["id"], self._frame_relay_switches)
if not frsw:
return
frsw = self._frame_relay_switches[frsw_id]
try:
# allocate a new UDP port
@@ -183,42 +189,39 @@ class FRSW(object):
- port (port identifier)
- port_id (port identifier)
- mappings (VCs mapped to the port)
- nio (nio type, one of the following)
- "NIO_UDP"
- nio (one of the following)
- type "nio_udp"
- lport (local port)
- rhost (remote host)
- rport (remote port)
- "NIO_GenericEthernet"
- type "nio_generic_ethernet"
- ethernet_device (Ethernet device name e.g. eth0)
- "NIO_LinuxEthernet"
- type "nio_linux_ethernet"
- ethernet_device (Ethernet device name e.g. eth0)
- "NIO_TAP"
- type "nio_tap"
- tap_device (TAP device name e.g. tap0)
- "NIO_UNIX"
- type "nio_unix"
- local_file (path to UNIX socket file)
- remote_file (path to UNIX socket file)
- "NIO_VDE"
- type "nio_vde"
- control_file (path to VDE control file)
- local_file (path to VDE local file)
- "NIO_Null"
- type "nio_null"
Response parameters:
- same as original request
- port_id (unique port identifier)
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, FRSW_ADD_NIO_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
frsw_id = request["id"]
if frsw_id not in self._frame_relay_switches:
self.send_custom_error("Frame relay switch id {} doesn't exist".format(frsw_id))
# get the Frame relay switch instance
frsw = self.get_device_instance(request["id"], self._frame_relay_switches)
if not frsw:
return
frsw = self._frame_relay_switches[frsw_id]
port = request["port"]
mappings = request["mappings"]
@@ -246,8 +249,7 @@ class FRSW(object):
self.send_custom_error(str(e))
return
# for now send back the original request
self.send_response(request)
self.send_response({"port_id": request["port_id"]})
@IModule.route("dynamips.frsw.delete_nio")
def frsw_delete_nio(self, request):
@@ -259,24 +261,21 @@ class FRSW(object):
- port (port identifier)
Response parameters:
- same as original request
- True on success
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, FRSW_DELETE_NIO_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
frsw_id = request["id"]
if frsw_id not in self._frame_relay_switches:
self.send_custom_error("Frame relay switch id {} doesn't exist".format(frsw_id))
# get the Frame relay switch instance
frsw = self.get_device_instance(request["id"], self._frame_relay_switches)
if not frsw:
return
frsw = self._frame_relay_switches[frsw_id]
port = request["port"]
try:
# remove the VCs mapped with this port/nio
for source, destination in frsw.mapping.copy().items():
@@ -292,5 +291,84 @@ class FRSW(object):
self.send_custom_error(str(e))
return
# for now send back the original request
self.send_response(request)
self.send_response(True)
@IModule.route("dynamips.frsw.start_capture")
def frsw_start_capture(self, request):
"""
Starts a packet capture.
Mandatory request parameters:
- id (vm identifier)
- port (port identifier)
- port_id (port identifier)
- capture_file_name
Optional request parameters:
- data_link_type (PCAP DLT_* value)
Response parameters:
- port_id (port identifier)
- capture_file_path (path to the capture file)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, FRSW_START_CAPTURE_SCHEMA):
return
# get the Frame relay switch instance
frsw = self.get_device_instance(request["id"], self._frame_relay_switches)
if not frsw:
return
port = request["port"]
capture_file_name = request["capture_file_name"]
data_link_type = request.get("data_link_type")
try:
capture_file_path = os.path.join(frsw.hypervisor.working_dir, "captures", capture_file_name)
frsw.start_capture(port, capture_file_path, data_link_type)
except DynamipsError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"],
"capture_file_path": capture_file_path}
self.send_response(response)
@IModule.route("dynamips.frsw.stop_capture")
def frsw_stop_capture(self, request):
"""
Stops a packet capture.
Mandatory request parameters:
- id (vm identifier)
- port_id (port identifier)
- port (port number)
Response parameters:
- port_id (port identifier)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, FRSW_STOP_CAPTURE_SCHEMA):
return
# get the Frame relay switch instance
frsw = self.get_device_instance(request["id"], self._frame_relay_switches)
if not frsw:
return
port = request["port"]
try:
frsw.stop_capture(port)
except DynamipsError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"]}
self.send_response(response)
+378 -174
View File
@@ -16,8 +16,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import base64
import ntpath
import time
from gns3server.modules import IModule
from gns3dms.cloud.rackspace_ctrl import get_provider
from ..dynamips_error import DynamipsError
from ..nodes.c1700 import C1700
@@ -49,12 +51,29 @@ from ..adapters.wic_1enet import WIC_1ENET
from ..adapters.wic_1t import WIC_1T
from ..adapters.wic_2t import WIC_2T
from ..schemas.vm import VM_CREATE_SCHEMA
from ..schemas.vm import VM_DELETE_SCHEMA
from ..schemas.vm import VM_START_SCHEMA
from ..schemas.vm import VM_STOP_SCHEMA
from ..schemas.vm import VM_SUSPEND_SCHEMA
from ..schemas.vm import VM_RELOAD_SCHEMA
from ..schemas.vm import VM_UPDATE_SCHEMA
from ..schemas.vm import VM_START_CAPTURE_SCHEMA
from ..schemas.vm import VM_STOP_CAPTURE_SCHEMA
from ..schemas.vm import VM_SAVE_CONFIG_SCHEMA
from ..schemas.vm import VM_EXPORT_CONFIG_SCHEMA
from ..schemas.vm import VM_IDLEPCS_SCHEMA
from ..schemas.vm import VM_AUTO_IDLEPC_SCHEMA
from ..schemas.vm import VM_ALLOCATE_UDP_PORT_SCHEMA
from ..schemas.vm import VM_ADD_NIO_SCHEMA
from ..schemas.vm import VM_DELETE_NIO_SCHEMA
import logging
log = logging.getLogger(__name__)
ADAPTER_MATRIX = {"C7200_IO_2FE": C7200_IO_2FE,
"C7200_IO_FE": C7200_IO_FE,
ADAPTER_MATRIX = {"C7200-IO-2FE": C7200_IO_2FE,
"C7200-IO-FE": C7200_IO_FE,
"C7200-IO-GE-E": C7200_IO_GE_E,
"NM-16ESW": NM_16ESW,
"NM-1E": NM_1E,
@@ -92,12 +111,12 @@ class VM(object):
Creates a new VM (router).
Mandatory request parameters:
- name (vm name)
- platform (platform name e.g. c7200)
- image (path to IOS image)
- ram (amount of RAM in MB)
Optional request parameters:
- name (vm name)
- console (console port number)
- aux (auxiliary console port number)
- mac_addr (MAC address)
@@ -110,25 +129,39 @@ class VM(object):
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, VM_CREATE_SCHEMA):
return
log.debug("received request {}".format(request))
#TODO: JSON schema validation
name = None
if "name" in request:
name = request["name"]
name = request["name"]
platform = request["platform"]
image = request["image"]
ram = request["ram"]
hypervisor = None
chassis = None
if "chassis" in request:
chassis = request["chassis"]
chassis = request.get("chassis")
router_id = request.get("router_id")
# Locate the image
updated_image_path = os.path.join(self.images_directory, image)
if os.path.isfile(updated_image_path):
image = updated_image_path
else:
if not os.path.exists(self.images_directory):
os.mkdir(self.images_directory)
cloud_path = request.get("cloud_path", None)
if cloud_path is not None:
# Download the image from cloud files
_, filename = ntpath.split(image)
src = '{}/{}'.format(cloud_path, filename)
provider = get_provider(self._cloud_settings)
log.debug("Downloading file from {} to {}...".format(src, updated_image_path))
provider.download_file(src, updated_image_path)
log.debug("Download of {} complete.".format(src))
image = updated_image_path
try:
if platform not in PLATFORMS:
raise DynamipsError("Unknown router platform: {}".format(platform))
if not self._hypervisor_manager:
self.start_hypervisor_manager()
@@ -136,12 +169,15 @@ class VM(object):
hypervisor = self._hypervisor_manager.allocate_hypervisor_for_router(image, ram)
if chassis:
router = PLATFORMS[platform](hypervisor, name, chassis=chassis)
router = PLATFORMS[platform](hypervisor, name, router_id, chassis=chassis)
elif platform == "c7200" and os.path.basename(image).lower().startswith("c7200p"):
router = PLATFORMS[platform](hypervisor, name, router_id, npe="npe-g2")
else:
router = PLATFORMS[platform](hypervisor, name)
router = PLATFORMS[platform](hypervisor, name, router_id)
router.ram = ram
router.image = image
router.sparsemem = self._hypervisor_manager.sparse_memory_support
if platform not in ("c1700", "c2600"):
router.sparsemem = self._hypervisor_manager.sparse_memory_support
router.mmap = self._hypervisor_manager.mmap_support
if "console" in request:
router.console = request["console"]
@@ -197,31 +233,30 @@ class VM(object):
- id (vm identifier)
Response parameters:
- same as original request
- True on success
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, VM_DELETE_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
# get the router instance
router_id = request["id"]
if router_id not in self._routers:
self.send_custom_error("IOS router id {} doesn't exist".format(router_id))
router = self.get_device_instance(router_id, self._routers)
if not router:
return
router = self._routers[router_id]
try:
router.delete()
router.clean_delete()
self._hypervisor_manager.unallocate_hypervisor_for_router(router)
del self._routers[router_id]
except DynamipsError as e:
self.send_custom_error(str(e))
return
self.send_response(request)
self.send_response(True)
@IModule.route("dynamips.vm.start")
def vm_start(self, request):
@@ -232,29 +267,26 @@ class VM(object):
- id (vm identifier)
Response parameters:
- same as original request
- True on success
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, VM_START_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
router_id = request["id"]
if router_id not in self._routers:
self.send_custom_error("IOS router id {} doesn't exist".format(router_id))
# get the router instance
router = self.get_device_instance(request["id"], self._routers)
if not router:
return
router = self._routers[router_id]
try:
router.start()
except DynamipsError as e:
self.send_custom_error(str(e))
return
self.send_response(request)
self.send_response(True)
@IModule.route("dynamips.vm.stop")
def vm_stop(self, request):
@@ -265,29 +297,27 @@ class VM(object):
- id (vm identifier)
Response parameters:
- same as original request
- True on success
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, VM_STOP_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
router_id = request["id"]
if router_id not in self._routers:
self.send_custom_error("IOS router id {} doesn't exist".format(router_id))
# get the router instance
router = self.get_device_instance(request["id"], self._routers)
if not router:
return
router = self._routers[router_id]
try:
router.stop()
except DynamipsError as e:
self.send_custom_error(str(e))
return
self.send_response(request)
self.send_response(True)
@IModule.route("dynamips.vm.suspend")
def vm_suspend(self, request):
@@ -298,29 +328,27 @@ class VM(object):
- id (vm identifier)
Response parameters:
- same as original request
- True on success
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, VM_SUSPEND_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
router_id = request["id"]
if router_id not in self._routers:
self.send_custom_error("IOS router id {} doesn't exist".format(router_id))
# get the router instance
router = self.get_device_instance(request["id"], self._routers)
if not router:
return
router = self._routers[router_id]
try:
router.suspend()
except DynamipsError as e:
self.send_custom_error(str(e))
return
self.send_response(request)
self.send_response(True)
@IModule.route("dynamips.vm.reload")
def vm_reload(self, request):
@@ -331,22 +359,20 @@ class VM(object):
- id (vm identifier)
Response parameters:
- same as original request
- True on success
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, VM_RELOAD_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
router_id = request["id"]
if router_id not in self._routers:
self.send_custom_error("IOS router id {} doesn't exist".format(router_id))
# get the router instance
router = self.get_device_instance(request["id"], self._routers)
if not router:
return
router = self._routers[router_id]
try:
if router.get_status() != "inactive":
router.stop()
@@ -354,7 +380,8 @@ class VM(object):
except DynamipsError as e:
self.send_custom_error(str(e))
return
self.send_response(request)
self.send_response(True)
@IModule.route("dynamips.vm.update")
def vm_update(self, request):
@@ -370,37 +397,56 @@ class VM(object):
- private_config_base64 (private-config base64 encoded)
Response parameters:
- same as original request
- updated settings
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, VM_UPDATE_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
router_id = request["id"]
if router_id not in self._routers:
self.send_custom_error("IOS router id {} doesn't exist".format(router_id))
# get the router instance
router = self.get_device_instance(request["id"], self._routers)
if not router:
return
router = self._routers[router_id]
response = {}
try:
default_startup_config_path = os.path.join(router.hypervisor.working_dir, "configs", "i{}_startup-config.cfg".format(router.id))
default_private_config_path = os.path.join(router.hypervisor.working_dir, "configs", "i{}_private-config.cfg".format(router.id))
# a new startup-config has been pushed
if "startup_config_base64" in request:
config_filename = "{}.cfg".format(router.name)
request["startup_config"] = self.save_base64config(request["startup_config_base64"], router, config_filename)
if "startup_config" in request:
router.set_config(request["startup_config"])
# update the request with the new local startup-config path
request["startup_config"] = self.create_config_from_base64(request["startup_config_base64"], router, default_startup_config_path)
# a new private-config has been pushed
if "private_config_base64" in request:
config_filename = "{}-private.cfg".format(router.name)
request["private_config"] = self.save_base64config(request["private_config_base64"], router, config_filename)
# update the request with the new local private-config path
request["private_config"] = self.create_config_from_base64(request["private_config_base64"], router, default_private_config_path)
if "startup_config" in request:
startup_config_path = request["startup_config"].replace("\\", '/')
if os.path.isfile(startup_config_path) and startup_config_path != default_startup_config_path:
# this is a local file set in the GUI
startup_config_path = self.create_config_from_file(startup_config_path, router, default_startup_config_path)
router.set_config(startup_config_path)
else:
router.set_config(startup_config_path)
response["startup_config"] = startup_config_path
del request["startup_config"]
if "private_config" in request:
router.set_config(router.startup_config, request["private_config"])
private_config_path = request["private_config"].replace("\\", '/')
if os.path.isfile(private_config_path) and private_config_path != default_private_config_path:
# this is a local file set in the GUI
private_config_path = self.create_config_from_file(private_config_path, router, default_private_config_path)
router.set_config(router.startup_config, private_config_path)
else:
router.set_config(router.startup_config, private_config_path)
response["private_config"] = private_config_path
del request["private_config"]
except DynamipsError as e:
self.send_custom_error(str(e))
@@ -411,6 +457,7 @@ class VM(object):
if hasattr(router, name) and getattr(router, name) != value:
try:
setattr(router, name, value)
response[name] = value
except DynamipsError as e:
self.send_custom_error(str(e))
return
@@ -422,14 +469,16 @@ class VM(object):
if router.slots[slot_id] and type(router.slots[slot_id]) != type(adapter):
router.slot_remove_binding(slot_id)
router.slot_add_binding(slot_id, adapter)
response[name] = value
except DynamipsError as e:
self.send_custom_error(str(e))
return
elif name.startswith("slot") and value == None:
elif name.startswith("slot") and value is None:
slot_id = int(name[-1])
if router.slots[slot_id]:
try:
router.slot_remove_binding(slot_id)
response[name] = value
except DynamipsError as e:
self.send_custom_error(str(e))
return
@@ -441,6 +490,7 @@ class VM(object):
if router.slots[0].wics[wic_slot_id] and type(router.slots[0].wics[wic_slot_id]) != type(wic):
router.uninstall_wic(wic_slot_id)
router.install_wic(wic_slot_id, wic)
response[name] = value
except DynamipsError as e:
self.send_custom_error(str(e))
return
@@ -449,16 +499,104 @@ class VM(object):
if router.slots[0].wics and router.slots[0].wics[wic_slot_id]:
try:
router.uninstall_wic(wic_slot_id)
response[name] = value
except DynamipsError as e:
self.send_custom_error(str(e))
return
# Update the ghost IOS file in case the RAM size has changed
if self._hypervisor_manager.ghost_ios_support:
self.set_ghost_ios(router)
try:
self.set_ghost_ios(router)
except DynamipsError as e:
self.send_custom_error(str(e))
return
# for now send back the original request
self.send_response(request)
self.send_response(response)
@IModule.route("dynamips.vm.start_capture")
def vm_start_capture(self, request):
"""
Starts a packet capture.
Mandatory request parameters:
- id (vm identifier)
- port_id (port identifier)
- slot (slot number)
- port (port number)
- capture_file_name
Optional request parameters:
- data_link_type (PCAP DLT_* value)
Response parameters:
- port_id (port identifier)
- capture_file_path (path to the capture file)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VM_START_CAPTURE_SCHEMA):
return
# get the router instance
router = self.get_device_instance(request["id"], self._routers)
if not router:
return
slot = request["slot"]
port = request["port"]
capture_file_name = request["capture_file_name"]
data_link_type = request.get("data_link_type")
try:
capture_file_path = os.path.join(router.hypervisor.working_dir, "captures", capture_file_name)
router.start_capture(slot, port, capture_file_path, data_link_type)
except DynamipsError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"],
"capture_file_path": capture_file_path}
self.send_response(response)
@IModule.route("dynamips.vm.stop_capture")
def vm_stop_capture(self, request):
"""
Stops a packet capture.
Mandatory request parameters:
- id (vm identifier)
- port_id (port identifier)
- slot (slot number)
- port (port number)
Response parameters:
- port_id (port identifier)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VM_STOP_CAPTURE_SCHEMA):
return
# get the router instance
router = self.get_device_instance(request["id"], self._routers)
if not router:
return
slot = request["slot"]
port = request["port"]
try:
router.stop_capture(slot, port)
except DynamipsError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"]}
self.send_response(response)
@IModule.route("dynamips.vm.save_config")
def vm_save_config(self, request):
@@ -469,92 +607,170 @@ class VM(object):
- id (vm identifier)
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, VM_SAVE_CONFIG_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
router_id = request["id"]
if router_id not in self._routers:
self.send_custom_error("IOS router id {} doesn't exist".format(router_id))
# get the router instance
router = self.get_device_instance(request["id"], self._routers)
if not router:
return
router = self._routers[router_id]
try:
if router.startup_config or router.private_config:
startup_config_base64, private_config_base64 = router.extract_config()
if startup_config_base64:
try:
config = base64.decodestring(startup_config_base64.encode("utf-8")).decode("utf-8")
config = "!\n" + config.replace("\r", "")
config_path = os.path.join(router.hypervisor.working_dir, router.startup_config)
with open(config_path, "w") as f:
log.info("saving startup-config to {}".format(router.startup_config))
f.write(config)
except OSError as e:
raise DynamipsError("Could not save the startup configuration {}: {}".format(config_path, e))
if private_config_base64:
try:
config = base64.decodestring(private_config_base64.encode("utf-8")).decode("utf-8")
config = "!\n" + config.replace("\r", "")
config_path = os.path.join(router.hypervisor.working_dir, router.private_config)
with open(config_path, "w") as f:
log.info("saving private-config to {}".format(router.private_config))
f.write(config)
except OSError as e:
raise DynamipsError("Could not save the private configuration {}: {}".format(config_path, e))
router.save_configs()
except DynamipsError as e:
log.warn("could not save config to {}: {}".format(router.startup_config, e))
@IModule.route("dynamips.vm.export_config")
def vm_export_config(self, request):
"""
Export the config from a router
Mandatory request parameters:
- id (vm identifier)
Response parameters:
- startup_config_base64 (startup-config base64 encoded)
- private_config_base64 (private-config base64 encoded)
- False if no configuration can be extracted
"""
# validate the request
if not self.validate_request(request, VM_EXPORT_CONFIG_SCHEMA):
return
# get the router instance
router = self.get_device_instance(request["id"], self._routers)
if not router:
return
response = {}
try:
startup_config_base64, private_config_base64 = router.extract_config()
if startup_config_base64:
response["startup_config_base64"] = startup_config_base64
if private_config_base64:
response["private_config_base64"] = private_config_base64
except DynamipsError:
self.send_custom_error("unable to extract configs from the NVRAM")
return
if not response:
self.send_response(False)
else:
self.send_response(response)
@IModule.route("dynamips.vm.idlepcs")
def vm_idlepcs(self, request):
"""
Get idle-pc proposals.
Get Idle-PC proposals.
Mandatory request parameters:
- id (vm identifier)
Optional request parameters:
- compute (returns previously compute idle-pc values if False)
- compute (returns previously compute Idle-PC values if False)
Response parameters:
- id (vm identifier)
- idlepcs (idle-pc values in an array)
- idlepcs (Idle-PC values in an array)
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, VM_IDLEPCS_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
router_id = request["id"]
if router_id not in self._routers:
self.send_custom_error("IOS router id {} doesn't exist".format(router_id))
# get the router instance
router = self.get_device_instance(request["id"], self._routers)
if not router:
return
router = self._routers[router_id]
try:
if "compute" in request and request["compute"] == False:
idlepcs = router.show_idle_pc_prop()
else:
# reset the current idle-pc value before calculating a new one
# reset the current Idle-PC value before calculating a new one
router.idlepc = "0x0"
idlepcs = router.get_idle_pc_prop()
except DynamipsError as e:
self.send_custom_error(str(e))
return
response = {"id": router_id,
response = {"id": router.id,
"idlepcs": idlepcs}
self.send_response(response)
@IModule.route("dynamips.vm.auto_idlepc")
def vm_auto_idlepc(self, request):
"""
Auto Idle-PC calculation.
Mandatory request parameters:
- id (vm identifier)
Response parameters:
- id (vm identifier)
- logs (logs for the calculation)
- idlepc (Idle-PC value)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VM_AUTO_IDLEPC_SCHEMA):
return
# get the router instance
router = self.get_device_instance(request["id"], self._routers)
if not router:
return
try:
router.idlepc = "0x0" # reset the current Idle-PC value before calculating a new one
was_auto_started = False
if router.get_status() != "running":
router.start()
was_auto_started = True
time.sleep(20) # leave time to the router to boot
logs = []
validated_idlepc = "0x0"
idlepcs = router.get_idle_pc_prop()
if not idlepcs:
logs.append("No Idle-PC values found")
for idlepc in idlepcs:
router.idlepc = idlepc.split()[0]
logs.append("Trying Idle-PC value {}".format(router.idlepc))
start_time = time.time()
initial_cpu_usage = router.get_cpu_usage()
logs.append("Initial CPU usage = {}%".format(initial_cpu_usage))
time.sleep(4) # wait 4 seconds to probe the cpu again
elapsed_time = time.time() - start_time
cpu_elapsed_usage = router.get_cpu_usage() - initial_cpu_usage
cpu_usage = abs(cpu_elapsed_usage * 100.0 / elapsed_time)
logs.append("CPU usage after {:.2} seconds = {:.2}%".format(elapsed_time, cpu_usage))
if cpu_usage > 100:
cpu_usage = 100
if cpu_usage < 70:
validated_idlepc = router.idlepc
logs.append("Idle-PC value {} has been validated".format(validated_idlepc))
break
except DynamipsError as e:
self.send_custom_error(str(e))
return
finally:
if was_auto_started:
router.stop()
response = {"id": router.id,
"logs": logs,
"idlepc": validated_idlepc}
self.send_response(response)
@IModule.route("dynamips.vm.allocate_udp_port")
def vm_allocate_udp_port(self, request):
"""
@@ -571,17 +787,14 @@ class VM(object):
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, VM_ALLOCATE_UDP_PORT_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
router_id = request["id"]
if router_id not in self._routers:
self.send_custom_error("IOS router id {} doesn't exist".format(router_id))
# get the router instance
router = self.get_device_instance(request["id"], self._routers)
if not router:
return
router = self._routers[router_id]
try:
# allocate a new UDP port
@@ -603,46 +816,42 @@ class VM(object):
- slot (slot number)
- port (port number)
- port_id (unique port identifier)
- nio (nio type, one of the following)
- "NIO_UDP"
- nio (one of the following)
- type "nio_udp"
- lport (local port)
- rhost (remote host)
- rport (remote port)
- "NIO_GenericEthernet"
- type "nio_generic_ethernet"
- ethernet_device (Ethernet device name e.g. eth0)
- "NIO_LinuxEthernet"
- type "nio_linux_ethernet"
- ethernet_device (Ethernet device name e.g. eth0)
- "NIO_TAP"
- type "nio_tap"
- tap_device (TAP device name e.g. tap0)
- "NIO_UNIX"
- type "nio_unix"
- local_file (path to UNIX socket file)
- remote_file (path to UNIX socket file)
- "NIO_VDE"
- type "nio_vde"
- control_file (path to VDE control file)
- local_file (path to VDE local file)
- "NIO_Null"
- type "nio_null"
Response parameters:
- same as original request
- port_id (unique port identifier)
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, VM_ADD_NIO_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
router_id = request["id"]
if router_id not in self._routers:
self.send_custom_error("IOS router id {} doesn't exist".format(router_id))
# get the router instance
router = self.get_device_instance(request["id"], self._routers)
if not router:
return
router = self._routers[router_id]
slot = request["slot"]
port = request["port"]
try:
nio = self.create_nio(router, request)
if not nio:
@@ -657,8 +866,7 @@ class VM(object):
self.send_custom_error(str(e))
return
# for now send back the original request
self.send_response(request)
self.send_response({"port_id": request["port_id"]})
@IModule.route("dynamips.vm.delete_nio")
def vm_delete_nio(self, request):
@@ -671,25 +879,22 @@ class VM(object):
- port (port identifier)
Response parameters:
- same as original request
- True on success
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, VM_DELETE_NIO_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
router_id = request["id"]
router = self._routers[router_id]
if router_id not in self._routers:
self.send_custom_error("IOS router id {} doesn't exist".format(router_id))
# get the router instance
router = self.get_device_instance(request["id"], self._routers)
if not router:
return
slot = request["slot"]
port = request["port"]
try:
nio = router.slot_remove_nio_binding(slot, port)
nio.delete()
@@ -697,5 +902,4 @@ class VM(object):
self.send_custom_error(str(e))
return
# for now send back the original request
self.send_response(request)
self.send_response(True)
@@ -25,6 +25,8 @@ class DynamipsError(Exception):
def __init__(self, message, original_exception=None):
Exception.__init__(self, message)
if isinstance(message, Exception):
message = str(message)
self._message = message
self._original_exception = original_exception
@@ -21,10 +21,10 @@ http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L46
"""
import socket
import errno
import re
import logging
from .dynamips_error import DynamipsError
from .nios.nio_udp_auto import NIO_UDP_auto
log = logging.getLogger(__name__)
@@ -53,12 +53,15 @@ class DynamipsHypervisor(object):
self._ghosts = {}
self._jitsharing_groups = {}
self._working_dir = working_dir
self._baseconsole = 2000
self._baseaux = 2100
self._baseudp = 10000
self._current_udp_port = self._baseudp
self._console_start_port_range = 2001
self._console_end_port_range = 2500
self._aux_start_port_range = 2501
self._aux_end_port_range = 3000
self._udp_start_port_range = 10001
self._udp_end_port_range = 20000
self._nio_udp_auto_instances = {}
self._version = "N/A"
self._timeout = 30
self._timeout = timeout
self._socket = None
self._uuid = None
@@ -77,9 +80,7 @@ class DynamipsHypervisor(object):
host = self._host
try:
self._socket = socket.create_connection((host,
self._port),
self._timeout)
self._socket = socket.create_connection((host, self._port), self._timeout)
except OSError as e:
raise DynamipsError("Could not connect to server: {}".format(e))
@@ -90,6 +91,9 @@ class DynamipsHypervisor(object):
self._uuid = self.send("hypervisor uuid")
# this forces to send the working dir to Dynamips
self.working_dir = self._working_dir
@property
def version(self):
"""
@@ -125,6 +129,7 @@ class DynamipsHypervisor(object):
"""
self.send("hypervisor close")
self._socket.shutdown(socket.SHUT_RDWR)
self._socket.close()
self._socket = None
@@ -134,8 +139,10 @@ class DynamipsHypervisor(object):
"""
self.send("hypervisor stop")
self._socket.shutdown(socket.SHUT_RDWR)
self._socket.close()
self._socket = None
self._nio_udp_auto_instances.clear()
def reset(self):
"""
@@ -143,6 +150,7 @@ class DynamipsHypervisor(object):
"""
self.send('hypervisor reset')
self._nio_udp_auto_instances.clear()
@property
def working_dir(self):
@@ -220,68 +228,124 @@ class DynamipsHypervisor(object):
self._devices = devices
@property
def baseconsole(self):
def console_start_port_range(self):
"""
Returns base console TCP port for this hypervisor.
Returns the console start port range value
:returns: base console value (integer)
:returns: console start port range value (integer)
"""
return self._baseconsole
return self._console_start_port_range
@baseconsole.setter
def baseconsole(self, baseconsole):
@console_start_port_range.setter
def console_start_port_range(self, console_start_port_range):
"""
Sets the base console TCP port for this hypervisor.
Set a new console start port range value
:param baseconsole: base console value (integer)
:param console_start_port_range: console start port range value (integer)
"""
self._baseconsole = baseconsole
self._console_start_port_range = console_start_port_range
@property
def baseaux(self):
def console_end_port_range(self):
"""
Returns base auxiliary port for this hypervisor.
Returns the console end port range value
:returns: base auxiliary port value (integer)
:returns: console end port range value (integer)
"""
return self._baseaux
return self._console_end_port_range
@baseaux.setter
def baseaux(self, baseaux):
@console_end_port_range.setter
def console_end_port_range(self, console_end_port_range):
"""
Sets the base auxiliary TCP port for this hypervisor.
Set a new console end port range value
:param baseaux: base auxiliary port value (integer)
:param console_end_port_range: console end port range value (integer)
"""
self._baseaux = baseaux
self._console_end_port_range = console_end_port_range
@property
def baseudp(self):
def aux_start_port_range(self):
"""
Returns the next available UDP port for UDP NIOs.
Returns the auxiliary console start port range value
:returns: base UDP port value (integer)
:returns: auxiliary console start port range value (integer)
"""
return self._baseudp
return self._aux_start_port_range
@baseudp.setter
def baseudp(self, baseudp):
@aux_start_port_range.setter
def aux_start_port_range(self, aux_start_port_range):
"""
Sets the next open UDP port for NIOs for this hypervisor.
Sets a new auxiliary console start port range value
:param baseudp: base UDP port value (integer)
:param aux_start_port_range: auxiliary console start port range value (integer)
"""
self._baseudp = baseudp
self._current_udp_port = self._baseudp
self._aux_start_port_range = aux_start_port_range
#FIXME
log.info("hypervisor a new base UDP {}".format(self._baseudp))
@property
def aux_end_port_range(self):
"""
Returns the auxiliary console end port range value
:returns: auxiliary console end port range value (integer)
"""
return self._aux_end_port_range
@aux_end_port_range.setter
def aux_end_port_range(self, aux_end_port_range):
"""
Sets a new auxiliary console end port range value
:param aux_end_port_range: auxiliary console end port range value (integer)
"""
self._aux_end_port_range = aux_end_port_range
@property
def udp_start_port_range(self):
"""
Returns the UDP start port range value
:returns: UDP start port range value (integer)
"""
return self._udp_start_port_range
@udp_start_port_range.setter
def udp_start_port_range(self, udp_start_port_range):
"""
Sets a new UDP start port range value
:param udp_start_port_range: UDP start port range value (integer)
"""
self._udp_start_port_range = udp_start_port_range
@property
def udp_end_port_range(self):
"""
Returns the UDP end port range value
:returns: UDP end port range value (integer)
"""
return self._udp_end_port_range
@udp_end_port_range.setter
def udp_end_port_range(self, udp_end_port_range):
"""
Sets an new UDP end port range value
:param udp_end_port_range: UDP end port range value (integer)
"""
self._udp_end_port_range = udp_end_port_range
@property
def ghosts(self):
@@ -343,58 +407,29 @@ class DynamipsHypervisor(object):
return self._port
@staticmethod
def find_unused_port(start_port, end_port, host='127.0.0.1', socket_type="TCP"):
def get_nio_udp_auto(self, port):
"""
Finds an unused port in a range.
Returns an allocated NIO UDP auto instance.
:param start_port: first port in the range
:param end_port: last port in the range
:param host: host/address for bind()
:param socket_type: TCP (default) or UDP
:returns: NIO UDP auto instance
"""
if socket_type == "UDP":
socket_type = socket.SOCK_DGRAM
if port in self._nio_udp_auto_instances:
return self._nio_udp_auto_instances.pop(port)
else:
socket_type = socket.SOCK_STREAM
return None
for port in range(start_port, end_port):
try:
if ":" in host:
# IPv6 address support
with socket.socket(socket.AF_INET6, socket_type) as s:
s.bind((host, port)) # the port is available if bind is a success
else:
with socket.socket(socket.AF_INET, socket_type) as s:
s.bind((host, port)) # the port is available if bind is a success
return port
except OSError as e:
if e.errno == errno.EADDRINUSE: # socket already in use
if port + 1 == end_port:
raise DynamipsError("Could not find a free port between {0} and {1}".format(start_port, end_port))
else:
continue
else:
raise DynamipsError("Could not find an unused port: {}".format(e))
def allocate_udp_port(self, max_port=100):
def allocate_udp_port(self):
"""
Allocates a new UDP port for creating an UDP NIO.
:param max_port: maximum number of port to scan in
order to find one available for use.
Allocates a new UDP port for creating an UDP NIO Auto.
:returns: port number (integer)
"""
start_port = self._current_udp_port
end_port = start_port + max_port
allocated_port = DynamipsHypervisor.find_unused_port(start_port, end_port, self._host, socket_type="UDP")
if allocated_port - self._current_udp_port > 1:
self._current_udp_port += allocated_port - self._current_udp_port
else:
self._current_udp_port += 1
# use Dynamips's NIO UDP auto back-end.
nio = NIO_UDP_auto(self, self._host, self._udp_start_port_range, self._udp_end_port_range)
self._nio_udp_auto_instances[nio.lport] = nio
allocated_port = nio.lport
return allocated_port
def send_raw(self, string):
@@ -439,8 +474,8 @@ class DynamipsHypervisor(object):
log.debug("sending {}".format(command))
self.socket.sendall(command.encode('utf-8'))
except OSError as e:
raise DynamipsError("Lost communication with {host}:{port} :{error}"
.format(host=self._host, port=self._port, error=e))
raise DynamipsError("Lost communication with {host}:{port} :{error}, Dynamips process running: {run}"
.format(host=self._host, port=self._port, error=e, run=self.is_running()))
# Now retrieve the result
data = []
@@ -450,16 +485,16 @@ class DynamipsHypervisor(object):
chunk = self.socket.recv(1024) # match to Dynamips' buffer size
buf += chunk.decode("utf-8")
except OSError as e:
raise DynamipsError("Communication timed out with {host}:{port} :{error}"
.format(host=self._host, port=self._port, error=e))
raise DynamipsError("Communication timed out with {host}:{port} :{error}, Dynamips process running: {run}"
.format(host=self._host, port=self._port, error=e, run=self.is_running()))
# If the buffer doesn't end in '\n' then we can't be done
try:
if buf[-1] != '\n':
continue
except IndexError:
raise DynamipsError("Could not communicate with {host}:{port}"
.format(host=self._host, port=self._port))
raise DynamipsError("Could not communicate with {host}:{port}, Dynamips process running: {run}"
.format(host=self._host, port=self._port, run=self.is_running()))
data += buf.split('\r\n')
if data[-1] == '':
@@ -467,8 +502,8 @@ class DynamipsHypervisor(object):
buf = ''
if len(data) == 0:
raise DynamipsError("no data returned from {host}:{port}"
.format(host=self._host, port=self._port))
raise DynamipsError("no data returned from {host}:{port}, Dynamips process running: {run}"
.format(host=self._host, port=self._port, run=self.is_running()))
# Does it contain an error code?
if self.error_re.search(data[-1]):
+10 -9
View File
@@ -70,7 +70,7 @@ class Hypervisor(DynamipsHypervisor):
:returns: id (integer)
"""
return(self._id)
return self._id
@property
def started(self):
@@ -90,7 +90,7 @@ class Hypervisor(DynamipsHypervisor):
:returns: path to Dynamips
"""
return(self._path)
return self._path
@path.setter
def path(self, path):
@@ -110,7 +110,7 @@ class Hypervisor(DynamipsHypervisor):
:returns: port number (integer)
"""
return(self._port)
return self._port
@port.setter
def port(self, port):
@@ -130,7 +130,7 @@ class Hypervisor(DynamipsHypervisor):
:returns: host/address (string)
"""
return(self._host)
return self._host
@host.setter
def host(self, host):
@@ -212,7 +212,7 @@ class Hypervisor(DynamipsHypervisor):
cwd=self._working_dir)
log.info("Dynamips started PID={}".format(self._process.pid))
self._started = True
except OSError as e:
except (OSError, subprocess.SubprocessError) as e:
log.error("could not start Dynamips: {}".format(e))
raise DynamipsError("could not start Dynamips: {}".format(e))
@@ -232,7 +232,7 @@ class Hypervisor(DynamipsHypervisor):
self._process.wait(1)
except subprocess.TimeoutExpired:
self._process.kill()
if self._process.poll() == None:
if self._process.poll() is None:
log.warn("Dynamips process {} is still running".format(self._process.pid))
if self._stdout_file and os.access(self._stdout_file, os.W_OK):
@@ -251,7 +251,7 @@ class Hypervisor(DynamipsHypervisor):
output = ""
if self._stdout_file and os.access(self._stdout_file, os.R_OK):
try:
with open(self._stdout_file) as file:
with open(self._stdout_file, errors="replace") as file:
output = file.read()
except OSError as e:
log.warn("could not read {}: {}".format(self._stdout_file, e))
@@ -264,7 +264,7 @@ class Hypervisor(DynamipsHypervisor):
:returns: True or False
"""
if self._process and self._process.poll() == None:
if self._process and self._process.poll() is None:
return True
return False
@@ -276,8 +276,9 @@ class Hypervisor(DynamipsHypervisor):
command = [self._path]
command.extend(["-N1"]) # use instance IDs for filenames
command.extend(["-l", "dynamips_log_{}.txt".format(self._port)]) # log file
if self._host != "0.0.0.0" and self._host != "::":
command.extend(["-H", self._host + ":" + str(self._port)])
command.extend(["-H", "{}:{}".format(self._host, self._port)])
else:
command.extend(["-H", str(self._port)])
return command
+197 -120
View File
@@ -19,12 +19,14 @@
Manages Dynamips hypervisors (load-balancing etc.)
"""
from gns3server.config import Config
from .hypervisor import Hypervisor
from .dynamips_error import DynamipsError
from ..attic import find_unused_port
from ..attic import wait_socket_is_ready
from pkg_resources import parse_version
import os
import socket
import time
import logging
@@ -38,39 +40,34 @@ class HypervisorManager(object):
:param path: path to the Dynamips executable
:param working_dir: path to a working directory
:param host: host/address for hypervisors to listen to
:param base_port: base TCP port for hypervisors
:param base_console: base TCP port for consoles
:param base_aux: base TCP port for auxiliary consoles
:param base_udp: base UDP port for UDP tunnels
:param console_host: IP address to bind for console connections
"""
def __init__(self,
path,
working_dir,
host='127.0.0.1',
base_hypervisor_port=7200,
base_console_port=2000,
base_aux_port=3000,
base_udp_port=10000):
def __init__(self, path, working_dir, host='127.0.0.1', console_host='0.0.0.0'):
self._hypervisors = []
self._path = path
self._working_dir = working_dir
self._host = host
self._base_hypervisor_port = base_hypervisor_port
self._current_port = self._base_hypervisor_port
self._base_console_port = base_console_port
self._base_aux_port = base_aux_port
self._base_udp_port = base_udp_port
self._current_base_udp_port = self._base_udp_port
self._udp_incrementation_per_hypervisor = 100
self._ghost_ios_support = True
self._mmap_support = True
self._jit_sharing_support = False
self._sparse_memory_support = True
self._allocate_hypervisor_per_device = True
self._memory_usage_limit_per_hypervisor = 1024
self._allocate_hypervisor_per_ios_image = True
self._console_host = console_host
self._host = console_host # FIXME: Dynamips must be patched to bind on a different address than the one used by the hypervisor.
config = Config.instance()
dynamips_config = config.get_section_config("DYNAMIPS")
self._hypervisor_start_port_range = dynamips_config.get("hypervisor_start_port_range", 7200)
self._hypervisor_end_port_range = dynamips_config.get("hypervisor_end_port_range", 7700)
self._console_start_port_range = dynamips_config.get("console_start_port_range", 2001)
self._console_end_port_range = dynamips_config.get("console_end_port_range", 2500)
self._aux_start_port_range = dynamips_config.get("aux_start_port_range", 2501)
self._aux_end_port_range = dynamips_config.get("aux_end_port_range", 3000)
self._udp_start_port_range = dynamips_config.get("udp_start_port_range", 10001)
self._udp_end_port_range = dynamips_config.get("udp_end_port_range", 20000)
self._ghost_ios_support = dynamips_config.get("ghost_ios_support", True)
self._mmap_support = dynamips_config.get("mmap_support", True)
self._jit_sharing_support = dynamips_config.get("jit_sharing_support", False)
self._sparse_memory_support = dynamips_config.get("sparse_memory_support", True)
self._allocate_hypervisor_per_device = dynamips_config.get("allocate_hypervisor_per_device", True)
self._memory_usage_limit_per_hypervisor = dynamips_config.get("memory_usage_limit_per_hypervisor", 1024)
self._allocate_hypervisor_per_ios_image = dynamips_config.get("allocate_hypervisor_per_ios_image", True)
def __del__(self):
"""
@@ -136,94 +133,204 @@ class HypervisorManager(object):
hypervisor.working_dir = self._working_dir
@property
def base_hypervisor_port(self):
def hypervisor_start_port_range(self):
"""
Returns the base hypervisor port.
Returns the hypervisor start port range value
:returns: base hypervisor port (integer)
:returns: hypervisor start port range value (integer)
"""
return self._base_hypervisor_port
return self._hypervisor_start_port_range
@base_hypervisor_port.setter
def base_hypervisor_port(self, base_hypervisor_port):
@hypervisor_start_port_range.setter
def hypervisor_start_port_range(self, hypervisor_start_port_range):
"""
Set a new base hypervisor port.
Sets a new hypervisor start port range value
:param base_hypervisor_port: base hypervisor port (integer)
:param hypervisor_start_port_range: hypervisor start port range value (integer)
"""
if self._base_hypervisor_port != base_hypervisor_port:
self._base_hypervisor_port = base_hypervisor_port
self._current_port = self._base_hypervisor_port
log.info("base hypervisor port set to {}".format(self._base_hypervisor_port))
if self._hypervisor_start_port_range != hypervisor_start_port_range:
self._hypervisor_start_port_range = hypervisor_start_port_range
log.info("hypervisor start port range value set to {}".format(self._hypervisor_start_port_range))
@property
def base_console_port(self):
def hypervisor_end_port_range(self):
"""
Returns the base console port.
Returns the hypervisor end port range value
:returns: base console port (integer)
:returns: hypervisor end port range value (integer)
"""
return self._base_console_port
return self._hypervisor_end_port_range
@base_console_port.setter
def base_console_port(self, base_console_port):
@hypervisor_end_port_range.setter
def hypervisor_end_port_range(self, hypervisor_end_port_range):
"""
Set a new base console port.
Sets a new hypervisor end port range value
:param base_console_port: base console port (integer)
:param hypervisor_end_port_range: hypervisor end port range value (integer)
"""
if self._base_console_port != base_console_port:
self._base_console_port = base_console_port
log.info("base console port set to {}".format(self._base_console_port))
if self._hypervisor_end_port_range != hypervisor_end_port_range:
self._hypervisor_end_port_range = hypervisor_end_port_range
log.info("hypervisor end port range value set to {}".format(self._hypervisor_end_port_range))
@property
def base_aux_port(self):
def console_start_port_range(self):
"""
Returns the base auxiliary console port.
Returns the console start port range value
:returns: base auxiliary console port (integer)
:returns: console start port range value (integer)
"""
return self._base_aux_port
return self._console_start_port_range
@base_aux_port.setter
def base_aux_port(self, base_aux_port):
@console_start_port_range.setter
def console_start_port_range(self, console_start_port_range):
"""
Set a new base auxiliary console port.
Sets a new console start port range value
:param base_aux_port: base auxiliary console port (integer)
:param console_start_port_range: console start port range value (integer)
"""
if self._base_aux_port != base_aux_port:
self._base_aux_port = base_aux_port
log.info("base aux port set to {}".format(self._base_aux_port))
if self._console_start_port_range != console_start_port_range:
self._console_start_port_range = console_start_port_range
log.info("console start port range value set to {}".format(self._console_start_port_range))
# update all existing hypervisors with the new value
for hypervisor in self._hypervisors:
hypervisor.console_start_port_range = console_start_port_range
@property
def base_udp_port(self):
def console_end_port_range(self):
"""
Returns the base UDP port.
Returns the console end port range value
:returns: base UDP port (integer)
:returns: console end port range value (integer)
"""
return self._base_udp_port
return self._console_end_port_range
@base_udp_port.setter
def base_udp_port(self, base_udp_port):
@console_end_port_range.setter
def console_end_port_range(self, console_end_port_range):
"""
Set a new base UDP port.
Sets a new console end port range value
:param base_udp_port: base UDP port (integer)
:param console_end_port_range: console end port range value (integer)
"""
if self._base_udp_port != base_udp_port:
self._base_udp_port = base_udp_port
self._current_base_udp_port = self._base_udp_port
log.info("base UDP port set to {}".format(self._base_udp_port))
if self._console_end_port_range != console_end_port_range:
self._console_end_port_range = console_end_port_range
log.info("console end port range value set to {}".format(self._console_end_port_range))
# update all existing hypervisors with the new value
for hypervisor in self._hypervisors:
hypervisor.console_end_port_range = console_end_port_range
@property
def aux_start_port_range(self):
"""
Returns the auxiliary console start port range value
:returns: auxiliary console start port range value (integer)
"""
return self._aux_start_port_range
@aux_start_port_range.setter
def aux_start_port_range(self, aux_start_port_range):
"""
Sets a new auxiliary console start port range value
:param aux_start_port_range: auxiliary console start port range value (integer)
"""
if self._aux_start_port_range != aux_start_port_range:
self._aux_start_port_range = aux_start_port_range
log.info("auxiliary console start port range value set to {}".format(self._aux_start_port_range))
# update all existing hypervisors with the new value
for hypervisor in self._hypervisors:
hypervisor.aux_start_port_range = aux_start_port_range
@property
def aux_end_port_range(self):
"""
Returns the auxiliary console end port range value
:returns: auxiliary console end port range value (integer)
"""
return self._aux_end_port_range
@aux_end_port_range.setter
def aux_end_port_range(self, aux_end_port_range):
"""
Sets a new auxiliary console end port range value
:param aux_end_port_range: auxiliary console end port range value (integer)
"""
if self._aux_end_port_range != aux_end_port_range:
self._aux_end_port_range = aux_end_port_range
log.info("auxiliary console end port range value set to {}".format(self._aux_end_port_range))
# update all existing hypervisors with the new value
for hypervisor in self._hypervisors:
hypervisor.aux_end_port_range = aux_end_port_range
@property
def udp_start_port_range(self):
"""
Returns the UDP start port range value
:returns: UDP start port range value (integer)
"""
return self._udp_start_port_range
@udp_start_port_range.setter
def udp_start_port_range(self, udp_start_port_range):
"""
Sets a new UDP start port range value
:param udp_start_port_range: UDP start port range value (integer)
"""
if self._udp_start_port_range != udp_start_port_range:
self._udp_start_port_range = udp_start_port_range
log.info("UDP start port range value set to {}".format(self._udp_start_port_range))
# update all existing hypervisors with the new value
for hypervisor in self._hypervisors:
hypervisor.udp_start_port_range = udp_start_port_range
@property
def udp_end_port_range(self):
"""
Returns the UDP end port range value
:returns: UDP end port range value (integer)
"""
return self._udp_end_port_range
@udp_end_port_range.setter
def udp_end_port_range(self, udp_end_port_range):
"""
Sets a new UDP end port range value
:param udp_end_port_range: UDP end port range value (integer)
"""
if self._udp_end_port_range != udp_end_port_range:
self._udp_end_port_range = udp_end_port_range
log.info("UDP end port range value set to {}".format(self._udp_end_port_range))
# update all existing hypervisors with the new value
for hypervisor in self._hypervisors:
hypervisor.udp_end_port_range = udp_end_port_range
@property
def ghost_ios_support(self):
@@ -399,35 +506,17 @@ class HypervisorManager(object):
else:
log.info("allocating an hypervisor per IOS image disabled")
def wait_for_hypervisor(self, host, port, timeout=10):
def wait_for_hypervisor(self, host, port):
"""
Waits for an hypervisor to be started (accepting a socket connection)
:param host: host/address to connect to the hypervisor
:param port: port to connect to the hypervisor
:param timeout: timeout value (default is 10 seconds)
"""
# connect to a local address by default
# if listening to all addresses (IPv4 or IPv6)
if host == "0.0.0.0":
host = "127.0.0.1"
elif host == "::":
host = "::1"
connection_success = False
begin = time.time()
# try to connect for 10 seconds
while(time.time() - begin < 10.0):
time.sleep(0.01)
try:
with socket.create_connection((host, port), timeout):
pass
except OSError as e:
last_exception = e
continue
connection_success = True
break
# wait for the socket for a maximum of 10 seconds.
connection_success, last_exception = wait_socket_is_ready(host, port, wait=10.0)
if not connection_success:
# FIXME: throw exception here
@@ -436,25 +525,6 @@ class HypervisorManager(object):
else:
log.info("Dynamips server ready after {:.4f} seconds".format(time.time() - begin))
def allocate_tcp_port(self, max_port=100):
"""
Allocates a new TCP port for a Dynamips hypervisor.
:param max_port: maximum number of port to scan in
order to find one available for use.
:returns: port number (integer)
"""
start_port = self._current_port
end_port = start_port + max_port
allocated_port = Hypervisor.find_unused_port(start_port, end_port, self._host)
if allocated_port - self._current_port > 1:
self._current_port += allocated_port - self._current_port
else:
self._current_port += 1
return allocated_port
def start_new_hypervisor(self):
"""
Creates a new Dynamips process and start it.
@@ -462,13 +532,17 @@ class HypervisorManager(object):
:returns: the new hypervisor instance
"""
port = self.allocate_tcp_port()
try:
port = find_unused_port(self._hypervisor_start_port_range, self._hypervisor_end_port_range, self._host)
except Exception as e:
raise DynamipsError(e)
hypervisor = Hypervisor(self._path,
self._working_dir,
self._host,
port)
log.info("creating new hypervisor {}:{}".format(hypervisor.host, hypervisor.port))
log.info("creating new hypervisor {}:{} with working directory {}".format(hypervisor.host, hypervisor.port, self._working_dir))
hypervisor.start()
self.wait_for_hypervisor(self._host, port)
@@ -477,10 +551,13 @@ class HypervisorManager(object):
hypervisor.connect()
if parse_version(hypervisor.version) < parse_version('0.2.11'):
raise DynamipsError("Dynamips version must be >= 0.2.11, detected version is {}".format(hypervisor.version))
hypervisor.baseconsole = self._base_console_port
hypervisor.baseaux = self._base_aux_port
hypervisor.baseudp = self._current_base_udp_port
self._current_base_udp_port += self._udp_incrementation_per_hypervisor
hypervisor.console_start_port_range = self._console_start_port_range
hypervisor.console_end_port_range = self._console_end_port_range
hypervisor.aux_start_port_range = self._aux_start_port_range
hypervisor.aux_end_port_range = self._aux_end_port_range
hypervisor.udp_start_port_range = self._udp_start_port_range
hypervisor.udp_end_port_range = self._udp_end_port_range
self._hypervisors.append(hypervisor)
return hypervisor
+5 -3
View File
@@ -57,6 +57,8 @@ class NIO(object):
Deletes this NIO.
"""
if self._input_filter or self._output_filter:
self.unbind_filter("both")
self._hypervisor.send("nio delete {}".format(self._name))
log.info("NIO {name} has been deleted".format(name=self._name))
@@ -134,7 +136,7 @@ class NIO(object):
def setup_filter(self, direction, options):
"""
Setups a packet filter binded with this NIO.
Setups a packet filter bound with this NIO.
Filter "freq_drop" has 1 argument "<frequency>". It will drop
everything with a -1 frequency, drop every Nth packet with a
@@ -174,7 +176,7 @@ class NIO(object):
:returns: tuple (filter name, filter options)
"""
return (self._input_filter, self._input_filter_options)
return self._input_filter, self._input_filter_options
@property
def output_filter(self):
@@ -184,7 +186,7 @@ class NIO(object):
:returns: tuple (filter name, filter options)
"""
return (self._output_filter, self._output_filter_options)
return self._output_filter, self._output_filter_options
def get_stats(self):
"""
@@ -33,6 +33,7 @@ class ATMBridge(object):
def __init__(self, hypervisor, name):
#FIXME: instance tracking
self._hypervisor = hypervisor
self._name = '"' + name + '"' # put name into quotes to protect spaces
self._hypervisor.send("atm_bridge create {}".format(self._name))
+66 -23
View File
@@ -20,6 +20,7 @@ Interface for Dynamips virtual ATM switch module ("atmsw").
http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L593
"""
import os
from ..dynamips_error import DynamipsError
import logging
@@ -34,26 +35,21 @@ class ATMSwitch(object):
:param name: name for this switch
"""
_allocated_names = []
_instance_count = 1
_instances = []
def __init__(self, hypervisor, name=None):
def __init__(self, hypervisor, name):
# create an unique ID
self._id = ATMSwitch._instance_count
ATMSwitch._instance_count += 1
# find an instance identifier (0 < id <= 4096)
self._id = 0
for identifier in range(1, 4097):
if identifier not in self._instances:
self._id = identifier
self._instances.append(self._id)
break
# let's create a unique name if none has been chosen
if not name:
name_id = self._id
while True:
name = "ATM" + str(name_id)
# check if the name has already been allocated to another switch
if name not in self._allocated_names:
break
name_id += 1
if self._id == 0:
raise DynamipsError("Maximum number of instances reached")
self._allocated_names.append(name)
self._hypervisor = hypervisor
self._name = '"' + name + '"' # put name into quotes to protect spaces
self._hypervisor.send("atmsw create {}".format(self._name))
@@ -68,11 +64,10 @@ class ATMSwitch(object):
@classmethod
def reset(cls):
"""
Resets the instance count and the allocated names list.
Resets the instance count and the allocated instances list.
"""
cls._instance_count = 1
cls._allocated_names.clear()
cls._instances.clear()
@property
def id(self):
@@ -102,7 +97,6 @@ class ATMSwitch(object):
:param new_name: New name for this switch
"""
new_name_no_quotes = new_name
new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces
self._hypervisor.send("atmsw rename {name} {new_name}".format(name=self._name,
new_name=new_name))
@@ -111,9 +105,7 @@ class ATMSwitch(object):
id=self._id,
new_name=new_name))
self._allocated_names.remove(self.name)
self._name = new_name
self._allocated_names.append(new_name_no_quotes)
@property
def hypervisor(self):
@@ -164,7 +156,7 @@ class ATMSwitch(object):
log.info("ATM switch {name} [id={id}] has been deleted".format(name=self._name,
id=self._id))
self._hypervisor.devices.remove(self)
self._allocated_names.remove(self.name)
self._instances.remove(self._id)
def has_port(self, port):
"""
@@ -360,3 +352,54 @@ class ATMSwitch(object):
vpi2=vpi2,
vci2=vci2))
del self._mapping[(port1, vpi1, vci1)]
def start_capture(self, port, output_file, data_link_type="DLT_ATM_RFC1483"):
"""
Starts a packet capture.
:param port: allocated port
:param output_file: PCAP destination file for the capture
:param data_link_type: PCAP data link type (DLT_*), default is DLT_ATM_RFC1483
"""
if port not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port))
nio = self._nios[port]
data_link_type = data_link_type.lower()
if data_link_type.startswith("dlt_"):
data_link_type = data_link_type[4:]
if nio.input_filter[0] is not None and nio.output_filter[0] is not None:
raise DynamipsError("Port {} has already a filter applied".format(port))
try:
os.makedirs(os.path.dirname(output_file))
except FileExistsError:
pass
except OSError as e:
raise DynamipsError("Could not create captures directory {}".format(e))
nio.bind_filter("both", "capture")
nio.setup_filter("both", "{} {}".format(data_link_type, output_file))
log.info("ATM switch {name} [id={id}]: starting packet capture on {port}".format(name=self._name,
id=self._id,
port=port))
def stop_capture(self, port):
"""
Stops a packet capture.
:param port: allocated port
"""
if port not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port))
nio = self._nios[port]
nio.unbind_filter("both")
log.info("ATM switch {name} [id={id}]: stopping packet capture on {port}".format(name=self._name,
id=self._id,
port=port))
@@ -29,12 +29,9 @@ class Bridge(object):
:param name: name for this bridge
"""
_allocated_names = []
def __init__(self, hypervisor, name):
self._hypervisor = hypervisor
self._allocated_names.append(name)
self._name = '"' + name + '"' # put name into quotes to protect spaces
self._hypervisor.send("nio_bridge create {}".format(self._name))
self._hypervisor.devices.append(self)
@@ -58,14 +55,11 @@ class Bridge(object):
:param new_name: New name for this bridge
"""
new_name_no_quotes = new_name
new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces
self._hypervisor.send("nio_bridge rename {name} {new_name}".format(name=self._name,
new_name=new_name))
self._allocated_names.remove(self.name)
self._name = new_name
self._allocated_names.append(new_name_no_quotes)
@property
def hypervisor(self):
@@ -103,7 +97,6 @@ class Bridge(object):
self._hypervisor.send("nio_bridge delete {}".format(self._name))
self._hypervisor.devices.remove(self)
self._allocated_names.remove(self.name)
def add_nio(self, nio):
"""
+7 -4
View File
@@ -34,22 +34,24 @@ class C1700(Router):
:param hypervisor: Dynamips hypervisor instance
:param name: name for this router
:param router_id: router instance ID
:param chassis: chassis for this router:
1720, 1721, 1750, 1751 or 1760 (default = 1720).
1710 is not supported.
"""
def __init__(self, hypervisor, name=None, chassis="1720"):
Router.__init__(self, hypervisor, name, platform="c1700")
def __init__(self, hypervisor, name, router_id=None, chassis="1720"):
Router.__init__(self, hypervisor, name, router_id, platform="c1700")
# Set default values for this platform
self._ram = 64
self._ram = 128
self._nvram = 32
self._disk0 = 0
self._disk1 = 0
self._chassis = chassis
self._iomem = 15 # percentage
self._clock_divisor = 8
self._sparsemem = False
if chassis != "1720":
self.chassis = chassis
@@ -71,7 +73,8 @@ class C1700(Router):
"disk1": self._disk1,
"chassis": self._chassis,
"iomem": self._iomem,
"clock_divisor": self._clock_divisor}
"clock_divisor": self._clock_divisor,
"sparsemem": self._sparsemem}
# update the router defaults with the platform specific defaults
router_defaults.update(platform_defaults)
+7 -4
View File
@@ -36,6 +36,7 @@ class C2600(Router):
:param hypervisor: Dynamips hypervisor instance
:param name: name for this router
:param router_id: router instance ID
:param chassis: chassis for this router:
2610, 2611, 2620, 2621, 2610XM, 2611XM
2620XM, 2621XM, 2650XM or 2651XM (default = 2610).
@@ -54,17 +55,18 @@ class C2600(Router):
"2650XM": C2600_MB_1FE,
"2651XM": C2600_MB_2FE}
def __init__(self, hypervisor, name=None, chassis="2610"):
Router.__init__(self, hypervisor, name, platform="c2600")
def __init__(self, hypervisor, name, router_id=None, chassis="2610"):
Router.__init__(self, hypervisor, name, router_id, platform="c2600")
# Set default values for this platform
self._ram = 64
self._ram = 128
self._nvram = 128
self._disk0 = 0
self._disk1 = 0
self._chassis = chassis
self._iomem = 15 # percentage
self._clock_divisor = 8
self._sparsemem = False
if chassis != "2610":
self.chassis = chassis
@@ -86,7 +88,8 @@ class C2600(Router):
"disk1": self._disk1,
"iomem": self._iomem,
"chassis": self._chassis,
"clock_divisor": self._clock_divisor}
"clock_divisor": self._clock_divisor,
"sparsemem": self._sparsemem}
# update the router defaults with the platform specific defaults
router_defaults.update(platform_defaults)
+4 -3
View File
@@ -33,13 +33,14 @@ class C2691(Router):
:param hypervisor: Dynamips hypervisor instance
:param name: name for this router
:param router_id: router instance ID
"""
def __init__(self, hypervisor, name=None):
Router.__init__(self, hypervisor, name, platform="c2691")
def __init__(self, hypervisor, name, router_id=None):
Router.__init__(self, hypervisor, name, router_id, platform="c2691")
# Set default values for this platform
self._ram = 128
self._ram = 192
self._nvram = 112
self._disk0 = 16
self._disk1 = 0
+4 -3
View File
@@ -33,15 +33,16 @@ class C3600(Router):
:param hypervisor: Dynamips hypervisor instance
:param name: name for this router
:param router_id: router instance ID
:param chassis: chassis for this router:
3620, 3640 or 3660 (default = 3640).
"""
def __init__(self, hypervisor, name=None, chassis="3640"):
Router.__init__(self, hypervisor, name, platform="c3600")
def __init__(self, hypervisor, name, router_id=None, chassis="3640"):
Router.__init__(self, hypervisor, name, router_id, platform="c3600")
# Set default values for this platform
self._ram = 128
self._ram = 192
self._nvram = 128
self._disk0 = 0
self._disk1 = 0
+3 -2
View File
@@ -33,10 +33,11 @@ class C3725(Router):
:param hypervisor: Dynamips hypervisor instance
:param name: name for this router
:param router_id: router instance ID
"""
def __init__(self, hypervisor, name=None):
Router.__init__(self, hypervisor, name, platform="c3725")
def __init__(self, hypervisor, name, router_id=None):
Router.__init__(self, hypervisor, name, router_id, platform="c3725")
# Set default values for this platform
self._ram = 128
+4 -3
View File
@@ -33,13 +33,14 @@ class C3745(Router):
:param hypervisor: Dynamips hypervisor instance
:param name: name for this router
:param router_id: router instance ID
"""
def __init__(self, hypervisor, name=None):
Router.__init__(self, hypervisor, name, platform="c3745")
def __init__(self, hypervisor, name, router_id=None):
Router.__init__(self, hypervisor, name, router_id, platform="c3745")
# Set default values for this platform
self._ram = 128
self._ram = 256
self._nvram = 304
self._disk0 = 16
self._disk1 = 0
+6 -5
View File
@@ -22,7 +22,7 @@ http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L294
from ..dynamips_error import DynamipsError
from .router import Router
from ..adapters.c7200_io_2fe import C7200_IO_2FE
from ..adapters.c7200_io_fe import C7200_IO_FE
from ..adapters.c7200_io_ge_e import C7200_IO_GE_E
import logging
@@ -35,14 +35,15 @@ class C7200(Router):
:param hypervisor: Dynamips hypervisor instance
:param name: name for this router
:param router_id: router instance ID
:param npe: default NPE
"""
def __init__(self, hypervisor, name=None, npe="npe-400"):
Router.__init__(self, hypervisor, name, platform="c7200")
def __init__(self, hypervisor, name, router_id=None, npe="npe-400"):
Router.__init__(self, hypervisor, name, router_id, platform="c7200")
# Set default values for this platform
self._ram = 256
self._ram = 512
self._nvram = 128
self._disk0 = 64
self._disk1 = 0
@@ -69,7 +70,7 @@ class C7200(Router):
if npe == "npe-g2":
self.slot_add_binding(0, C7200_IO_GE_E())
else:
self.slot_add_binding(0, C7200_IO_2FE())
self.slot_add_binding(0, C7200_IO_FE())
def defaults(self):
"""
@@ -20,6 +20,7 @@ Interface for Dynamips virtual Ethernet switch module ("ethsw").
http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L558
"""
import os
from ..dynamips_error import DynamipsError
import logging
@@ -34,26 +35,21 @@ class EthernetSwitch(object):
:param name: name for this switch
"""
_allocated_names = []
_instance_count = 1
_instances = []
def __init__(self, hypervisor, name=None):
def __init__(self, hypervisor, name):
# create an unique ID
self._id = EthernetSwitch._instance_count
EthernetSwitch._instance_count += 1
# find an instance identifier (0 < id <= 4096)
self._id = 0
for identifier in range(1, 4097):
if identifier not in self._instances:
self._id = identifier
self._instances.append(self._id)
break
# let's create a unique name if none has been chosen
if not name:
name_id = self._id
while True:
name = "SW" + str(name_id)
# check if the name has already been allocated to another switch
if name not in self._allocated_names:
break
name_id += 1
if self._id == 0:
raise DynamipsError("Maximum number of instances reached")
self._allocated_names.append(name)
self._hypervisor = hypervisor
self._name = '"' + name + '"' # put name into quotes to protect spaces
self._hypervisor.send("ethsw create {}".format(self._name))
@@ -68,11 +64,10 @@ class EthernetSwitch(object):
@classmethod
def reset(cls):
"""
Resets the instance count and the allocated names list.
Resets the instance count and the allocated instances list.
"""
cls._instance_count = 1
cls._allocated_names.clear()
cls._instances.clear()
@property
def id(self):
@@ -102,7 +97,6 @@ class EthernetSwitch(object):
:param new_name: New name for this switch
"""
new_name_no_quotes = new_name
new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces
self._hypervisor.send("ethsw rename {name} {new_name}".format(name=self._name,
new_name=new_name))
@@ -111,9 +105,7 @@ class EthernetSwitch(object):
id=self._id,
new_name=new_name))
self._allocated_names.remove(self.name)
self._name = new_name
self._allocated_names.append(new_name_no_quotes)
@property
def hypervisor(self):
@@ -164,7 +156,7 @@ class EthernetSwitch(object):
log.info("Ethernet switch {name} [id={id}] has been deleted".format(name=self._name,
id=self._id))
self._hypervisor.devices.remove(self)
self._allocated_names.remove(self.name)
self._instances.remove(self._id)
def add_nio(self, nio, port):
"""
@@ -296,3 +288,54 @@ class EthernetSwitch(object):
"""
self._hypervisor.send("ethsw clear_mac_addr_table {}".format(self._name))
def start_capture(self, port, output_file, data_link_type="DLT_EN10MB"):
"""
Starts a packet capture.
:param port: allocated port
:param output_file: PCAP destination file for the capture
:param data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB
"""
if port not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port))
nio = self._nios[port]
data_link_type = data_link_type.lower()
if data_link_type.startswith("dlt_"):
data_link_type = data_link_type[4:]
if nio.input_filter[0] is not None and nio.output_filter[0] is not None:
raise DynamipsError("Port {} has already a filter applied".format(port))
try:
os.makedirs(os.path.dirname(output_file))
except FileExistsError:
pass
except OSError as e:
raise DynamipsError("Could not create captures directory {}".format(e))
nio.bind_filter("both", "capture")
nio.setup_filter("both", "{} {}".format(data_link_type, output_file))
log.info("Ethernet switch {name} [id={id}]: starting packet capture on {port}".format(name=self._name,
id=self._id,
port=port))
def stop_capture(self, port):
"""
Stops a packet capture.
:param port: allocated port
"""
if port not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port))
nio = self._nios[port]
nio.unbind_filter("both")
log.info("Ethernet switch {name} [id={id}]: stopping packet capture on {port}".format(name=self._name,
id=self._id,
port=port))
@@ -20,6 +20,7 @@ Interface for Dynamips virtual Frame-Relay switch module.
http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L642
"""
import os
from ..dynamips_error import DynamipsError
import logging
@@ -34,26 +35,21 @@ class FrameRelaySwitch(object):
:param name: name for this switch
"""
_allocated_names = []
_instance_count = 1
_instances = []
def __init__(self, hypervisor, name=None):
def __init__(self, hypervisor, name):
# create an unique ID
self._id = FrameRelaySwitch._instance_count
FrameRelaySwitch._instance_count += 1
# find an instance identifier (0 < id <= 4096)
self._id = 0
for identifier in range(1, 4097):
if identifier not in self._instances:
self._id = identifier
self._instances.append(self._id)
break
# let's create a unique name if none has been chosen
if not name:
name_id = self._id
while True:
name = "FR" + str(name_id)
# check if the name has already been allocated to another switch
if name not in self._allocated_names:
break
name_id += 1
if self._id == 0:
raise DynamipsError("Maximum number of instances reached")
self._allocated_names.append(name)
self._hypervisor = hypervisor
self._name = '"' + name + '"' # put name into quotes to protect spaces
self._hypervisor.send("frsw create {}".format(self._name))
@@ -68,11 +64,10 @@ class FrameRelaySwitch(object):
@classmethod
def reset(cls):
"""
Resets the instance count and the allocated names list.
Resets the instance count and the allocated instances list.
"""
cls._instance_count = 1
cls._allocated_names.clear()
cls._instances.clear()
@property
def id(self):
@@ -102,7 +97,6 @@ class FrameRelaySwitch(object):
:param new_name: New name for this switch
"""
new_name_no_quotes = new_name
new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces
self._hypervisor.send("frsw rename {name} {new_name}".format(name=self._name,
new_name=new_name))
@@ -111,9 +105,7 @@ class FrameRelaySwitch(object):
id=self._id,
new_name=new_name))
self._allocated_names.remove(self.name)
self._name = new_name
self._allocated_names.append(new_name_no_quotes)
@property
def hypervisor(self):
@@ -164,7 +156,7 @@ class FrameRelaySwitch(object):
log.info("Frame Relay switch {name} [id={id}] has been deleted".format(name=self._name,
id=self._id))
self._hypervisor.devices.remove(self)
self._allocated_names.remove(self.name)
self._instances.remove(self._id)
def has_port(self, port):
"""
@@ -282,3 +274,54 @@ class FrameRelaySwitch(object):
port2=port2,
dlci2=dlci2))
del self._mapping[(port1, dlci1)]
def start_capture(self, port, output_file, data_link_type="DLT_FRELAY"):
"""
Starts a packet capture.
:param port: allocated port
:param output_file: PCAP destination file for the capture
:param data_link_type: PCAP data link type (DLT_*), default is DLT_FRELAY
"""
if port not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port))
nio = self._nios[port]
data_link_type = data_link_type.lower()
if data_link_type.startswith("dlt_"):
data_link_type = data_link_type[4:]
if nio.input_filter[0] is not None and nio.output_filter[0] is not None:
raise DynamipsError("Port {} has already a filter applied".format(port))
try:
os.makedirs(os.path.dirname(output_file))
except FileExistsError:
pass
except OSError as e:
raise DynamipsError("Could not create captures directory {}".format(e))
nio.bind_filter("both", "capture")
nio.setup_filter("both", "{} {}".format(data_link_type, output_file))
log.info("Frame relay switch {name} [id={id}]: starting packet capture on {port}".format(name=self._name,
id=self._id,
port=port))
def stop_capture(self, port):
"""
Stops a packet capture.
:param port: allocated port
"""
if port not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port))
nio = self._nios[port]
nio.unbind_filter("both")
log.info("Frame relay switch {name} [id={id}]: stopping packet capture on {port}".format(name=self._name,
id=self._id,
port=port))
+65 -16
View File
@@ -19,6 +19,7 @@
Hub object that uses the Bridge interface to create a hub with ports.
"""
import os
from .bridge import Bridge
from ..dynamips_error import DynamipsError
@@ -34,23 +35,20 @@ class Hub(Bridge):
:param name: name for this hub
"""
_instance_count = 1
_instances = []
def __init__(self, hypervisor, name):
# create an unique ID
self._id = Hub._instance_count
Hub._instance_count += 1
# find an instance identifier (0 < id <= 4096)
self._id = 0
for identifier in range(1, 4097):
if identifier not in self._instances:
self._id = identifier
self._instances.append(self._id)
break
# let's create a unique name if none has been chosen
if not name:
name_id = self._id
while True:
name = "Hub" + str(name_id)
# check if the name has already been allocated to another switch
if name not in self._allocated_names:
break
name_id += 1
if self._id == 0:
raise DynamipsError("Maximum number of instances reached")
self._mapping = {}
Bridge.__init__(self, hypervisor, name)
@@ -61,11 +59,10 @@ class Hub(Bridge):
@classmethod
def reset(cls):
"""
Resets the instance count and the allocated names list.
Resets the instance count and the allocated instances list.
"""
cls._instance_count = 1
cls._allocated_names.clear()
cls._instances.clear()
@property
def id(self):
@@ -95,6 +92,7 @@ class Hub(Bridge):
Bridge.delete(self)
log.info("Ethernet hub {name} [id={id}] has been deleted".format(name=self._name,
id=self._id))
self._instances.remove(self._id)
def add_nio(self, nio, port):
"""
@@ -137,3 +135,54 @@ class Hub(Bridge):
del self._mapping[port]
return nio
def start_capture(self, port, output_file, data_link_type="DLT_EN10MB"):
"""
Starts a packet capture.
:param port: allocated port
:param output_file: PCAP destination file for the capture
:param data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB
"""
if port not in self._mapping:
raise DynamipsError("Port {} is not allocated".format(port))
nio = self._mapping[port]
data_link_type = data_link_type.lower()
if data_link_type.startswith("dlt_"):
data_link_type = data_link_type[4:]
if nio.input_filter[0] is not None and nio.output_filter[0] is not None:
raise DynamipsError("Port {} has already a filter applied".format(port))
try:
os.makedirs(os.path.dirname(output_file))
except FileExistsError:
pass
except OSError as e:
raise DynamipsError("Could not create captures directory {}".format(e))
nio.bind_filter("both", "capture")
nio.setup_filter("both", "{} {}".format(data_link_type, output_file))
log.info("Ethernet hub {name} [id={id}]: starting packet capture on {port}".format(name=self._name,
id=self._id,
port=port))
def stop_capture(self, port):
"""
Stops a packet capture.
:param port: allocated port
"""
if port not in self._mapping:
raise DynamipsError("Port {} is not allocated".format(port))
nio = self._mapping[port]
nio.unbind_filter("both")
log.info("Ethernet hub {name} [id={id}]: stopping packet capture on {port}".format(name=self._name,
id=self._id,
port=port))
+294 -75
View File
@@ -20,11 +20,13 @@ Interface for Dynamips virtual Machine module ("vm")
http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L77
"""
from ..dynamips_hypervisor import DynamipsHypervisor
from ..dynamips_error import DynamipsError
from ...attic import find_unused_port
import time
import sys
import os
import base64
import logging
log = logging.getLogger(__name__)
@@ -36,34 +38,45 @@ class Router(object):
:param hypervisor: Dynamips hypervisor instance
:param name: name for this router
:param router_id: router instance ID
:param platform: c7200, c3745, c3725, c3600, c2691, c2600 or c1700
:param ghost_flag: used when creating a ghost IOS.
"""
_allocated_names = []
_instance_count = 1
_instances = []
_allocated_console_ports = []
_allocated_aux_ports = []
_status = {0: "inactive",
1: "shutting down",
2: "running",
3: "suspended"}
def __init__(self, hypervisor, name=None, platform="c7200", ghost_flag=False):
def __init__(self, hypervisor, name, router_id=None, platform="c7200", ghost_flag=False):
# create an unique ID
self._id = Router._instance_count
Router._instance_count += 1
if not ghost_flag:
# let's create a unique name if none has been chosen
if not name:
name_id = self._id
while True:
name = "R" + str(name_id)
# check if the name has already been allocated to another router
if name not in self._allocated_names:
break
name_id += 1
if not router_id:
# find an instance identifier if none is provided (0 < id <= 4096)
self._id = 0
for identifier in range(1, 4097):
if identifier not in self._instances:
self._id = identifier
self._instances.append(self._id)
break
if self._id == 0:
raise DynamipsError("Maximum number of instances reached")
else:
if router_id in self._instances:
raise DynamipsError("Router identifier {} is already used by another router".format(router_id))
self._id = router_id
self._instances.append(self._id)
else:
log.info("creating a new ghost IOS file")
self._id = 0
name = "Ghost"
self._allocated_names.append(name)
self._hypervisor = hypervisor
self._name = '"' + name + '"' # put name into quotes to protect spaces
self._platform = platform
@@ -103,27 +116,45 @@ class Router(object):
platform=platform,
id=self._id))
# get console and aux ports
self.console = (self._hypervisor.baseconsole - 1) + self._id
self.aux = (self._hypervisor.baseaux - 1) + self._id
try:
# allocate a console port
self._console = find_unused_port(self._hypervisor.console_start_port_range,
self._hypervisor.console_end_port_range,
self._hypervisor.host,
ignore_ports=self._allocated_console_ports)
self._hypervisor.send("vm set_con_tcp_port {name} {console}".format(name=self._name,
console=self._console))
self._allocated_console_ports.append(self._console)
# allocate a auxiliary console port
self._aux = find_unused_port(self._hypervisor.aux_start_port_range,
self._hypervisor.aux_end_port_range,
self._hypervisor.host,
ignore_ports=self._allocated_aux_ports)
self._hypervisor.send("vm set_aux_tcp_port {name} {aux}".format(name=self._name,
aux=self._aux))
self._allocated_aux_ports.append(self._aux)
except Exception as e:
raise DynamipsError(e)
# get the default base MAC address
self._mac_addr = self._hypervisor.send("{platform} get_mac_addr {name}".format(platform=self._platform,
name=self._name))[0]
else:
log.info("creating a new ghost IOS file")
Router._instance_count -= 1
name=self._name))[0]
self._hypervisor.devices.append(self)
@classmethod
def reset(cls):
"""
Resets the instance count and the allocated names list.
Resets the instance count and the allocated instances list.
"""
cls._instance_count = 1
cls._allocated_names.clear()
cls._instances.clear()
cls._allocated_console_ports.clear()
cls._allocated_aux_ports.clear()
def defaults(self):
"""
@@ -195,10 +226,32 @@ class Router(object):
:param new_name: new name string
"""
if new_name in self._allocated_names:
raise DynamipsError('Name "{}" is already used by another router'.format(new_name))
if self._startup_config:
# change the hostname in the startup-config
startup_config_path = os.path.join(self.hypervisor.working_dir, "configs", "i{}_startup-config.cfg".format(self.id))
if os.path.isfile(startup_config_path):
try:
with open(startup_config_path, "r+", errors="replace") as f:
old_config = f.read()
new_config = old_config.replace(self.name, new_name)
f.seek(0)
f.write(new_config)
except OSError as e:
raise DynamipsError("Could not amend the configuration {}: {}".format(startup_config_path, e))
if self._private_config:
# change the hostname in the private-config
private_config_path = os.path.join(self.hypervisor.working_dir, "configs", "i{}_private-config.cfg".format(self.id))
if os.path.isfile(private_config_path):
try:
with open(private_config_path, "r+", errors="replace") as f:
old_config = f.read()
new_config = old_config.replace(self.name, new_name)
f.seek(0)
f.write(new_config)
except OSError as e:
raise DynamipsError("Could not amend the configuration {}: {}".format(private_config_path, e))
new_name_no_quotes = new_name
new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces
self._hypervisor.send("vm rename {name} {new_name}".format(name=self._name,
new_name=new_name))
@@ -206,10 +259,7 @@ class Router(object):
log.info("router {name} [id={id}]: renamed to {new_name}".format(name=self._name,
id=self._id,
new_name=new_name))
self._allocated_names.remove(self.name)
self._name = new_name
self._allocated_names.append(new_name_no_quotes)
@property
def platform(self):
@@ -255,11 +305,43 @@ class Router(object):
Deletes this router.
"""
self._hypervisor.send("vm delete {}".format(self._name))
self._hypervisor.devices.remove(self)
log.info("router {name} [id={id}] has been deleted".format(name=self._name, id=self._id))
if self._id in self._instances:
self._instances.remove(self._id)
if self.console:
self._allocated_console_ports.remove(self.console)
if self.aux:
self._allocated_aux_ports.remove(self.aux)
def clean_delete(self):
"""
Deletes this router & associated files (nvram, disks etc.)
"""
self._hypervisor.send("vm clean_delete {}".format(self._name))
self._hypervisor.devices.remove(self)
log.info("router {name} [id={id}] has been deleted".format(name=self._name, id=self._id))
self._allocated_names.remove(self.name)
if self._startup_config:
# delete the startup-config
startup_config_path = os.path.join(self.hypervisor.working_dir, "configs", "{}.cfg".format(self.name))
if os.path.isfile(startup_config_path):
os.remove(startup_config_path)
if self._private_config:
# delete the private-config
private_config_path = os.path.join(self.hypervisor.working_dir, "configs", "{}-private.cfg".format(self.name))
if os.path.isfile(private_config_path):
os.remove(private_config_path)
log.info("router {name} [id={id}] has been deleted (including associated files)".format(name=self._name, id=self._id))
if self._id in self._instances:
self._instances.remove(self._id)
if self.console:
self._allocated_console_ports.remove(self.console)
if self.aux:
self._allocated_aux_ports.remove(self.aux)
def start(self):
"""
@@ -267,23 +349,27 @@ class Router(object):
At least the IOS image must be set before starting it.
"""
if self.get_status() == "suspended":
status = self.get_status()
if status == "suspended":
self.resume()
else:
elif status == "inactive":
if self.console and self.aux:
# check that console and aux ports are available
try:
#FIXME: use a defined range
DynamipsHypervisor.find_unused_port(self.console, self.console + 100, self._hypervisor.host)
except DynamipsError:
raise DynamipsError("console port {} is not available".format(self.console))
if not os.path.isfile(self._image) or not os.path.exists(self._image):
if os.path.islink(self._image):
raise DynamipsError("IOS image '{}' linked to '{}' is not accessible".format(self._image, os.path.realpath(self._image)))
else:
raise DynamipsError("IOS image '{}' is not accessible".format(self._image))
try:
#FIXME: use a defined range
DynamipsHypervisor.find_unused_port(self.aux, self.aux + 100, self._hypervisor.host)
except DynamipsError:
raise DynamipsError("aux port {} is not available".format(self.aux))
try:
with open(self._image, "rb") as f:
# read the first 7 bytes of the file.
elf_header_start = f.read(7)
except OSError as e:
raise DynamipsError("Cannot read ELF header for IOS image {}: {}".format(self._image, e))
# IOS images must start with the ELF magic number, be 32-bit, big endian and have an ELF version of 1
if elf_header_start != b'\x7fELF\x01\x02\x01':
raise DynamipsError("'{}' is not a valid IOS image".format(self._image))
self._hypervisor.send("vm start {}".format(self._name))
log.info("router {name} [id={id}] has been started".format(name=self._name, id=self._id))
@@ -294,8 +380,9 @@ class Router(object):
The settings are kept.
"""
self._hypervisor.send("vm stop {}".format(self._name))
log.info("router {name} [id={id}] has been stopped".format(name=self._name, id=self._id))
if self.get_status() != "inactive":
self._hypervisor.send("vm stop {}".format(self._name))
log.info("router {name} [id={id}] has been stopped".format(name=self._name, id=self._id))
def suspend(self):
"""
@@ -356,8 +443,11 @@ class Router(object):
if not self._image:
raise DynamipsError("Register an IOS image fist")
self._hypervisor.send("vm set_tsg {name} {group_id}".format(name=self._name,
group_id=group_id))
try:
self._hypervisor.send("vm set_tsg {name} {group_id}".format(name=self._name,
group_id=group_id))
except DynamipsError:
raise DynamipsError("JIT sharing is only supported in Dynamips >= 0.2.8-RC3 unstable")
log.info("router {name} [id={id}]: set in JIT sharing group {group_id}".format(name=self._name,
id=self._id,
@@ -486,10 +576,10 @@ class Router(object):
reply = self._hypervisor.send("vm extract_config {}".format(self._name))[0].rsplit(' ', 2)[-2:]
except IOError:
#for some reason Dynamips gets frozen when it does not find the magic number in the NVRAM file.
return (None, None)
return None, None
startup_config = reply[0][1:-1] # get statup-config and remove single quotes
private_config = reply[1][1:-1] # get private-config and remove single quotes
return (startup_config, private_config)
return startup_config, private_config
def push_config(self, startup_config, private_config='(keep)'):
"""
@@ -512,6 +602,35 @@ class Router(object):
log.info("router {name} [id={id}]: new private-config pushed".format(name=self._name,
id=self._id))
def save_configs(self):
"""
Saves the startup-config and private-config to files.
"""
if self.startup_config or self.private_config:
startup_config_base64, private_config_base64 = self.extract_config()
if startup_config_base64:
try:
config = base64.decodebytes(startup_config_base64.encode("utf-8")).decode("utf-8")
config = "!\n" + config.replace("\r", "")
config_path = os.path.join(self.hypervisor.working_dir, self.startup_config)
with open(config_path, "w") as f:
log.info("saving startup-config to {}".format(self.startup_config))
f.write(config)
except OSError as e:
raise DynamipsError("Could not save the startup configuration {}: {}".format(config_path, e))
if private_config_base64:
try:
config = base64.decodebytes(private_config_base64.encode("utf-8")).decode("utf-8")
config = "!\n" + config.replace("\r", "")
config_path = os.path.join(self.hypervisor.working_dir, self.private_config)
with open(config_path, "w") as f:
log.info("saving private-config to {}".format(self.private_config))
f.write(config)
except OSError as e:
raise DynamipsError("Could not save the private configuration {}: {}".format(config_path, e))
@property
def ram(self):
"""
@@ -632,7 +751,7 @@ class Router(object):
else:
flag = 0
self._hypervisor.send("vm set_sparse_mem {name} {sparsemem}".format(name=self._name,
sparsemem=flag))
sparsemem=flag))
if sparsemem:
log.info("router {name} [id={id}]: sparse memory enabled".format(name=self._name,
@@ -688,6 +807,9 @@ class Router(object):
:param idlepc: idlepc value (string)
"""
if not idlepc:
idlepc = "0x0"
if not self.is_running():
# router is not running
self._hypervisor.send("vm set_idle_pc {name} {idlepc}".format(name=self._name,
@@ -715,10 +837,10 @@ class Router(object):
# router is not running
raise DynamipsError("router {name} is not running".format(name=self._name))
log.info("router {name} [id={id}] has started calculating idle-pc values".format(name=self._name, id=self._id))
log.info("router {name} [id={id}] has started calculating Idle-PC values".format(name=self._name, id=self._id))
begin = time.time()
idlepcs = self._hypervisor.send("vm get_idle_pc_prop {} 0".format(self._name))
log.info("router {name} [id={id}] has finished calculating idle-pc values after {time:.4f} seconds".format(name=self._name,
log.info("router {name} [id={id}] has finished calculating Idle-PC values after {time:.4f} seconds".format(name=self._name,
id=self._id,
time=time.time() - begin))
return idlepcs
@@ -892,7 +1014,7 @@ class Router(object):
translated by the JIT (they contain the native code
corresponding to MIPS code pages).
:param excec_area: exec area value (integer)
:param exec_area: exec area value (integer)
"""
self._hypervisor.send("vm set_exec_area {name} {exec_area}".format(name=self._name,
@@ -1007,6 +1129,9 @@ class Router(object):
if console == self._console:
return
if console in self._allocated_console_ports:
raise DynamipsError("Console port {} is already used by another router".format(console))
self._hypervisor.send("vm set_con_tcp_port {name} {console}".format(name=self._name,
console=console))
@@ -1014,7 +1139,9 @@ class Router(object):
id=self._id,
old_console=self._console,
new_console=console))
self._allocated_console_ports.remove(self._console)
self._console = console
self._allocated_console_ports.append(self._console)
@property
def aux(self):
@@ -1037,6 +1164,9 @@ class Router(object):
if aux == self._aux:
return
if aux in self._allocated_aux_ports:
raise DynamipsError("Auxiliary console port {} is already used by another router".format(aux))
self._hypervisor.send("vm set_aux_tcp_port {name} {aux}".format(name=self._name,
aux=aux))
@@ -1044,7 +1174,10 @@ class Router(object):
id=self._id,
old_aux=self._aux,
new_aux=aux))
self._allocated_aux_ports.remove(self._aux)
self._aux = aux
self._allocated_aux_ports.append(self._aux)
def get_cpu_info(self, cpu_id=0):
"""
@@ -1155,6 +1288,15 @@ class Router(object):
return (self._hypervisor.send("{platform} show_hardware {name}".format(platform=self._platform,
name=self._name)))
def get_cpu_usage(self):
"""
Returns the CPU usage.
:return: CPU usage in percent
"""
return int(self._hypervisor.send("vm cpu_usage {name} 0".format(name=self._name))[0])
def get_slot_bindings(self):
"""
Returns slot bindings.
@@ -1162,7 +1304,7 @@ class Router(object):
:returns: slot bindings (adapter names) list
"""
return (self._hypervisor.send("vm slot_bindings {}".format(self._name)))
return self._hypervisor.send("vm slot_bindings {}".format(self._name))
def slot_add_binding(self, slot_id, adapter):
"""
@@ -1178,12 +1320,19 @@ class Router(object):
raise DynamipsError("Slot {slot_id} doesn't exist on router {name}".format(name=self._name,
slot_id=slot_id))
if slot != None:
if slot is not None:
current_adapter = slot
raise DynamipsError("Slot {slot_id} is already occupied by adapter {adapter} on router {name}".format(name=self._name,
slot_id=slot_id,
adapter=current_adapter))
# Only c7200, c3600 and c3745 (NM-4T only) support new adapter while running
if self.is_running() and not ((self._platform == 'c7200' and not str(adapter).startswith('C7200'))
and not (self._platform == 'c3600' and self.chassis == '3660')
and not (self._platform == 'c3745' and adapter == 'NM-4T')):
raise DynamipsError("Adapter {adapter} cannot be added while router {name} is running".format(adapter=adapter,
name=self._name))
self._hypervisor.send("vm slot_add_binding {name} {slot_id} 0 {adapter}".format(name=self._name,
slot_id=slot_id,
adapter=adapter))
@@ -1195,11 +1344,8 @@ class Router(object):
self._slots[slot_id] = adapter
# Generate an OIR event if the router is running and
# only for c7200, c3600 and c3745 (NM-4T only)
if self.is_running() and (self._platform == 'c7200' \
or (self._platform == 'c3600' and self.chassis == '3660') \
or (self._platform == 'c3745' and adapter == 'NM-4T')):
# Generate an OIR event if the router is running
if self.is_running():
self._hypervisor.send("vm slot_oir_start {name} {slot_id} 0".format(name=self._name,
slot_id=slot_id))
@@ -1221,17 +1367,19 @@ class Router(object):
raise DynamipsError("Slot {slot_id} doesn't exist on router {name}".format(name=self._name,
slot_id=slot_id))
if adapter == None:
if adapter is None:
raise DynamipsError("No adapter in slot {slot_id} on router {name}".format(name=self._name,
slot_id=slot_id))
#FIXME: check if adapter can be removed!
# Only c7200, c3600 and c3745 (NM-4T only) support to remove adapter while running
if self.is_running() and not ((self._platform == 'c7200' and not str(adapter).startswith('C7200'))
and not (self._platform == 'c3600' and self.chassis == '3660')
and not (self._platform == 'c3745' and adapter == 'NM-4T')):
raise DynamipsError("Adapter {adapter} cannot be removed while router {name} is running".format(adapter=adapter,
name=self._name))
# Generate an OIR event if the router is running and
# only for c7200, c3600 and c3745 (NM-4T only)
if self.is_running() and (self._platform == 'c7200' \
or (self._platform == 'c3600' and self.chassis == '3660') \
or (self._platform == 'c3745' and adapter == 'NM-4T')):
# Generate an OIR event if the router is running
if self.is_running():
self._hypervisor.send("vm slot_oir_stop {name} {slot_id} 0".format(name=self._name,
slot_id=slot_id))
@@ -1312,8 +1460,8 @@ class Router(object):
# WIC1 = 16, WIC2 = 32 and WIC3 = 48
internal_wic_slot_id = 16 * (wic_slot_id + 1)
self._hypervisor.send("vm slot_remove_binding {name} {slot_id} {wic_slot_id}".format(name=self._name,
slot_id=slot_id,
wic_slot_id=internal_wic_slot_id))
slot_id=slot_id,
wic_slot_id=internal_wic_slot_id))
log.info("router {name} [id={id}]: {wic} removed from WIC slot {wic_slot_id}".format(name=self._name,
id=self._id,
@@ -1436,6 +1584,77 @@ class Router(object):
slot_id=slot_id,
port_id=port_id))
def start_capture(self, slot_id, port_id, output_file, data_link_type="DLT_EN10MB"):
"""
Starts a packet capture.
:param slot_id: slot ID
:param port_id: port ID
:param output_file: PCAP destination file for the capture
:param data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB
"""
try:
adapter = self._slots[slot_id]
except IndexError:
raise DynamipsError("Slot {slot_id} doesn't exist on router {name}".format(name=self._name,
slot_id=slot_id))
if not adapter.port_exists(port_id):
raise DynamipsError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter,
port_id=port_id))
data_link_type = data_link_type.lower()
if data_link_type.startswith("dlt_"):
data_link_type = data_link_type[4:]
nio = adapter.get_nio(port_id)
if nio.input_filter[0] is not None and nio.output_filter[0] is not None:
raise DynamipsError("Port {port_id} has already a filter applied on {adapter}".format(adapter=adapter,
port_id=port_id))
try:
os.makedirs(os.path.dirname(output_file))
except FileExistsError:
pass
except OSError as e:
raise DynamipsError("Could not create captures directory {}".format(e))
nio.bind_filter("both", "capture")
nio.setup_filter("both", '{} "{}"'.format(data_link_type, output_file))
log.info("router {name} [id={id}]: starting packet capture on port {slot_id}/{port_id}".format(name=self._name,
id=self._id,
nio_name=nio.name,
slot_id=slot_id,
port_id=port_id))
def stop_capture(self, slot_id, port_id):
"""
Stops a packet capture.
:param slot_id: slot ID
:param port_id: port ID
"""
try:
adapter = self._slots[slot_id]
except IndexError:
raise DynamipsError("Slot {slot_id} doesn't exist on router {name}".format(name=self._name,
slot_id=slot_id))
if not adapter.port_exists(port_id):
raise DynamipsError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter,
port_id=port_id))
nio = adapter.get_nio(port_id)
nio.unbind_filter("both")
log.info("router {name} [id={id}]: stopping packet capture on port {slot_id}/{port_id}".format(name=self._name,
id=self._id,
nio_name=nio.name,
slot_id=slot_id,
port_id=port_id))
def _create_slots(self, numslots):
"""
Creates the appropriate number of slots for this router.
Whitespace-only changes.
@@ -0,0 +1,322 @@
# -*- 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/>.
ATMSW_CREATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to create a new ATM switch instance",
"type": "object",
"properties": {
"name": {
"description": "ATM switch name",
"type": "string",
"minLength": 1,
},
},
"additionalProperties": False,
"required": ["name"]
}
ATMSW_DELETE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to delete an ATM switch instance",
"type": "object",
"properties": {
"id": {
"description": "ATM switch instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
ATMSW_UPDATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to update an ATM switch instance",
"type": "object",
"properties": {
"id": {
"description": "ATM switch instance ID",
"type": "integer"
},
"name": {
"description": "ATM switch name",
"type": "string",
"minLength": 1,
},
},
"additionalProperties": False,
"required": ["id"]
}
ATMSW_ALLOCATE_UDP_PORT_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to allocate an UDP port for an ATM switch instance",
"type": "object",
"properties": {
"id": {
"description": "ATM switch instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the ATM switch instance",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id", "port_id"]
}
ATMSW_ADD_NIO_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to add a NIO for an ATM switch instance",
"type": "object",
"definitions": {
"UDP": {
"description": "UDP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_udp"]
},
"lport": {
"description": "Local port",
"type": "integer",
"minimum": 1,
"maximum": 65535
},
"rhost": {
"description": "Remote host",
"type": "string",
"minLength": 1
},
"rport": {
"description": "Remote port",
"type": "integer",
"minimum": 1,
"maximum": 65535
}
},
"required": ["type", "lport", "rhost", "rport"],
"additionalProperties": False
},
"Ethernet": {
"description": "Generic Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_generic_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"LinuxEthernet": {
"description": "Linux Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_linux_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"TAP": {
"description": "TAP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_tap"]
},
"tap_device": {
"description": "TAP device name e.g. tap0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "tap_device"],
"additionalProperties": False
},
"UNIX": {
"description": "UNIX Network Input/Output",
"properties": {
"type": {
"enum": ["nio_unix"]
},
"local_file": {
"description": "path to the UNIX socket file (local)",
"type": "string",
"minLength": 1
},
"remote_file": {
"description": "path to the UNIX socket file (remote)",
"type": "string",
"minLength": 1
},
},
"required": ["type", "local_file", "remote_file"],
"additionalProperties": False
},
"VDE": {
"description": "VDE Network Input/Output",
"properties": {
"type": {
"enum": ["nio_vde"]
},
"control_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
"local_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
},
"required": ["type", "control_file", "local_file"],
"additionalProperties": False
},
"NULL": {
"description": "NULL Network Input/Output",
"properties": {
"type": {
"enum": ["nio_null"]
},
},
"required": ["type"],
"additionalProperties": False
},
},
"properties": {
"id": {
"description": "ATM switch instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the ATM switch instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 1,
},
"mappings": {
"type": "object",
},
"nio": {
"type": "object",
"description": "Network Input/Output",
"oneOf": [
{"$ref": "#/definitions/UDP"},
{"$ref": "#/definitions/Ethernet"},
{"$ref": "#/definitions/LinuxEthernet"},
{"$ref": "#/definitions/TAP"},
{"$ref": "#/definitions/UNIX"},
{"$ref": "#/definitions/VDE"},
{"$ref": "#/definitions/NULL"},
]
},
},
"additionalProperties": False,
"required": ["id", "port", "port_id", "mappings", "nio"],
}
ATMSW_DELETE_NIO_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to delete a NIO for an ATM switch instance",
"type": "object",
"properties": {
"id": {
"description": "ATM switch instance ID",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 1,
},
},
"additionalProperties": False,
"required": ["id", "port"]
}
ATMSW_START_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to start a packet capture on an ATM switch instance port",
"type": "object",
"properties": {
"id": {
"description": "ATM switch instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the ATM switch instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 1,
},
"capture_file_name": {
"description": "Capture file name",
"type": "string",
"minLength": 1,
},
"data_link_type": {
"description": "PCAP data link type",
"type": "string",
"minLength": 1,
},
},
"additionalProperties": False,
"required": ["id", "port_id", "port", "capture_file_name"]
}
ATMSW_STOP_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to stop a packet capture on an ATM switch instance port",
"type": "object",
"properties": {
"id": {
"description": "ATM switch instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the ATM switch instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 1,
},
},
"additionalProperties": False,
"required": ["id", "port_id", "port"]
}
@@ -0,0 +1,319 @@
# -*- 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/>.
ETHHUB_CREATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to create a new Ethernet hub instance",
"type": "object",
"properties": {
"name": {
"description": "Ethernet hub name",
"type": "string",
"minLength": 1,
},
},
"additionalProperties": False,
"required": ["name"]
}
ETHHUB_DELETE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to delete an Ethernet hub instance",
"type": "object",
"properties": {
"id": {
"description": "Ethernet hub instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
ETHHUB_UPDATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to update an Ethernet hub instance",
"type": "object",
"properties": {
"id": {
"description": "Ethernet hub instance ID",
"type": "integer"
},
"name": {
"description": "Ethernet hub name",
"type": "string",
"minLength": 1,
},
},
"additionalProperties": False,
"required": ["id"]
}
ETHHUB_ALLOCATE_UDP_PORT_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to allocate an UDP port for an Ethernet hub instance",
"type": "object",
"properties": {
"id": {
"description": "Ethernet hub instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the Ethernet hub instance",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id", "port_id"]
}
ETHHUB_ADD_NIO_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to add a NIO for an Ethernet hub instance",
"type": "object",
"definitions": {
"UDP": {
"description": "UDP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_udp"]
},
"lport": {
"description": "Local port",
"type": "integer",
"minimum": 1,
"maximum": 65535
},
"rhost": {
"description": "Remote host",
"type": "string",
"minLength": 1
},
"rport": {
"description": "Remote port",
"type": "integer",
"minimum": 1,
"maximum": 65535
}
},
"required": ["type", "lport", "rhost", "rport"],
"additionalProperties": False
},
"Ethernet": {
"description": "Generic Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_generic_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"LinuxEthernet": {
"description": "Linux Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_linux_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"TAP": {
"description": "TAP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_tap"]
},
"tap_device": {
"description": "TAP device name e.g. tap0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "tap_device"],
"additionalProperties": False
},
"UNIX": {
"description": "UNIX Network Input/Output",
"properties": {
"type": {
"enum": ["nio_unix"]
},
"local_file": {
"description": "path to the UNIX socket file (local)",
"type": "string",
"minLength": 1
},
"remote_file": {
"description": "path to the UNIX socket file (remote)",
"type": "string",
"minLength": 1
},
},
"required": ["type", "local_file", "remote_file"],
"additionalProperties": False
},
"VDE": {
"description": "VDE Network Input/Output",
"properties": {
"type": {
"enum": ["nio_vde"]
},
"control_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
"local_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
},
"required": ["type", "control_file", "local_file"],
"additionalProperties": False
},
"NULL": {
"description": "NULL Network Input/Output",
"properties": {
"type": {
"enum": ["nio_null"]
},
},
"required": ["type"],
"additionalProperties": False
},
},
"properties": {
"id": {
"description": "Ethernet hub instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the Ethernet hub instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 1,
},
"nio": {
"type": "object",
"description": "Network Input/Output",
"oneOf": [
{"$ref": "#/definitions/UDP"},
{"$ref": "#/definitions/Ethernet"},
{"$ref": "#/definitions/LinuxEthernet"},
{"$ref": "#/definitions/TAP"},
{"$ref": "#/definitions/UNIX"},
{"$ref": "#/definitions/VDE"},
{"$ref": "#/definitions/NULL"},
]
},
},
"additionalProperties": False,
"required": ["id", "port_id", "port", "nio"]
}
ETHHUB_DELETE_NIO_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to delete a NIO for an Ethernet hub instance",
"type": "object",
"properties": {
"id": {
"description": "Ethernet hub instance ID",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 1,
},
},
"additionalProperties": False,
"required": ["id", "port"]
}
ETHHUB_START_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to start a packet capture on an Ethernet hub instance port",
"type": "object",
"properties": {
"id": {
"description": "Ethernet hub instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the Ethernet hub instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 1,
},
"capture_file_name": {
"description": "Capture file name",
"type": "string",
"minLength": 1,
},
"data_link_type": {
"description": "PCAP data link type",
"type": "string",
"minLength": 1,
},
},
"additionalProperties": False,
"required": ["id", "port_id", "port", "capture_file_name"]
}
ETHHUB_STOP_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to stop a packet capture on an Ethernet hub instance port",
"type": "object",
"properties": {
"id": {
"description": "Ethernet hub instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the Ethernet hub instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 1,
},
},
"additionalProperties": False,
"required": ["id", "port_id", "port"]
}
@@ -0,0 +1,348 @@
# -*- 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/>.
ETHSW_CREATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to create a new Ethernet switch instance",
"type": "object",
"properties": {
"name": {
"description": "Ethernet switch name",
"type": "string",
"minLength": 1,
},
},
"additionalProperties": False,
"required": ["name"]
}
ETHSW_DELETE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to delete an Ethernet switch instance",
"type": "object",
"properties": {
"id": {
"description": "Ethernet switch instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
#TODO: ports {'1': {'vlan': 1, 'type': 'qinq'}
ETHSW_UPDATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to update an Ethernet switch instance",
"type": "object",
"properties": {
"id": {
"description": "Ethernet switch instance ID",
"type": "integer"
},
"name": {
"description": "Ethernet switch name",
"type": "string",
"minLength": 1,
},
# "ports": {
# "type": "object",
# "properties": {
# "type": {
# "description": "Port type",
# "enum": ["access", "dot1q", "qinq"],
# },
# "vlan": {
# "description": "VLAN number",
# "type": "integer",
# "minimum": 1
# },
# },
# },
},
#"additionalProperties": False,
"required": ["id"]
}
ETHSW_ALLOCATE_UDP_PORT_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to allocate an UDP port for an Ethernet switch instance",
"type": "object",
"properties": {
"id": {
"description": "Ethernet switch instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the Ethernet switch instance",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id", "port_id"]
}
ETHSW_ADD_NIO_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to add a NIO for an Ethernet switch instance",
"type": "object",
"definitions": {
"UDP": {
"description": "UDP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_udp"]
},
"lport": {
"description": "Local port",
"type": "integer",
"minimum": 1,
"maximum": 65535
},
"rhost": {
"description": "Remote host",
"type": "string",
"minLength": 1
},
"rport": {
"description": "Remote port",
"type": "integer",
"minimum": 1,
"maximum": 65535
}
},
"required": ["type", "lport", "rhost", "rport"],
"additionalProperties": False
},
"Ethernet": {
"description": "Generic Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_generic_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"LinuxEthernet": {
"description": "Linux Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_linux_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"TAP": {
"description": "TAP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_tap"]
},
"tap_device": {
"description": "TAP device name e.g. tap0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "tap_device"],
"additionalProperties": False
},
"UNIX": {
"description": "UNIX Network Input/Output",
"properties": {
"type": {
"enum": ["nio_unix"]
},
"local_file": {
"description": "path to the UNIX socket file (local)",
"type": "string",
"minLength": 1
},
"remote_file": {
"description": "path to the UNIX socket file (remote)",
"type": "string",
"minLength": 1
},
},
"required": ["type", "local_file", "remote_file"],
"additionalProperties": False
},
"VDE": {
"description": "VDE Network Input/Output",
"properties": {
"type": {
"enum": ["nio_vde"]
},
"control_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
"local_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
},
"required": ["type", "control_file", "local_file"],
"additionalProperties": False
},
"NULL": {
"description": "NULL Network Input/Output",
"properties": {
"type": {
"enum": ["nio_null"]
},
},
"required": ["type"],
"additionalProperties": False
},
},
"properties": {
"id": {
"description": "Ethernet switch instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the Ethernet switch instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 1,
},
"port_type": {
"description": "Port type",
"enum": ["access", "dot1q", "qinq"],
},
"vlan": {
"description": "VLAN number",
"type": "integer",
"minimum": 1
},
"nio": {
"type": "object",
"description": "Network Input/Output",
"oneOf": [
{"$ref": "#/definitions/UDP"},
{"$ref": "#/definitions/Ethernet"},
{"$ref": "#/definitions/LinuxEthernet"},
{"$ref": "#/definitions/TAP"},
{"$ref": "#/definitions/UNIX"},
{"$ref": "#/definitions/VDE"},
{"$ref": "#/definitions/NULL"},
]
},
},
"additionalProperties": False,
"required": ["id", "port_id", "port", "port_type", "vlan", "nio"],
"dependencies": {
"port_type": ["vlan"],
"vlan": ["port_type"]
}
}
ETHSW_DELETE_NIO_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to delete a NIO for an Ethernet switch instance",
"type": "object",
"properties": {
"id": {
"description": "Ethernet switch instance ID",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 1,
},
},
"additionalProperties": False,
"required": ["id", "port"]
}
ETHSW_START_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to start a packet capture on an Ethernet switch instance port",
"type": "object",
"properties": {
"id": {
"description": "Ethernet switch instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the Ethernet switch instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 1,
},
"capture_file_name": {
"description": "Capture file name",
"type": "string",
"minLength": 1,
},
"data_link_type": {
"description": "PCAP data link type",
"type": "string",
"minLength": 1,
},
},
"additionalProperties": False,
"required": ["id", "port_id", "port", "capture_file_name"]
}
ETHSW_STOP_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to stop a packet capture on an Ethernet switch instance port",
"type": "object",
"properties": {
"id": {
"description": "Ethernet switch instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the Ethernet switch instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 1,
},
},
"additionalProperties": False,
"required": ["id", "port_id", "port"]
}
+322
View File
@@ -0,0 +1,322 @@
# -*- 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/>.
FRSW_CREATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to create a new Frame relay switch instance",
"type": "object",
"properties": {
"name": {
"description": "Frame relay switch name",
"type": "string",
"minLength": 1,
},
},
"additionalProperties": False,
"required": ["name"]
}
FRSW_DELETE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to delete a Frame relay switch instance",
"type": "object",
"properties": {
"id": {
"description": "Frame relay switch instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
FRSW_UPDATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to update a Frame relay switch instance",
"type": "object",
"properties": {
"id": {
"description": "Frame relay switch instance ID",
"type": "integer"
},
"name": {
"description": "Frame relay switch name",
"type": "string",
"minLength": 1,
},
},
"additionalProperties": False,
"required": ["id"]
}
FRSW_ALLOCATE_UDP_PORT_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to allocate an UDP port for a Frame relay switch instance",
"type": "object",
"properties": {
"id": {
"description": "Frame relay switch instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the Frame relay switch instance",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id", "port_id"]
}
FRSW_ADD_NIO_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to add a NIO for a Frame relay switch instance",
"type": "object",
"definitions": {
"UDP": {
"description": "UDP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_udp"]
},
"lport": {
"description": "Local port",
"type": "integer",
"minimum": 1,
"maximum": 65535
},
"rhost": {
"description": "Remote host",
"type": "string",
"minLength": 1
},
"rport": {
"description": "Remote port",
"type": "integer",
"minimum": 1,
"maximum": 65535
}
},
"required": ["type", "lport", "rhost", "rport"],
"additionalProperties": False
},
"Ethernet": {
"description": "Generic Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_generic_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"LinuxEthernet": {
"description": "Linux Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_linux_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"TAP": {
"description": "TAP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_tap"]
},
"tap_device": {
"description": "TAP device name e.g. tap0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "tap_device"],
"additionalProperties": False
},
"UNIX": {
"description": "UNIX Network Input/Output",
"properties": {
"type": {
"enum": ["nio_unix"]
},
"local_file": {
"description": "path to the UNIX socket file (local)",
"type": "string",
"minLength": 1
},
"remote_file": {
"description": "path to the UNIX socket file (remote)",
"type": "string",
"minLength": 1
},
},
"required": ["type", "local_file", "remote_file"],
"additionalProperties": False
},
"VDE": {
"description": "VDE Network Input/Output",
"properties": {
"type": {
"enum": ["nio_vde"]
},
"control_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
"local_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
},
"required": ["type", "control_file", "local_file"],
"additionalProperties": False
},
"NULL": {
"description": "NULL Network Input/Output",
"properties": {
"type": {
"enum": ["nio_null"]
},
},
"required": ["type"],
"additionalProperties": False
},
},
"properties": {
"id": {
"description": "Frame relay switch instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the Frame relay switch instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 1,
},
"mappings": {
"type": "object",
},
"nio": {
"type": "object",
"description": "Network Input/Output",
"oneOf": [
{"$ref": "#/definitions/UDP"},
{"$ref": "#/definitions/Ethernet"},
{"$ref": "#/definitions/LinuxEthernet"},
{"$ref": "#/definitions/TAP"},
{"$ref": "#/definitions/UNIX"},
{"$ref": "#/definitions/VDE"},
{"$ref": "#/definitions/NULL"},
]
},
},
"additionalProperties": False,
"required": ["id", "port", "port_id", "mappings", "nio"],
}
FRSW_DELETE_NIO_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to delete a NIO for a Frame relay switch instance",
"type": "object",
"properties": {
"id": {
"description": "Frame relay switch instance ID",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 1,
},
},
"additionalProperties": False,
"required": ["id", "port"]
}
FRSW_START_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to start a packet capture on a Frame relay switch instance port",
"type": "object",
"properties": {
"id": {
"description": "Frame relay switch instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the Frame relay instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 1,
},
"capture_file_name": {
"description": "Capture file name",
"type": "string",
"minLength": 1,
},
"data_link_type": {
"description": "PCAP data link type",
"type": "string",
"minLength": 1,
},
},
"additionalProperties": False,
"required": ["id", "port_id", "port", "capture_file_name"]
}
FRSW_STOP_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to stop a packet capture on a Frame relay switch instance port",
"type": "object",
"properties": {
"id": {
"description": "Frame relay switch instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the Frame relay instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 1,
},
},
"additionalProperties": False,
"required": ["id", "port_id", "port"]
}
+719
View File
@@ -0,0 +1,719 @@
# -*- 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/>.
VM_CREATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to create a new VM instance",
"type": "object",
"properties": {
"name": {
"description": "Router name",
"type": "string",
"minLength": 1,
},
"router_id": {
"description": "VM/router instance ID",
"type": "integer"
},
"platform": {
"description": "router platform",
"type": "string",
"minLength": 1,
"pattern": "^c[0-9]{4}$"
},
"chassis": {
"description": "router chassis model",
"type": "string",
"minLength": 1,
"pattern": "^[0-9]{4}(XM)?$"
},
"image": {
"description": "path to the IOS image file",
"type": "string",
"minLength": 1
},
"ram": {
"description": "amount of RAM in MB",
"type": "integer"
},
"console": {
"description": "console TCP port",
"type": "integer",
"minimum": 1,
"maximum": 65535
},
"aux": {
"description": "auxiliary console TCP port",
"type": "integer",
"minimum": 1,
"maximum": 65535
},
"mac_addr": {
"description": "base MAC address",
"type": "string",
"minLength": 1,
"pattern": "^([0-9a-fA-F]{4}\\.){2}[0-9a-fA-F]{4}$"
},
"cloud_path": {
"description": "Path to the image in the cloud object store",
"type": "string",
}
},
"additionalProperties": False,
"required": ["name", "platform", "image", "ram"]
}
VM_DELETE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to delete a VM instance",
"type": "object",
"properties": {
"id": {
"description": "VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
VM_START_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to start a VM instance",
"type": "object",
"properties": {
"id": {
"description": "VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
VM_STOP_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to stop a VM instance",
"type": "object",
"properties": {
"id": {
"description": "VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
VM_SUSPEND_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to suspend a VM instance",
"type": "object",
"properties": {
"id": {
"description": "VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
VM_RELOAD_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to reload a VM instance",
"type": "object",
"properties": {
"id": {
"description": "VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
#TODO: improve platform specific properties (dependencies?)
VM_UPDATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to update a VM instance",
"type": "object",
"properties": {
"id": {
"description": "VM instance ID",
"type": "integer"
},
"name": {
"description": "Router name",
"type": "string",
"minLength": 1,
},
"platform": {
"description": "platform",
"type": "string",
"minLength": 1,
"pattern": "^c[0-9]{4}$"
},
"image": {
"description": "path to the IOS image",
"type": "string",
"minLength": 1,
},
"startup_config": {
"description": "path to the IOS startup configuration file",
"type": "string",
"minLength": 1,
},
"private_config": {
"description": "path to the IOS private configuration file",
"type": "string",
"minLength": 1,
},
"ram": {
"description": "amount of RAM in MB",
"type": "integer"
},
"nvram": {
"description": "amount of NVRAM in KB",
"type": "integer"
},
"mmap": {
"description": "MMAP feature",
"type": "boolean"
},
"sparsemem": {
"description": "sparse memory feature",
"type": "boolean"
},
"clock_divisor": {
"description": "clock divisor",
"type": "integer"
},
"idlepc": {
"description": "Idle-PC value",
"type": "string",
"pattern": "^(0x[0-9a-fA-F]+)?$"
},
"idlemax": {
"description": "idlemax value",
"type": "integer",
},
"idlesleep": {
"description": "idlesleep value",
"type": "integer",
},
"exec_area": {
"description": "exec area value",
"type": "integer",
},
"jit_sharing_group": {
"description": "JIT sharing group",
"type": "integer",
},
"disk0": {
"description": "disk0 size in MB",
"type": "integer"
},
"disk1": {
"description": "disk1 size in MB",
"type": "integer"
},
"confreg": {
"description": "configuration register",
"type": "string",
"minLength": 1,
"pattern": "^0x[0-9a-fA-F]{4}$"
},
"console": {
"description": "console TCP port",
"type": "integer",
"minimum": 1,
"maximum": 65535
},
"aux": {
"description": "auxiliary console TCP port",
"type": "integer",
"minimum": 1,
"maximum": 65535
},
"mac_addr": {
"description": "base MAC address",
"type": "string",
"minLength": 1,
"pattern": "^([0-9a-fA-F]{4}\\.){2}[0-9a-fA-F]{4}$"
},
"system_id": {
"description": "system ID",
"type": "string",
"minLength": 1,
},
"slot0": {
"description": "Network module slot 0",
"oneOf": [
{"type": "string"},
{"type": "null"}
]
},
"slot1": {
"description": "Network module slot 1",
"oneOf": [
{"type": "string"},
{"type": "null"}
]
},
"slot2": {
"description": "Network module slot 2",
"oneOf": [
{"type": "string"},
{"type": "null"}
]
},
"slot3": {
"description": "Network module slot 3",
"oneOf": [
{"type": "string"},
{"type": "null"}
]
},
"slot4": {
"description": "Network module slot 4",
"oneOf": [
{"type": "string"},
{"type": "null"}
]
},
"slot5": {
"description": "Network module slot 5",
"oneOf": [
{"type": "string"},
{"type": "null"}
]
},
"slot6": {
"description": "Network module slot 6",
"oneOf": [
{"type": "string"},
{"type": "null"}
]
},
"wic0": {
"description": "Network module WIC slot 0",
"oneOf": [
{"type": "string"},
{"type": "null"}
]
},
"wic1": {
"description": "Network module WIC slot 0",
"oneOf": [
{"type": "string"},
{"type": "null"}
]
},
"wic2": {
"description": "Network module WIC slot 0",
"oneOf": [
{"type": "string"},
{"type": "null"}
]
},
"startup_config_base64": {
"description": "startup configuration base64 encoded",
"type": "string"
},
"private_config_base64": {
"description": "private configuration base64 encoded",
"type": "string"
},
# C7200 properties
"npe": {
"description": "NPE model",
"enum": ["npe-100",
"npe-150",
"npe-175",
"npe-200",
"npe-225",
"npe-300",
"npe-400",
"npe-g2"]
},
"midplane": {
"description": "Midplane model",
"enum": ["std", "vxr"]
},
"sensors": {
"description": "Temperature sensors",
"type": "array"
},
"power_supplies": {
"description": "Power supplies status",
"type": "array"
},
# I/O memory property for all platforms but C7200
"iomem": {
"description": "I/O memory percentage",
"type": "integer",
"minimum": 0,
"maximum": 100
},
},
"additionalProperties": False,
"required": ["id"]
}
VM_START_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to start a packet capture on a VM instance port",
"type": "object",
"properties": {
"id": {
"description": "VM instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the VM instance",
"type": "integer"
},
"slot": {
"description": "Slot number",
"type": "integer",
"minimum": 0,
"maximum": 6
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 0,
"maximum": 49 # maximum is 16 for regular port numbers, WICs port numbers start at 16, 32 or 48
},
"capture_file_name": {
"description": "Capture file name",
"type": "string",
"minLength": 1,
},
"data_link_type": {
"description": "PCAP data link type",
"type": "string",
"minLength": 1,
},
},
"additionalProperties": False,
"required": ["id", "port_id", "slot", "port", "capture_file_name"]
}
VM_STOP_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to stop a packet capture on a VM instance port",
"type": "object",
"properties": {
"id": {
"description": "VM instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the VM instance",
"type": "integer"
},
"slot": {
"description": "Slot number",
"type": "integer",
"minimum": 0,
"maximum": 6
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 0,
"maximum": 49 # maximum is 16 for regular port numbers, WICs port numbers start at 16, 32 or 48
},
},
"additionalProperties": False,
"required": ["id", "port_id", "slot", "port"]
}
VM_SAVE_CONFIG_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to save the configs for VM instance",
"type": "object",
"properties": {
"id": {
"description": "VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
VM_EXPORT_CONFIG_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to export the configs for VM instance",
"type": "object",
"properties": {
"id": {
"description": "VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
VM_IDLEPCS_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to calculate or show Idle-PCs for VM instance",
"type": "object",
"properties": {
"id": {
"description": "VM instance ID",
"type": "integer"
},
"compute": {
"description": "indicates to compute new Idle-PC values",
"type": "boolean"
},
},
"additionalProperties": False,
"required": ["id"]
}
VM_AUTO_IDLEPC_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request an auto Idle-PC calculation for this VM instance",
"type": "object",
"properties": {
"id": {
"description": "VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
VM_ALLOCATE_UDP_PORT_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to allocate an UDP port for a VM instance",
"type": "object",
"properties": {
"id": {
"description": "VM instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the VM instance",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id", "port_id"]
}
VM_ADD_NIO_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to add a NIO for a VM instance",
"type": "object",
"definitions": {
"UDP": {
"description": "UDP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_udp"]
},
"lport": {
"description": "Local port",
"type": "integer",
"minimum": 1,
"maximum": 65535
},
"rhost": {
"description": "Remote host",
"type": "string",
"minLength": 1
},
"rport": {
"description": "Remote port",
"type": "integer",
"minimum": 1,
"maximum": 65535
}
},
"required": ["type", "lport", "rhost", "rport"],
"additionalProperties": False
},
"Ethernet": {
"description": "Generic Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_generic_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"LinuxEthernet": {
"description": "Linux Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_linux_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"TAP": {
"description": "TAP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_tap"]
},
"tap_device": {
"description": "TAP device name e.g. tap0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "tap_device"],
"additionalProperties": False
},
"UNIX": {
"description": "UNIX Network Input/Output",
"properties": {
"type": {
"enum": ["nio_unix"]
},
"local_file": {
"description": "path to the UNIX socket file (local)",
"type": "string",
"minLength": 1
},
"remote_file": {
"description": "path to the UNIX socket file (remote)",
"type": "string",
"minLength": 1
},
},
"required": ["type", "local_file", "remote_file"],
"additionalProperties": False
},
"VDE": {
"description": "VDE Network Input/Output",
"properties": {
"type": {
"enum": ["nio_vde"]
},
"control_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
"local_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
},
"required": ["type", "control_file", "local_file"],
"additionalProperties": False
},
"NULL": {
"description": "NULL Network Input/Output",
"properties": {
"type": {
"enum": ["nio_null"]
},
},
"required": ["type"],
"additionalProperties": False
},
},
"properties": {
"id": {
"description": "VM instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the VM instance",
"type": "integer"
},
"slot": {
"description": "Slot number",
"type": "integer",
"minimum": 0,
"maximum": 6
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 0,
"maximum": 49 # maximum is 16 for regular port numbers, WICs port numbers start at 16, 32 or 48
},
"nio": {
"type": "object",
"description": "Network Input/Output",
"oneOf": [
{"$ref": "#/definitions/UDP"},
{"$ref": "#/definitions/Ethernet"},
{"$ref": "#/definitions/LinuxEthernet"},
{"$ref": "#/definitions/TAP"},
{"$ref": "#/definitions/UNIX"},
{"$ref": "#/definitions/VDE"},
{"$ref": "#/definitions/NULL"},
]
},
},
"additionalProperties": False,
"required": ["id", "port_id", "slot", "port", "nio"]
}
VM_DELETE_NIO_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to delete a NIO for a VM instance",
"type": "object",
"properties": {
"id": {
"description": "VM instance ID",
"type": "integer"
},
"slot": {
"description": "Slot number",
"type": "integer",
"minimum": 0,
"maximum": 6
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 0,
"maximum": 49 # maximum is 16 for regular port numbers, WICs port numbers start at 16, 32 or 48
},
},
"additionalProperties": False,
"required": ["id", "slot", "port"]
}
+376 -209
View File
@@ -20,21 +20,36 @@ IOU server module.
"""
import os
import sys
import base64
import ntpath
import stat
import tempfile
import fcntl
import struct
import socket
import shutil
from gns3server.modules import IModule
from gns3server.config import Config
from gns3dms.cloud.rackspace_ctrl import get_provider
from .iou_device import IOUDevice
from .iou_error import IOUError
from .nios.nio_udp import NIO_UDP
from .nios.nio_tap import NIO_TAP
from .nios.nio_generic_ethernet import NIO_GenericEthernet
import gns3server.jsonrpc as jsonrpc
from ..attic import find_unused_port
from ..attic import has_privileged_access
from .schemas import IOU_CREATE_SCHEMA
from .schemas import IOU_DELETE_SCHEMA
from .schemas import IOU_UPDATE_SCHEMA
from .schemas import IOU_START_SCHEMA
from .schemas import IOU_STOP_SCHEMA
from .schemas import IOU_RELOAD_SCHEMA
from .schemas import IOU_ALLOCATE_UDP_PORT_SCHEMA
from .schemas import IOU_ADD_NIO_SCHEMA
from .schemas import IOU_DELETE_NIO_SCHEMA
from .schemas import IOU_START_CAPTURE_SCHEMA
from .schemas import IOU_STOP_CAPTURE_SCHEMA
from .schemas import IOU_EXPORT_CONFIG_SCHEMA
import logging
log = logging.getLogger(__name__)
@@ -54,20 +69,17 @@ class IOU(IModule):
# get the iouyap location
config = Config.instance()
iou_config = config.get_section_config(name.upper())
self._iouyap = iou_config.get("iouyap")
self._iouyap = iou_config.get("iouyap_path")
if not self._iouyap or not os.path.isfile(self._iouyap):
iouyap_in_cwd = os.path.join(os.getcwd(), "iouyap")
if os.path.isfile(iouyap_in_cwd):
self._iouyap = iouyap_in_cwd
else:
# look for iouyap if none is defined or accessible
for path in os.environ["PATH"].split(":"):
try:
if "iouyap" in os.listdir(path) and os.access(os.path.join(path, "iouyap"), os.X_OK):
self._iouyap = os.path.join(path, "iouyap")
break
except OSError:
continue
paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep)
# look for iouyap in the current working directory and $PATH
for path in paths:
try:
if "iouyap" in os.listdir(path) and os.access(os.path.join(path, "iouyap"), os.X_OK):
self._iouyap = os.path.join(path, "iouyap")
break
except OSError:
continue
if not self._iouyap:
log.warning("iouyap binary couldn't be found!")
@@ -77,34 +89,40 @@ class IOU(IModule):
# a new process start when calling IModule
IModule.__init__(self, name, *args, **kwargs)
self._iou_instances = {}
self._console_start_port_range = 4001
self._console_end_port_range = 4512
self._current_console_port = self._console_start_port_range
self._udp_start_port_range = 30001
self._udp_end_port_range = 40001
self._current_udp_port = self._udp_start_port_range
self._host = kwargs["host"]
self._console_start_port_range = iou_config.get("console_start_port_range", 4001)
self._console_end_port_range = iou_config.get("console_end_port_range", 4500)
self._allocated_udp_ports = []
self._udp_start_port_range = iou_config.get("udp_start_port_range", 30001)
self._udp_end_port_range = iou_config.get("udp_end_port_range", 35000)
self._host = iou_config.get("host", kwargs["host"])
self._console_host = iou_config.get("console_host", kwargs["console_host"])
self._projects_dir = kwargs["projects_dir"]
self._tempdir = kwargs["temp_dir"]
self._working_dir = self._projects_dir
self._server_iourc_path = iou_config.get("iourc", "")
self._iourc = ""
# check every 5 seconds
self._iou_callback = self.add_periodic_callback(self._check_iou_is_alive, 5000)
self._iou_callback.start()
def stop(self):
def stop(self, signum=None):
"""
Properly stops the module.
:param signum: signal number (if called by the signal handler)
"""
self._iou_callback.stop()
# delete all IOU instances
for iou_id in self._iou_instances:
iou_instance = self._iou_instances[iou_id]
iou_instance.delete()
IModule.stop(self) # this will stop the I/O loop
self.delete_iourc_file()
IModule.stop(self, signum) # this will stop the I/O loop
def _check_iou_is_alive(self):
"""
@@ -132,12 +150,39 @@ class IOU(IModule):
self.send_notification("{}.iouyap_stopped".format(self.name), notification)
iou_instance.stop()
@IModule.route("iou.reset")
def reset(self, request):
def get_iou_instance(self, iou_id):
"""
Resets the module.
Returns an IOU device instance.
:param request: JSON request
:param iou_id: IOU device identifier
:returns: IOUDevice instance
"""
if iou_id not in self._iou_instances:
log.debug("IOU device ID {} doesn't exist".format(iou_id), exc_info=1)
self.send_custom_error("IOU device ID {} doesn't exist".format(iou_id))
return None
return self._iou_instances[iou_id]
def delete_iourc_file(self):
"""
Deletes the IOURC file.
"""
if self._iourc and os.path.isfile(self._iourc):
try:
log.info("deleting iourc file {}".format(self._iourc))
os.remove(self._iourc)
except OSError as e:
log.warn("could not delete iourc file {}: {}".format(self._iourc, e))
@IModule.route("iou.reset")
def reset(self, request=None):
"""
Resets the module (JSON-RPC notification).
:param request: JSON request (not used)
"""
# delete all IOU instances
@@ -149,17 +194,10 @@ class IOU(IModule):
IOUDevice.reset()
self._iou_instances.clear()
self._remote_server = False
self._current_console_port = self._console_start_port_range
self._current_udp_port = self._udp_start_port_range
if self._iourc and os.path.isfile(self._iourc):
try:
log.info("deleting iourc file {}".format(self._iourc))
os.remove(self._iourc)
except OSError as e:
log.warn("could not delete iourc file {}: {}".format(self._iourc, e))
self._allocated_udp_ports.clear()
self.delete_iourc_file()
self._working_dir = self._projects_dir
log.info("IOU module has been reset")
@IModule.route("iou.settings")
@@ -171,6 +209,7 @@ class IOU(IModule):
- iourc (base64 encoded iourc file)
Optional request parameters:
- iouyap (path to iouyap)
- working_dir (path to a working directory)
- project_name
- console_start_port_range
@@ -181,12 +220,12 @@ class IOU(IModule):
:param request: JSON request
"""
if request == None:
if request is None:
self.send_param_error()
return
if "iourc" in request:
iourc_content = base64.decodestring(request["iourc"].encode("utf-8")).decode("utf-8")
iourc_content = base64.decodebytes(request["iourc"].encode("utf-8")).decode("utf-8")
iourc_content = iourc_content.replace("\r\n", "\n") # dos2unix
try:
with tempfile.NamedTemporaryFile(mode="w", delete=False) as f:
@@ -194,7 +233,7 @@ class IOU(IModule):
f.write(iourc_content)
self._iourc = f.name
except OSError as e:
raise IOUError("Could not save iourc file to {}: {}".format(f.name, e))
raise IOUError("Could not create the iourc file: {}".format(e))
if "iouyap" in request and request["iouyap"]:
self._iouyap = request["iouyap"]
@@ -221,7 +260,7 @@ class IOU(IModule):
self._working_dir = new_working_dir
for iou_id in self._iou_instances:
iou_instance = self._iou_instances[iou_id]
iou_instance.working_dir = self._working_dir
iou_instance.working_dir = os.path.join(self._working_dir, "iou", "device-{}".format(iou_instance.id))
if "console_start_port_range" in request and "console_end_port_range" in request:
self._console_start_port_range = request["console_start_port_range"]
@@ -238,38 +277,60 @@ class IOU(IModule):
"""
Creates a new IOU instance.
Mandatory request parameters:
- path (path to the IOU executable)
Optional request parameters:
- name (IOU name)
- path (path to the IOU executable)
- console (IOU console port)
Response parameters:
- id (IOU instance identifier)
- name (IOU name)
- default settings
:param request: JSON request
"""
#TODO: JSON schema validation for the request
name = None
if request and "name" in request:
name = request["name"]
# validate the request
if not self.validate_request(request, IOU_CREATE_SCHEMA):
return
name = request["name"]
iou_path = request["path"]
console = request.get("console")
iou_id = request.get("iou_id")
updated_iou_path = os.path.join(self.images_directory, iou_path)
if os.path.isfile(updated_iou_path):
iou_path = updated_iou_path
else:
if not os.path.exists(self.images_directory):
os.mkdir(self.images_directory)
cloud_path = request.get("cloud_path", None)
if cloud_path is not None:
# Download the image from cloud files
_, filename = ntpath.split(iou_path)
src = '{}/{}'.format(cloud_path, filename)
provider = get_provider(self._cloud_settings)
log.debug("Downloading file from {} to {}...".format(src, updated_iou_path))
provider.download_file(src, updated_iou_path)
log.debug("Download of {} complete.".format(src))
# Make file executable
st = os.stat(updated_iou_path)
os.chmod(updated_iou_path, st.st_mode | stat.S_IEXEC)
iou_path = updated_iou_path
try:
try:
os.makedirs(self._working_dir)
except FileExistsError:
pass
except OSError as e:
raise IOUError("Could not create working directory {}".format(e))
iou_instance = IOUDevice(name,
iou_path,
self._working_dir,
iou_id,
console,
self._console_host,
self._console_start_port_range,
self._console_end_port_range)
iou_instance = IOUDevice(iou_path, self._working_dir, host=self._host, name=name)
# find a console port
if self._current_console_port >= self._console_end_port_range:
self._current_console_port = self._console_start_port_range
iou_instance.console = IOUDevice.find_unused_port(self._current_console_port, self._console_end_port_range, self._host)
self._current_console_port += 1
except IOUError as e:
self.send_custom_error(str(e))
return
@@ -290,31 +351,29 @@ class IOU(IModule):
Mandatory request parameters:
- id (IOU instance identifier)
Response parameters:
- same as original request
Response parameter:
- True on success
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, IOU_DELETE_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
iou_id = request["id"]
if iou_id not in self._iou_instances:
self.send_custom_error("IOU device id {} doesn't exist".format(iou_id))
# get the instance
iou_instance = self.get_iou_instance(request["id"])
if not iou_instance:
return
iou_instance = self._iou_instances[iou_id]
try:
iou_instance.delete()
del self._iou_instances[iou_id]
iou_instance.clean_delete()
del self._iou_instances[request["id"]]
except IOUError as e:
self.send_custom_error(str(e))
return
self.send_response(request)
self.send_response(True)
@IModule.route("iou.update")
def iou_update(self, request):
@@ -326,55 +385,69 @@ class IOU(IModule):
Optional request parameters:
- any setting to update
- startup_config_base64 (startup-config base64 encoded)
- initial_config_base64 (initial-config base64 encoded)
Response parameters:
- same as original request
- updated settings
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, IOU_UPDATE_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
iou_id = request["id"]
if iou_id not in self._iou_instances:
self.send_custom_error("IOU device id {} doesn't exist".format(iou_id))
# get the instance
iou_instance = self.get_iou_instance(request["id"])
if not iou_instance:
return
iou_instance = self._iou_instances[iou_id]
config_path = os.path.join(iou_instance.working_dir, "initial-config.cfg")
try:
# a new startup-config has been pushed
if "startup_config_base64" in request:
config = base64.decodestring(request["startup_config_base64"].encode("utf-8")).decode("utf-8")
if "initial_config_base64" in request:
# a new initial-config has been pushed
config = base64.decodebytes(request["initial_config_base64"].encode("utf-8")).decode("utf-8")
config = "!\n" + config.replace("\r", "")
config = config.replace('%h', iou_instance.name)
config_path = os.path.join(iou_instance.working_dir, "startup-config")
try:
with open(config_path, "w") as f:
log.info("saving startup-config to {}".format(config_path))
log.info("saving initial-config to {}".format(config_path))
f.write(config)
except OSError as e:
raise IOUError("Could not save the configuration {}: {}".format(config_path, e))
request["startup_config"] = os.path.basename(config_path)
if "startup_config" in request:
iou_instance.startup_config = request["startup_config"]
# update the request with the new local initial-config path
request["initial_config"] = os.path.basename(config_path)
elif "initial_config" in request:
if os.path.isfile(request["initial_config"]) and request["initial_config"] != config_path:
# this is a local file set in the GUI
try:
with open(request["initial_config"], "r", errors="replace") as f:
config = f.read()
with open(config_path, "w") as f:
config = "!\n" + config.replace("\r", "")
config = config.replace('%h', iou_instance.name)
f.write(config)
request["initial_config"] = os.path.basename(config_path)
except OSError as e:
raise IOUError("Could not save the configuration from {} to {}: {}".format(request["initial_config"], config_path, e))
elif not os.path.isfile(config_path):
raise IOUError("Startup-config {} could not be found on this server".format(request["initial_config"]))
except IOUError as e:
self.send_custom_error(str(e))
return
# update the IOU settings
response = {}
for name, value in request.items():
if hasattr(iou_instance, name) and getattr(iou_instance, name) != value:
try:
setattr(iou_instance, name, value)
response[name] = value
except IOUError as e:
self.send_custom_error(str(e))
return
self.send_response(request)
self.send_response(response)
@IModule.route("iou.start")
def vm_start(self, request):
@@ -385,32 +458,32 @@ class IOU(IModule):
- id (IOU instance identifier)
Response parameters:
- same as original request
- True on success
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, IOU_START_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
iou_id = request["id"]
if iou_id not in self._iou_instances:
self.send_custom_error("IOU device id {} doesn't exist".format(iou_id))
# get the instance
iou_instance = self.get_iou_instance(request["id"])
if not iou_instance:
return
iou_instance = self._iou_instances[iou_id]
try:
log.debug("starting IOU with command: {}".format(iou_instance.command()))
iou_instance.iouyap = self._iouyap
iou_instance.iourc = self._iourc
if self._iourc:
iou_instance.iourc = self._iourc
else:
# if there is no IOURC file pushed by the client then use the server IOURC file
iou_instance.iourc = self._server_iourc_path
iou_instance.start()
except IOUError as e:
self.send_custom_error(str(e))
return
self.send_response(request)
self.send_response(True)
@IModule.route("iou.stop")
def vm_stop(self, request):
@@ -421,29 +494,26 @@ class IOU(IModule):
- id (IOU instance identifier)
Response parameters:
- same as original request
- True on success
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, IOU_STOP_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
iou_id = request["id"]
if iou_id not in self._iou_instances:
self.send_custom_error("IOU device id {} doesn't exist".format(iou_id))
# get the instance
iou_instance = self.get_iou_instance(request["id"])
if not iou_instance:
return
iou_instance = self._iou_instances[iou_id]
try:
iou_instance.stop()
except IOUError as e:
self.send_custom_error(str(e))
return
self.send_response(request)
self.send_response(True)
@IModule.route("iou.reload")
def vm_reload(self, request):
@@ -454,22 +524,19 @@ class IOU(IModule):
- id (IOU identifier)
Response parameters:
- same as original request
- True on success
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, IOU_RELOAD_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
iou_id = request["id"]
if iou_id not in self._iou_instances:
self.send_custom_error("IOU device id {} doesn't exist".format(iou_id))
# get the instance
iou_instance = self.get_iou_instance(request["id"])
if not iou_instance:
return
iou_instance = self._iou_instances[iou_id]
try:
if iou_instance.is_running():
@@ -478,7 +545,7 @@ class IOU(IModule):
except IOUError as e:
self.send_custom_error(str(e))
return
self.send_response(request)
self.send_response(True)
@IModule.route("iou.allocate_udp_port")
def allocate_udp_port(self, request):
@@ -496,37 +563,32 @@ class IOU(IModule):
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, IOU_ALLOCATE_UDP_PORT_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
iou_id = request["id"]
if iou_id not in self._iou_instances:
self.send_custom_error("IOU device id {} doesn't exist".format(iou_id))
# get the instance
iou_instance = self.get_iou_instance(request["id"])
if not iou_instance:
return
iou_instance = self._iou_instances[iou_id]
try:
# find a UDP port
if self._current_udp_port >= self._udp_end_port_range:
self._current_udp_port = self._udp_start_port_range
port = IOUDevice.find_unused_port(self._current_udp_port, self._udp_end_port_range, host=self._host, socket_type="UDP")
self._current_udp_port += 1
log.info("{} [id={}] has allocated UDP port {} with host {}".format(iou_instance .name,
iou_instance .id,
port,
self._host))
response = {"lport": port}
except IOUError as e:
port = find_unused_port(self._udp_start_port_range,
self._udp_end_port_range,
host=self._host,
socket_type="UDP",
ignore_ports=self._allocated_udp_ports)
except Exception as e:
self.send_custom_error(str(e))
return
response["port_id"] = request["port_id"]
self._allocated_udp_ports.append(port)
log.info("{} [id={}] has allocated UDP port {} with host {}".format(iou_instance.name,
iou_instance.id,
port,
self._host))
response = {"lport": port,
"port_id": request["port_id"]}
self.send_response(response)
@IModule.route("iou.add_nio")
@@ -539,75 +601,58 @@ class IOU(IModule):
- slot (slot number)
- port (port number)
- port_id (unique port identifier)
- nio (nio type, one of the following)
- "NIO_UDP"
- nio (one of the following)
- type "nio_udp"
- lport (local port)
- rhost (remote host)
- rport (remote port)
- "NIO_GenericEthernet"
- type "nio_generic_ethernet"
- ethernet_device (Ethernet device name e.g. eth0)
- "NIO_TAP"
- type "nio_tap"
- tap_device (TAP device name e.g. tap0)
Response parameters:
- same as original request
- port_id (unique port identifier)
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, IOU_ADD_NIO_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
iou_id = request["id"]
if iou_id not in self._iou_instances:
self.send_custom_error("IOU device id {} doesn't exist".format(iou_id))
# get the instance
iou_instance = self.get_iou_instance(request["id"])
if not iou_instance:
return
iou_instance = self._iou_instances[iou_id]
slot = request["slot"]
port = request["port"]
try:
nio = None
if request["nio"] == "NIO_UDP":
lport = request["lport"]
rhost = request["rhost"]
rport = request["rport"]
nio = NIO_UDP(lport, rhost, rport)
elif request["nio"] == "NIO_TAP":
tap_device = request["tap_device"]
# check that we have access to the tap device
TUNSETIFF = 0x400454ca
IFF_TAP = 0x0002
IFF_NO_PI = 0x1000
if request["nio"]["type"] == "nio_udp":
lport = request["nio"]["lport"]
rhost = request["nio"]["rhost"]
rport = request["nio"]["rport"]
try:
tun = os.open("/dev/net/tun", os.O_RDWR)
#TODO: handle IPv6
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.connect((rhost, rport))
except OSError as e:
raise IOUError("Could not open /dev/net/tun: {}".format(e))
ifr = struct.pack("16sH", tap_device.encode("utf-8"), IFF_TAP | IFF_NO_PI)
try:
fcntl.ioctl(tun, TUNSETIFF, ifr)
os.close(tun)
except IOError as e:
raise IOUError("TAP NIO {}: {}".format(tap_device, e))
raise IOUError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e))
nio = NIO_UDP(lport, rhost, rport)
elif request["nio"]["type"] == "nio_tap":
tap_device = request["nio"]["tap_device"]
if not has_privileged_access(self._iouyap):
raise IOUError("{} has no privileged access to {}.".format(self._iouyap, tap_device))
nio = NIO_TAP(tap_device)
elif request["nio"] == "NIO_GenericEthernet":
ethernet_device = request["ethernet_device"]
# check that we have access to the Ethernet device
try:
with socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW):
pass
except socket.error as e:
raise IOUError("Generic Ethernet NIO {}: {}".format(ethernet_device, e))
elif request["nio"]["type"] == "nio_generic_ethernet":
ethernet_device = request["nio"]["ethernet_device"]
if not has_privileged_access(self._iouyap):
raise IOUError("{} has no privileged access to {}.".format(self._iouyap, ethernet_device))
nio = NIO_GenericEthernet(ethernet_device)
if not nio:
raise IOUError("Requested NIO doesn't exist or is not supported: {}".format(request["nio"]))
raise IOUError("Requested NIO does not exist or is not supported: {}".format(request["nio"]["type"]))
except IOUError as e:
self.send_custom_error(str(e))
return
@@ -618,8 +663,7 @@ class IOU(IModule):
self.send_custom_error(str(e))
return
# for now send back the original request
self.send_response(request)
self.send_response({"port_id": request["port_id"]})
@IModule.route("iou.delete_nio")
def delete_nio(self, request):
@@ -632,33 +676,156 @@ class IOU(IModule):
- port (port identifier)
Response parameters:
- same as original request
- True on success
:param request: JSON request
"""
if request == None:
self.send_param_error()
# validate the request
if not self.validate_request(request, IOU_DELETE_NIO_SCHEMA):
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
iou_id = request["id"]
iou_instance = self._iou_instances[iou_id]
if iou_id not in self._iou_instances:
self.send_custom_error("IOU device id {} doesn't exist".format(iou_id))
# get the instance
iou_instance = self.get_iou_instance(request["id"])
if not iou_instance:
return
slot = request["slot"]
port = request["port"]
try:
iou_instance.slot_remove_nio_binding(slot, port)
nio = iou_instance.slot_remove_nio_binding(slot, port)
if isinstance(nio, NIO_UDP) and nio.lport in self._allocated_udp_ports:
self._allocated_udp_ports.remove(nio.lport)
except IOUError as e:
self.send_custom_error(str(e))
return
# for now send back the original request
self.send_response(request)
self.send_response(True)
@IModule.route("iou.start_capture")
def start_capture(self, request):
"""
Starts a packet capture.
Mandatory request parameters:
- id (vm identifier)
- slot (slot number)
- port (port number)
- port_id (port identifier)
- capture_file_name
Optional request parameters:
- data_link_type (PCAP DLT_* value)
Response parameters:
- port_id (port identifier)
- capture_file_path (path to the capture file)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, IOU_START_CAPTURE_SCHEMA):
return
# get the instance
iou_instance = self.get_iou_instance(request["id"])
if not iou_instance:
return
slot = request["slot"]
port = request["port"]
capture_file_name = request["capture_file_name"]
data_link_type = request.get("data_link_type")
try:
capture_file_path = os.path.join(self._working_dir, "captures", capture_file_name)
iou_instance.start_capture(slot, port, capture_file_path, data_link_type)
except IOUError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"],
"capture_file_path": capture_file_path}
self.send_response(response)
@IModule.route("iou.stop_capture")
def stop_capture(self, request):
"""
Stops a packet capture.
Mandatory request parameters:
- id (vm identifier)
- slot (slot number)
- port (port number)
- port_id (port identifier)
Response parameters:
- port_id (port identifier)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, IOU_STOP_CAPTURE_SCHEMA):
return
# get the instance
iou_instance = self.get_iou_instance(request["id"])
if not iou_instance:
return
slot = request["slot"]
port = request["port"]
try:
iou_instance.stop_capture(slot, port)
except IOUError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"]}
self.send_response(response)
@IModule.route("iou.export_config")
def export_config(self, request):
"""
Exports the initial-config from an IOU instance.
Mandatory request parameters:
- id (vm identifier)
Response parameters:
- initial_config_base64 (initial-config base64 encoded)
- False if no configuration can be exported
"""
# validate the request
if not self.validate_request(request, IOU_EXPORT_CONFIG_SCHEMA):
return
# get the instance
iou_instance = self.get_iou_instance(request["id"])
if not iou_instance:
return
if not iou_instance.initial_config:
self.send_custom_error("unable to export the initial-config because it doesn't exist")
return
response = {}
initial_config_path = os.path.join(iou_instance.working_dir, iou_instance.initial_config)
try:
with open(initial_config_path, "rb") as f:
config = f.read()
response["initial_config_base64"] = base64.encodebytes(config).decode("utf-8")
except OSError as e:
self.send_custom_error("unable to export the initial-config: {}".format(e))
return
if not response:
self.send_response(False)
else:
self.send_response(response)
@IModule.route("iou.echo")
def echo(self, request):
@@ -668,7 +835,7 @@ class IOU(IModule):
:param request: JSON request
"""
if request == None:
if request is None:
self.send_param_error()
else:
log.debug("received request {}".format(request))
+372 -117
View File
@@ -21,13 +21,14 @@ order to run an IOU instance.
"""
import os
import socket
import errno
import re
import signal
import subprocess
import argparse
import threading
import configparser
import shutil
from .ioucon import start_ioucon
from .iou_error import IOUError
from .adapters.ethernet_adapter import EthernetAdapter
@@ -35,6 +36,7 @@ from .adapters.serial_adapter import SerialAdapter
from .nios.nio_udp import NIO_UDP
from .nios.nio_tap import NIO_TAP
from .nios.nio_generic_ethernet import NIO_GenericEthernet
from ..attic import find_unused_port
import logging
log = logging.getLogger(__name__)
@@ -44,35 +46,51 @@ class IOUDevice(object):
"""
IOU device implementation.
:param name: name of this IOU device
:param path: path to IOU executable
:param working_dir: path to a working directory
:param host: host/address to bind for console and UDP connections
:param name: name of this IOU device
:param iou_id: IOU instance ID
:param console: TCP console port
:param console_host: IP address to bind for console connections
:param console_start_port_range: TCP console port range start
:param console_end_port_range: TCP console port range end
"""
_instances = []
_allocated_console_ports = []
def __init__(self, path, working_dir, host="127.0.0.1", name=None):
def __init__(self,
name,
path,
working_dir,
iou_id=None,
console=None,
console_host="0.0.0.0",
console_start_port_range=4001,
console_end_port_range=4512):
# find an instance identifier (0 < id <= 512)
self._id = 0
for identifier in range(1, 513):
if identifier not in self._instances:
self._id = identifier
self._instances.append(self._id)
break
if not iou_id:
# find an instance identifier if none is provided (0 < id <= 512)
self._id = 0
for identifier in range(1, 513):
if identifier not in self._instances:
self._id = identifier
self._instances.append(self._id)
break
if self._id == 0:
raise IOUError("Maximum number of IOU instances reached")
if name:
self._name = name
if self._id == 0:
raise IOUError("Maximum number of IOU instances reached")
else:
self._name = "IOU{}".format(self._id)
if iou_id in self._instances:
raise IOUError("IOU identifier {} is already used by another IOU device".format(iou_id))
self._id = iou_id
self._instances.append(self._id)
self._name = name
self._path = path
self._iourc = ""
self._iouyap = ""
self._console = None
self._console = console
self._working_dir = None
self._command = []
self._process = None
@@ -81,19 +99,42 @@ class IOUDevice(object):
self._iouyap_stdout_file = ""
self._ioucon_thead = None
self._ioucon_thread_stop_event = None
self._host = host
self._started = False
self._console_host = console_host
self._console_start_port_range = console_start_port_range
self._console_end_port_range = console_end_port_range
# IOU settings
self._ethernet_adapters = [EthernetAdapter(), EthernetAdapter()] # one adapter = 4 interfaces
self._serial_adapters = [SerialAdapter(), SerialAdapter()] # one adapter = 4 interfaces
self._slots = self._ethernet_adapters + self._serial_adapters
self._use_default_iou_values = True # for RAM & NVRAM values
self._nvram = 128 # Kilobytes
self._startup_config = ""
self._initial_config = ""
self._ram = 256 # Megabytes
self._l1_keepalives = False # used to overcome the always-up Ethernet interfaces (not supported by all IOSes).
# update the working directory
self.working_dir = working_dir
working_dir_path = os.path.join(working_dir, "iou", "device-{}".format(self._id))
if iou_id and not os.path.isdir(working_dir_path):
raise IOUError("Working directory {} doesn't exist".format(working_dir_path))
# create the device own working directory
self.working_dir = working_dir_path
if not self._console:
# allocate a console port
try:
self._console = find_unused_port(self._console_start_port_range,
self._console_end_port_range,
self._console_host,
ignore_ports=self._allocated_console_ports)
except Exception as e:
raise IOUError(e)
if self._console in self._allocated_console_ports:
raise IOUError("Console port {} is already in used another IOU device".format(console))
self._allocated_console_ports.append(self._console)
log.info("IOU device {name} [id={id}] has been created".format(name=self._name,
id=self._id))
@@ -107,12 +148,14 @@ class IOUDevice(object):
iou_defaults = {"name": self._name,
"path": self._path,
"startup_config": self._startup_config,
"intial_config": self._initial_config,
"use_default_iou_values": self._use_default_iou_values,
"ram": self._ram,
"nvram": self._nvram,
"ethernet_adapters": len(self._ethernet_adapters),
"serial_adapters": len(self._serial_adapters),
"console": self._console}
"console": self._console,
"l1_keepalives": self._l1_keepalives}
return iou_defaults
@@ -124,7 +167,7 @@ class IOUDevice(object):
:returns: id (integer)
"""
return(self._id)
return self._id
@classmethod
def reset(cls):
@@ -133,6 +176,7 @@ class IOUDevice(object):
"""
cls._instances.clear()
cls._allocated_console_ports.clear()
@property
def name(self):
@@ -152,10 +196,23 @@ class IOUDevice(object):
:param new_name: name
"""
self._name = new_name
if self._initial_config:
# update the initial-config
config_path = os.path.join(self._working_dir, "initial-config.cfg")
if os.path.isfile(config_path):
try:
with open(config_path, "r+", errors="replace") as f:
old_config = f.read()
new_config = old_config.replace(self._name, new_name)
f.seek(0)
f.write(new_config)
except OSError as e:
raise IOUError("Could not amend the configuration {}: {}".format(config_path, e))
log.info("IOU {name} [id={id}]: renamed to {new_name}".format(name=self._name,
id=self._id,
new_name=new_name))
self._name = new_name
@property
def path(self):
@@ -165,7 +222,7 @@ class IOUDevice(object):
:returns: path to IOU
"""
return(self._path)
return self._path
@path.setter
def path(self, path):
@@ -177,8 +234,8 @@ class IOUDevice(object):
self._path = path
log.info("IOU {name} [id={id}]: path changed to {path}".format(name=self._name,
id=self._id,
path=path))
id=self._id,
path=path))
@property
def iourc(self):
@@ -188,14 +245,14 @@ class IOUDevice(object):
:returns: path to the iourc file
"""
return(self._iourc)
return self._iourc
@iourc.setter
def iourc(self, iourc):
"""
Sets the path to the iourc file.
:param path: path to the iourc file.
:param iourc: path to the iourc file.
"""
self._iourc = iourc
@@ -211,14 +268,14 @@ class IOUDevice(object):
:returns: path to iouyap
"""
return(self._iouyap)
return self._iouyap
@iouyap.setter
def iouyap(self, iouyap):
"""
Sets the path to iouyap.
:param path: path to iouyap
:param iouyap: path to iouyap
"""
self._iouyap = iouyap
@@ -244,8 +301,6 @@ class IOUDevice(object):
:param working_dir: path to the working directory
"""
# create our own working directory
working_dir = os.path.join(working_dir, "iou", "device-{}".format(self._id))
try:
os.makedirs(working_dir)
except FileExistsError:
@@ -255,8 +310,8 @@ class IOUDevice(object):
self._working_dir = working_dir
log.info("IOU {name} [id={id}]: working directory changed to {wd}".format(name=self._name,
id=self._id,
wd=self._working_dir))
id=self._id,
wd=self._working_dir))
@property
def console(self):
@@ -276,10 +331,15 @@ class IOUDevice(object):
:param console: console port (integer)
"""
if console in self._allocated_console_ports:
raise IOUError("Console port {} is already used by another IOU device".format(console))
self._allocated_console_ports.remove(self._console)
self._console = console
self._allocated_console_ports.append(self._console)
log.info("IOU {name} [id={id}]: console port set to {port}".format(name=self._name,
id=self._id,
port=console))
id=self._id,
port=console))
def command(self):
"""
@@ -296,10 +356,38 @@ class IOUDevice(object):
"""
self.stop()
self._instances.remove(self._id)
if self._id in self._instances:
self._instances.remove(self._id)
if self.console and self.console in self._allocated_console_ports:
self._allocated_console_ports.remove(self.console)
log.info("IOU device {name} [id={id}] has been deleted".format(name=self._name,
id=self._id))
def clean_delete(self):
"""
Deletes this IOU device & all files (nvram, initial-config etc.)
"""
self.stop()
if self._id in self._instances:
self._instances.remove(self._id)
if self.console:
self._allocated_console_ports.remove(self.console)
try:
shutil.rmtree(self._working_dir)
except OSError as e:
log.error("could not delete IOU device {name} [id={id}]: {error}".format(name=self._name,
id=self._id,
error=e))
return
log.info("IOU device {name} [id={id}] has been deleted (including associated files)".format(name=self._name,
id=self._id))
@property
def started(self):
"""
@@ -327,6 +415,7 @@ class IOUDevice(object):
for unit in adapter.ports.keys():
nio = adapter.get_nio(unit)
if nio:
connection = None
if isinstance(nio, NIO_UDP):
# UDP tunnel
connection = {"tunnel_udp": "{lport}:{rhost}:{rport}".format(lport=nio.lport,
@@ -340,7 +429,25 @@ class IOUDevice(object):
# Ethernet interface
connection = {"eth_dev": "{ethernet_device}".format(ethernet_device=nio.ethernet_device)}
config["{iouyap_id}:{bay}/{unit}".format(iouyap_id=str(self._id + 512), bay=bay_id, unit=unit_id)] = connection
if connection:
interface = "{iouyap_id}:{bay}/{unit}".format(iouyap_id=str(self._id + 512), bay=bay_id, unit=unit_id)
config[interface] = connection
if nio.capturing:
pcap_data_link_type = nio.pcap_data_link_type.upper()
if pcap_data_link_type == "DLT_PPP_SERIAL":
pcap_protocol = "ppp"
elif pcap_data_link_type == "DLT_C_HDLC":
pcap_protocol = "hdlc"
elif pcap_data_link_type == "DLT_FRELAY":
pcap_protocol = "fr"
else:
pcap_protocol = "ethernet"
capture_info = {"pcap_file": "{pcap_file}".format(pcap_file=nio.pcap_output_file),
"pcap_protocol": pcap_protocol,
"pcap_overwrite": "y"}
config[interface].update(capture_info)
unit_id += 1
bay_id += 1
@@ -377,7 +484,7 @@ class IOUDevice(object):
"""
if not self._ioucon_thead:
telnet_server = "{}:{}".format(self._host, self._console)
telnet_server = "{}:{}".format(self._console_host, self.console)
log.info("starting ioucon for IOU instance {} to accept Telnet connections on {}".format(self._name, telnet_server))
args = argparse.Namespace(appl_id=str(self._id), debug=False, escape='^^', telnet_limit=0, telnet_server=telnet_server)
self._ioucon_thread_stop_event = threading.Event()
@@ -391,7 +498,7 @@ class IOUDevice(object):
try:
self._update_iouyap_config()
command = [self._iouyap, str(self._id + 512)] # iouyap has always IOU ID + 512
command = [self._iouyap, "-q", str(self._id + 512)] # iouyap has always IOU ID + 512
log.info("starting iouyap: {}".format(command))
self._iouyap_stdout_file = os.path.join(self._working_dir, "iouyap.log")
log.info("logging to {}".format(self._iouyap_stdout_file))
@@ -402,11 +509,28 @@ class IOUDevice(object):
cwd=self._working_dir)
log.info("iouyap started PID={}".format(self._iouyap_process.pid))
except OSError as e:
except (OSError, subprocess.SubprocessError) as e:
iouyap_stdout = self.read_iouyap_stdout()
log.error("could not start iouyap: {}\n{}".format(e, iouyap_stdout))
raise IOUError("Could not start iouyap: {}\n{}".format(e, iouyap_stdout))
def _library_check(self):
"""
Checks for missing shared library dependencies in the IOU image.
"""
try:
output = subprocess.check_output(["ldd", self._path])
except (FileNotFoundError, subprocess.SubprocessError) as e:
log.warn("could not determine the shared library dependencies for {}: {}".format(self._path, e))
return
p = re.compile("([\.\w]+)\s=>\s+not found")
missing_libs = p.findall(output.decode("utf-8"))
if missing_libs:
raise IOUError("The following shared library dependencies cannot be found for IOU image {}: {}".format(self._path,
", ".join(missing_libs)))
def start(self):
"""
Starts the IOU process.
@@ -414,11 +538,31 @@ class IOUDevice(object):
if not self.is_running():
if not os.path.isfile(self._path):
raise IOUError("IOU '{}' is not accessible".format(self._path))
if not os.path.isfile(self._path) or not os.path.exists(self._path):
if os.path.islink(self._path):
raise IOUError("IOU image '{}' linked to '{}' is not accessible".format(self._path, os.path.realpath(self._path)))
else:
raise IOUError("IOU image '{}' is not accessible".format(self._path))
try:
with open(self._path, "rb") as f:
# read the first 7 bytes of the file.
elf_header_start = f.read(7)
except OSError as e:
raise IOUError("Cannot read ELF header for IOU image '{}': {}".format(self._path, e))
# IOU images must start with the ELF magic number, be 32-bit, little endian
# and have an ELF version of 1 normal IOS image are big endian!
if elf_header_start != b'\x7fELF\x01\x01\x01':
raise IOUError("'{}' is not a valid IOU image".format(self._path))
if not os.access(self._path, os.X_OK):
raise IOUError("IOU image '{}' is not executable".format(self._path))
self._library_check()
if not self._iourc or not os.path.isfile(self._iourc):
raise IOUError("A iourc file is necessary to start IOU")
raise IOUError("A valid iourc file is necessary to start IOU")
if not self._iouyap or not os.path.isfile(self._iouyap):
raise IOUError("iouyap is necessary to start IOU")
@@ -440,10 +584,12 @@ class IOUDevice(object):
env=env)
log.info("IOU instance {} started PID={}".format(self._id, self._process.pid))
self._started = True
except OSError as e:
except FileNotFoundError as e:
raise IOUError("could not start IOU: {}: 32-bit binary support is probably not installed".format(e))
except (OSError, subprocess.SubprocessError) as e:
iou_stdout = self.read_iou_stdout()
log.error("could not start IOU: {}\n{}".format(e, iou_stdout))
raise IOUError("could not start IOU: {}\n{}".format(e, iou_stdout))
log.error("could not start IOU {}: {}\n{}".format(self._path, e, iou_stdout))
raise IOUError("could not start IOU {}: {}\n{}".format(self._path, e, iou_stdout))
# start console support
self._start_ioucon()
@@ -455,25 +601,11 @@ class IOUDevice(object):
Stops the IOU process.
"""
# stop the IOU process
if self.is_running():
log.info("stopping IOU instance {} PID={}".format(self._id, self._process.pid))
try:
self._process.terminate()
self._process.wait(1)
except subprocess.TimeoutExpired:
self._process.kill()
if self._process.poll() == None:
log.warn("IOU instance {} PID={} is still running".format(self._id,
self._process.pid))
self._process = None
self._started = False
# stop console support
if self._ioucon_thead:
self._ioucon_thread_stop_event.set()
if self._ioucon_thead.is_alive():
self._ioucon_thead.join(timeout=0.10)
self._ioucon_thead.join(timeout=3.0) # wait for the thread to free the console port
self._ioucon_thead = None
# stop iouyap
@@ -484,11 +616,25 @@ class IOUDevice(object):
self._iouyap_process.wait(1)
except subprocess.TimeoutExpired:
self._iouyap_process.kill()
if self._iouyap_process.poll() == None:
if self._iouyap_process.poll() is None:
log.warn("iouyap PID={} for IOU instance {} is still running".format(self._iouyap_process.pid,
self._id))
self._iouyap_process = None
# stop the IOU process
if self.is_running():
log.info("stopping IOU instance {} PID={}".format(self._id, self._process.pid))
try:
self._process.terminate()
self._process.wait(1)
except subprocess.TimeoutExpired:
self._process.kill()
if self._process.poll() is None:
log.warn("IOU instance {} PID={} is still running".format(self._id,
self._process.pid))
self._process = None
self._started = False
def read_iou_stdout(self):
"""
Reads the standard output of the IOU process.
@@ -498,7 +644,7 @@ class IOUDevice(object):
output = ""
if self._iou_stdout_file:
try:
with open(self._iou_stdout_file) as file:
with open(self._iou_stdout_file, errors="replace") as file:
output = file.read()
except OSError as e:
log.warn("could not read {}: {}".format(self._iou_stdout_file, e))
@@ -513,7 +659,7 @@ class IOUDevice(object):
output = ""
if self._iouyap_stdout_file:
try:
with open(self._iouyap_stdout_file) as file:
with open(self._iouyap_stdout_file, errors="replace") as file:
output = file.read()
except OSError as e:
log.warn("could not read {}: {}".format(self._iouyap_stdout_file, e))
@@ -526,7 +672,7 @@ class IOUDevice(object):
:returns: True or False
"""
if self._process and self._process.poll() == None:
if self._process and self._process.poll() is None:
return True
return False
@@ -537,7 +683,7 @@ class IOUDevice(object):
:returns: True or False
"""
if self._iouyap_process and self._iouyap_process.poll() == None:
if self._iouyap_process and self._iouyap_process.poll() is None:
return True
return False
@@ -576,6 +722,8 @@ class IOUDevice(object):
:param slot_id: slot ID
:param port_id: port ID
:returns: NIO instance
"""
try:
@@ -599,6 +747,26 @@ class IOUDevice(object):
self._update_iouyap_config()
os.kill(self._iouyap_process.pid, signal.SIGHUP)
return nio
def _enable_l1_keepalives(self, command):
"""
Enables L1 keepalive messages if supported.
:param command: command line
"""
env = os.environ.copy()
env["IOURC"] = self._iourc
try:
output = subprocess.check_output([self._path, "-h"], stderr=subprocess.STDOUT, cwd=self._working_dir, env=env)
if re.search("-l\s+Enable Layer 1 keepalive messages", output.decode("utf-8")):
command.extend(["-l"])
else:
raise IOUError("layer 1 keepalive messages are not supported by {}".format(os.path.basename(self._path)))
except (OSError, subprocess.SubprocessError) as e:
log.warn("could not determine if layer 1 keepalive messages are supported by {}: {}".format(os.path.basename(self._path), e))
def _build_command(self):
"""
Command to start the IOU process.
@@ -629,20 +797,70 @@ class IOUDevice(object):
-N Ignore the NETMAP file
"""
#TODO: add support for keepalive and watchdog
command = [self._path]
if len(self._ethernet_adapters) != 2:
command.extend(["-e", str(len(self._ethernet_adapters))])
if len(self._serial_adapters) != 2:
command.extend(["-s", str(len(self._serial_adapters))])
command.extend(["-n", str(self._nvram)])
command.extend(["-m", str(self._ram)])
if not self.use_default_iou_values:
command.extend(["-n", str(self._nvram)])
command.extend(["-m", str(self._ram)])
command.extend(["-L"]) # disable local console, use remote console
if self._startup_config:
command.extend(["-c", self._startup_config])
if self._initial_config:
command.extend(["-c", self._initial_config])
if self._l1_keepalives:
self._enable_l1_keepalives(command)
command.extend([str(self._id)])
return command
@property
def use_default_iou_values(self):
"""
Returns if this device uses the default IOU image values.
:returns: boolean
"""
return self._use_default_iou_values
@use_default_iou_values.setter
def use_default_iou_values(self, state):
"""
Sets if this device uses the default IOU image values.
:param state: boolean
"""
self._use_default_iou_values = state
if state:
log.info("IOU {name} [id={id}]: uses the default IOU image values".format(name=self._name, id=self._id))
else:
log.info("IOU {name} [id={id}]: does not use the default IOU image values".format(name=self._name, id=self._id))
@property
def l1_keepalives(self):
"""
Returns either layer 1 keepalive messages option is enabled or disabled.
:returns: boolean
"""
return self._l1_keepalives
@l1_keepalives.setter
def l1_keepalives(self, state):
"""
Enables or disables layer 1 keepalive messages.
:param state: boolean
"""
self._l1_keepalives = state
if state:
log.info("IOU {name} [id={id}]: has activated layer 1 keepalive messages".format(name=self._name, id=self._id))
else:
log.info("IOU {name} [id={id}]: has deactivated layer 1 keepalive messages".format(name=self._name, id=self._id))
@property
def ram(self):
"""
@@ -699,27 +917,27 @@ class IOUDevice(object):
self._nvram = nvram
@property
def startup_config(self):
def initial_config(self):
"""
Returns the startup-config for this IOU instance.
Returns the initial-config for this IOU instance.
:returns: path to startup-config file
:returns: path to initial-config file
"""
return self._startup_config
return self._initial_config
@startup_config.setter
def startup_config(self, startup_config):
@initial_config.setter
def initial_config(self, initial_config):
"""
Sets the startup-config for this IOU instance.
Sets the initial-config for this IOU instance.
:param startup_config: path to startup-config file
:param initial_config: path to initial-config file
"""
self._startup_config = startup_config
log.info("IOU {name} [id={id}]: startup_config set to {config}".format(name=self._name,
id=self._id,
config=self._startup_config))
self._initial_config = initial_config
log.info("IOU {name} [id={id}]: initial_config set to {config}".format(name=self._name,
id=self._id,
config=self._initial_config))
@property
def ethernet_adapters(self):
@@ -777,37 +995,74 @@ class IOUDevice(object):
self._slots = self._ethernet_adapters + self._serial_adapters
@staticmethod
def find_unused_port(start_port, end_port, host='127.0.0.1', socket_type="TCP"):
def start_capture(self, slot_id, port_id, output_file, data_link_type="DLT_EN10MB"):
"""
Finds an unused port in the specified range.
Starts a packet capture.
:param start_port: first port in the range
:param end_port: last port in the range
:param host: host/address for bind()
:param socket_type: TCP (default) or UDP
:param slot_id: slot ID
:param port_id: port ID
:param port: allocated port
:param output_file: PCAP destination file for the capture
:param data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB
"""
if socket_type == "UDP":
socket_type = socket.SOCK_DGRAM
else:
socket_type = socket.SOCK_STREAM
try:
adapter = self._slots[slot_id]
except IndexError:
raise IOUError("Slot {slot_id} doesn't exist on IOU {name}".format(name=self._name,
slot_id=slot_id))
for port in range(start_port, end_port):
try:
if ":" in host:
# IPv6 address support
with socket.socket(socket.AF_INET6, socket_type) as s:
s.bind((host, port)) # the port is available if bind is a success
else:
with socket.socket(socket.AF_INET, socket_type) as s:
s.bind((host, port)) # the port is available if bind is a success
return port
except OSError as e:
if e.errno == errno.EADDRINUSE: # socket already in use
if port + 1 == end_port:
raise IOUError("Could not find a free port between {0} and {1}".format(start_port, end_port))
else:
continue
else:
raise IOUError("Could not find an unused port: {}".format(e))
if not adapter.port_exists(port_id):
raise IOUError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter,
port_id=port_id))
nio = adapter.get_nio(port_id)
if nio.capturing:
raise IOUError("Packet capture is already activated on {slot_id}/{port_id}".format(slot_id=slot_id,
port_id=port_id))
try:
os.makedirs(os.path.dirname(output_file))
except FileExistsError:
pass
except OSError as e:
raise IOUError("Could not create captures directory {}".format(e))
nio.startPacketCapture(output_file, data_link_type)
log.info("IOU {name} [id={id}]: starting packet capture on {slot_id}/{port_id}".format(name=self._name,
id=self._id,
slot_id=slot_id,
port_id=port_id))
if self.is_iouyap_running():
self._update_iouyap_config()
os.kill(self._iouyap_process.pid, signal.SIGHUP)
def stop_capture(self, slot_id, port_id):
"""
Stops a packet capture.
:param slot_id: slot ID
:param port_id: port ID
"""
try:
adapter = self._slots[slot_id]
except IndexError:
raise IOUError("Slot {slot_id} doesn't exist on IOU {name}".format(name=self._name,
slot_id=slot_id))
if not adapter.port_exists(port_id):
raise IOUError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter,
port_id=port_id))
nio = adapter.get_nio(port_id)
nio.stopPacketCapture()
log.info("IOU {name} [id={id}]: stopping packet capture on {slot_id}/{port_id}".format(name=self._name,
id=self._id,
slot_id=slot_id,
port_id=port_id))
if self.is_iouyap_running():
self._update_iouyap_config()
os.kill(self._iouyap_process.pid, signal.SIGHUP)
+2
View File
@@ -25,6 +25,8 @@ class IOUError(Exception):
def __init__(self, message, original_exception=None):
Exception.__init__(self, message)
if isinstance(message, Exception):
message = str(message)
self._message = message
self._original_exception = original_exception
+47 -43
View File
@@ -56,32 +56,32 @@ EXIT_ABORT = 2
# Mostly from:
# https://code.google.com/p/miniboa/source/browse/trunk/miniboa/telnet.py
#--[ Telnet Commands ]---------------------------------------------------------
SE = 240 # End of subnegotiation parameters
NOP = 241 # No operation
DATMK = 242 # Data stream portion of a sync.
BREAK = 243 # NVT Character BRK
IP = 244 # Interrupt Process
AO = 245 # Abort Output
AYT = 246 # Are you there
EC = 247 # Erase Character
EL = 248 # Erase Line
GA = 249 # The Go Ahead Signal
SB = 250 # Sub-option to follow
WILL = 251 # Will; request or confirm option begin
WONT = 252 # Wont; deny option request
DO = 253 # Do = Request or confirm remote option
DONT = 254 # Don't = Demand or confirm option halt
IAC = 255 # Interpret as Command
SEND = 1 # Sub-process negotiation SEND command
IS = 0 # Sub-process negotiation IS command
SE = 240 # End of sub-negotiation parameters
NOP = 241 # No operation
DATMK = 242 # Data stream portion of a sync.
BREAK = 243 # NVT Character BRK
IP = 244 # Interrupt Process
AO = 245 # Abort Output
AYT = 246 # Are you there
EC = 247 # Erase Character
EL = 248 # Erase Line
GA = 249 # The Go Ahead Signal
SB = 250 # Sub-option to follow
WILL = 251 # Will; request or confirm option begin
WONT = 252 # Wont; deny option request
DO = 253 # Do = Request or confirm remote option
DONT = 254 # Don't = Demand or confirm option halt
IAC = 255 # Interpret as Command
SEND = 1 # Sub-process negotiation SEND command
IS = 0 # Sub-process negotiation IS command
#--[ Telnet Options ]----------------------------------------------------------
BINARY = 0 # Transmit Binary
ECHO = 1 # Echo characters back to sender
RECON = 2 # Reconnection
SGA = 3 # Suppress Go-Ahead
TMARK = 6 # Timing Mark
TTYPE = 24 # Terminal Type
NAWS = 31 # Negotiate About Window Size
ECHO = 1 # Echo characters back to sender
RECON = 2 # Reconnection
SGA = 3 # Suppress Go-Ahead
TMARK = 6 # Timing Mark
TTYPE = 24 # Terminal Type
NAWS = 31 # Negotiate About Window Size
LINEMO = 34 # Line Mode
@@ -139,7 +139,10 @@ class FileLock:
def unlock(self):
if self.fd:
# Deleting first prevents a race condition
os.unlink(self.fd.name)
try:
os.unlink(self.fd.name)
except FileNotFoundError as e:
log.debug("{}".format(e))
self.fd.close()
def __enter__(self):
@@ -221,6 +224,8 @@ class TelnetServer(Console):
buf = self._read_cur(bufsize, socket.MSG_DONTWAIT)
except BlockingIOError:
return None
except ConnectionResetError:
buf = b''
if not buf:
self._disconnect(fileno)
@@ -296,9 +301,7 @@ class TelnetServer(Console):
buf.extend(self._read_block(1))
iac_cmd.append(buf[iac_loc + 2])
# We do ECHO, SGA, and BINARY. Period.
if (iac_cmd[1] == DO
and iac_cmd[2] not in [ECHO, SGA, BINARY]):
if iac_cmd[1] == DO and iac_cmd[2] not in [ECHO, SGA, BINARY]:
self._write_cur(bytes([IAC, WONT, iac_cmd[2]]))
log.debug("Telnet WON'T {:#x}".format(iac_cmd[2]))
else:
@@ -323,7 +326,7 @@ class TelnetServer(Console):
fd.send(bytes([IAC, WILL, ECHO,
IAC, WILL, SGA,
IAC, WILL, BINARY,
IAC, DO, BINARY]))
IAC, DO, BINARY]))
if args.telnet_limit and len(self.fd_dict) > args.telnet_limit:
fd.send(b'\r\nToo many connections\r\n')
@@ -334,7 +337,10 @@ class TelnetServer(Console):
def _disconnect(self, fileno):
fd = self.fd_dict.pop(fileno)
log.info("Telnet client disconnected")
fd.shutdown(socket.SHUT_RDWR)
try:
fd.shutdown(socket.SHUT_RDWR)
except OSError as e:
log.warn("shutdown: {}".format(e))
fd.close()
def __enter__(self):
@@ -375,7 +381,10 @@ class IOU(Router):
return buf
def write(self, buf):
self.fd.send(buf)
try:
self.fd.send(buf)
except BlockingIOError:
return
def _open(self):
self.fd = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
@@ -387,14 +396,12 @@ class IOU(Router):
except FileNotFoundError:
pass
except Exception as e:
raise NetioError("Couldn't unlink socket {}: {}"
.format(self.ttyC, e))
raise NetioError("Couldn't unlink socket {}: {}".format(self.ttyC, e))
try:
self.fd.bind(self.ttyC)
except Exception as e:
raise NetioError("Couldn't create socket {}: {}"
.format(self.ttyC, e))
raise NetioError("Couldn't create socket {}: {}".format(self.ttyC, e))
def _connect(self):
# Keep trying until we connect or die trying
@@ -402,12 +409,10 @@ class IOU(Router):
try:
self.fd.connect(self.ttyS)
except FileNotFoundError:
log.debug("Waiting to connect to {}".format(self.ttyS),
file=sys.stderr)
log.debug("Waiting to connect to {}".format(self.ttyS))
time.sleep(RETRY_DELAY)
except Exception as e:
raise NetioError("Couldn't connect to socket {}: {}"
.format(self.ttyS, e))
raise NetioError("Couldn't connect to socket {}: {}".format(self.ttyS, e))
else:
break
@@ -460,8 +465,7 @@ def mkdir_netio(netio_dir):
except FileExistsError:
pass
except Exception as e:
raise NetioError("Couldn't create directory {}: {}"
.format(netio_dir, e))
raise NetioError("Couldn't create directory {}: {}".format(netio_dir, e))
def send_recv_loop(console, router, esc_char, stop_event):
@@ -600,7 +604,7 @@ def start_ioucon(cmdline_args, stop_event):
nport = int(port)
except ValueError:
pass
if (addr == '' or nport == 0):
if addr == '' or nport == 0:
raise ConfigError('format for --telnet-server must be '
'ADDR:PORT (like 127.0.0.1:20000)')
@@ -626,7 +630,7 @@ def start_ioucon(cmdline_args, stop_event):
if args.debug:
traceback.print_exc(file=sys.stderr)
else:
print(e, file=sys.stderr)
log.error("ioucon: {}".format(e))
sys.exit(EXIT_FAILURE)
log.info("exiting...")
+79
View File
@@ -0,0 +1,79 @@
# -*- 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/>.
"""
Base interface for NIOs.
"""
class NIO(object):
"""
Network Input/Output.
"""
def __init__(self):
self._capturing = False
self._pcap_output_file = ""
self._pcap_data_link_type = ""
def startPacketCapture(self, pcap_output_file, pcap_data_link_type="DLT_EN10MB"):
"""
:param pcap_output_file: PCAP destination file for the capture
:param pcap_data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB
"""
self._capturing = True
self._pcap_output_file = pcap_output_file
self._pcap_data_link_type = pcap_data_link_type
def stopPacketCapture(self):
self._capturing = False
self._pcap_output_file = ""
self._pcap_data_link_type = ""
@property
def capturing(self):
"""
Returns either a capture is configured on this NIO.
:returns: boolean
"""
return self._capturing
@property
def pcap_output_file(self):
"""
Returns the path to the PCAP output file.
:returns: path to the PCAP output file
"""
return self._pcap_output_file
@property
def pcap_data_link_type(self):
"""
Returns the PCAP data link type
:returns: PCAP data link type (DLT_* value)
"""
return self._pcap_data_link_type
@@ -19,16 +19,19 @@
Interface for generic Ethernet NIOs (PCAP library).
"""
from .nio import NIO
class NIO_GenericEthernet(object):
class NIO_GenericEthernet(NIO):
"""
NIO generic Ethernet NIO.
Generic Ethernet NIO.
:param ethernet_device: Ethernet device name (e.g. eth0)
"""
def __init__(self, ethernet_device):
NIO.__init__(self)
self._ethernet_device = ethernet_device
@property
+5 -2
View File
@@ -19,16 +19,19 @@
Interface for TAP NIOs (UNIX based OSes only).
"""
from .nio import NIO
class NIO_TAP(object):
class NIO_TAP(NIO):
"""
IOU TAP NIO.
TAP NIO.
:param tap_device: TAP device name (e.g. tap0)
"""
def __init__(self, tap_device):
NIO.__init__(self)
self._tap_device = tap_device
@property
+5 -2
View File
@@ -19,10 +19,12 @@
Interface for UDP NIOs.
"""
from .nio import NIO
class NIO_UDP(object):
class NIO_UDP(NIO):
"""
IOU UDP NIO.
UDP NIO.
:param lport: local port number
:param rhost: remote address/host
@@ -33,6 +35,7 @@ class NIO_UDP(object):
def __init__(self, lport, rhost, rport):
NIO.__init__(self)
self._lport = lport
self._rhost = rhost
self._rport = rport
+472
View File
@@ -0,0 +1,472 @@
# -*- 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/>.
IOU_CREATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to create a new IOU instance",
"type": "object",
"properties": {
"name": {
"description": "IOU device name",
"type": "string",
"minLength": 1,
},
"iou_id": {
"description": "IOU device instance ID",
"type": "integer"
},
"console": {
"description": "console TCP port",
"minimum": 1,
"maximum": 65535,
"type": "integer"
},
"path": {
"description": "path to the IOU executable",
"type": "string",
"minLength": 1,
},
"cloud_path": {
"description": "Path to the image in the cloud object store",
"type": "string",
}
},
"additionalProperties": False,
"required": ["name", "path"],
}
IOU_DELETE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to delete an IOU instance",
"type": "object",
"properties": {
"id": {
"description": "IOU device instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
IOU_UPDATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to update an IOU instance",
"type": "object",
"properties": {
"id": {
"description": "IOU device instance ID",
"type": "integer"
},
"name": {
"description": "IOU device name",
"type": "string",
"minLength": 1,
},
"path": {
"description": "path to the IOU executable",
"type": "string",
"minLength": 1,
},
"initial_config": {
"description": "path to the IOU initial configuration file",
"type": "string",
"minLength": 1,
},
"ram": {
"description": "amount of RAM in MB",
"type": "integer"
},
"nvram": {
"description": "amount of NVRAM in KB",
"type": "integer"
},
"ethernet_adapters": {
"description": "number of Ethernet adapters",
"type": "integer",
"minimum": 0,
"maximum": 16,
},
"serial_adapters": {
"description": "number of serial adapters",
"type": "integer",
"minimum": 0,
"maximum": 16,
},
"console": {
"description": "console TCP port",
"minimum": 1,
"maximum": 65535,
"type": "integer"
},
"use_default_iou_values": {
"description": "use the default IOU RAM & NVRAM values",
"type": "boolean"
},
"l1_keepalives": {
"description": "enable or disable layer 1 keepalive messages",
"type": "boolean"
},
"initial_config_base64": {
"description": "initial configuration base64 encoded",
"type": "string"
},
},
"additionalProperties": False,
"required": ["id"]
}
IOU_START_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to start an IOU instance",
"type": "object",
"properties": {
"id": {
"description": "IOU device instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
IOU_STOP_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to stop an IOU instance",
"type": "object",
"properties": {
"id": {
"description": "IOU device instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
IOU_RELOAD_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to reload an IOU instance",
"type": "object",
"properties": {
"id": {
"description": "IOU device instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
IOU_ALLOCATE_UDP_PORT_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to allocate an UDP port for an IOU instance",
"type": "object",
"properties": {
"id": {
"description": "IOU device instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the IOU instance",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id", "port_id"]
}
IOU_ADD_NIO_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to add a NIO for an IOU instance",
"type": "object",
"definitions": {
"UDP": {
"description": "UDP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_udp"]
},
"lport": {
"description": "Local port",
"type": "integer",
"minimum": 1,
"maximum": 65535
},
"rhost": {
"description": "Remote host",
"type": "string",
"minLength": 1
},
"rport": {
"description": "Remote port",
"type": "integer",
"minimum": 1,
"maximum": 65535
}
},
"required": ["type", "lport", "rhost", "rport"],
"additionalProperties": False
},
"Ethernet": {
"description": "Generic Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_generic_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"LinuxEthernet": {
"description": "Linux Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_linux_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"TAP": {
"description": "TAP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_tap"]
},
"tap_device": {
"description": "TAP device name e.g. tap0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "tap_device"],
"additionalProperties": False
},
"UNIX": {
"description": "UNIX Network Input/Output",
"properties": {
"type": {
"enum": ["nio_unix"]
},
"local_file": {
"description": "path to the UNIX socket file (local)",
"type": "string",
"minLength": 1
},
"remote_file": {
"description": "path to the UNIX socket file (remote)",
"type": "string",
"minLength": 1
},
},
"required": ["type", "local_file", "remote_file"],
"additionalProperties": False
},
"VDE": {
"description": "VDE Network Input/Output",
"properties": {
"type": {
"enum": ["nio_vde"]
},
"control_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
"local_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
},
"required": ["type", "control_file", "local_file"],
"additionalProperties": False
},
"NULL": {
"description": "NULL Network Input/Output",
"properties": {
"type": {
"enum": ["nio_null"]
},
},
"required": ["type"],
"additionalProperties": False
},
},
"properties": {
"id": {
"description": "IOU device instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the IOU instance",
"type": "integer"
},
"slot": {
"description": "Slot number",
"type": "integer",
"minimum": 0,
"maximum": 15
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 0,
"maximum": 3
},
"nio": {
"type": "object",
"description": "Network Input/Output",
"oneOf": [
{"$ref": "#/definitions/UDP"},
{"$ref": "#/definitions/Ethernet"},
{"$ref": "#/definitions/LinuxEthernet"},
{"$ref": "#/definitions/TAP"},
{"$ref": "#/definitions/UNIX"},
{"$ref": "#/definitions/VDE"},
{"$ref": "#/definitions/NULL"},
]
},
},
"additionalProperties": False,
"required": ["id", "port_id", "slot", "port", "nio"]
}
IOU_DELETE_NIO_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to delete a NIO for an IOU instance",
"type": "object",
"properties": {
"id": {
"description": "IOU device instance ID",
"type": "integer"
},
"slot": {
"description": "Slot number",
"type": "integer",
"minimum": 0,
"maximum": 15
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 0,
"maximum": 3
},
},
"additionalProperties": False,
"required": ["id", "slot", "port"]
}
IOU_START_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to start a packet capture on an IOU instance port",
"type": "object",
"properties": {
"id": {
"description": "IOU device instance ID",
"type": "integer"
},
"slot": {
"description": "Slot number",
"type": "integer",
"minimum": 0,
"maximum": 15
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 0,
"maximum": 3
},
"port_id": {
"description": "Unique port identifier for the IOU instance",
"type": "integer"
},
"capture_file_name": {
"description": "Capture file name",
"type": "string",
"minLength": 1,
},
"data_link_type": {
"description": "PCAP data link type",
"type": "string",
"minLength": 1,
},
},
"additionalProperties": False,
"required": ["id", "slot", "port", "port_id", "capture_file_name"]
}
IOU_STOP_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to stop a packet capture on an IOU instance port",
"type": "object",
"properties": {
"id": {
"description": "IOU device instance ID",
"type": "integer"
},
"slot": {
"description": "Slot number",
"type": "integer",
"minimum": 0,
"maximum": 15
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 0,
"maximum": 3
},
"port_id": {
"description": "Unique port identifier for the IOU instance",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id", "slot", "port", "port_id"]
}
IOU_EXPORT_CONFIG_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to export an initial-config from an IOU instance",
"type": "object",
"properties": {
"id": {
"description": "IOU device instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
+686
View File
@@ -0,0 +1,686 @@
# -*- 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/>.
"""
QEMU server module.
"""
import sys
import os
import socket
import shutil
import subprocess
import re
from gns3server.modules import IModule
from gns3server.config import Config
from .qemu_vm import QemuVM
from .qemu_error import QemuError
from .nios.nio_udp import NIO_UDP
from ..attic import find_unused_port
from .schemas import QEMU_CREATE_SCHEMA
from .schemas import QEMU_DELETE_SCHEMA
from .schemas import QEMU_UPDATE_SCHEMA
from .schemas import QEMU_START_SCHEMA
from .schemas import QEMU_STOP_SCHEMA
from .schemas import QEMU_SUSPEND_SCHEMA
from .schemas import QEMU_RELOAD_SCHEMA
from .schemas import QEMU_ALLOCATE_UDP_PORT_SCHEMA
from .schemas import QEMU_ADD_NIO_SCHEMA
from .schemas import QEMU_DELETE_NIO_SCHEMA
import logging
log = logging.getLogger(__name__)
class Qemu(IModule):
"""
QEMU module.
:param name: module name
:param args: arguments for the module
:param kwargs: named arguments for the module
"""
def __init__(self, name, *args, **kwargs):
# a new process start when calling IModule
IModule.__init__(self, name, *args, **kwargs)
self._qemu_instances = {}
config = Config.instance()
qemu_config = config.get_section_config(name.upper())
self._console_start_port_range = qemu_config.get("console_start_port_range", 5001)
self._console_end_port_range = qemu_config.get("console_end_port_range", 5500)
self._monitor_start_port_range = qemu_config.get("monitor_start_port_range", 5501)
self._monitor_end_port_range = qemu_config.get("monitor_end_port_range", 6000)
self._allocated_udp_ports = []
self._udp_start_port_range = qemu_config.get("udp_start_port_range", 40001)
self._udp_end_port_range = qemu_config.get("udp_end_port_range", 45500)
self._host = qemu_config.get("host", kwargs["host"])
self._console_host = qemu_config.get("console_host", kwargs["console_host"])
self._monitor_host = qemu_config.get("monitor_host", "127.0.0.1")
self._projects_dir = kwargs["projects_dir"]
self._tempdir = kwargs["temp_dir"]
self._working_dir = self._projects_dir
def stop(self, signum=None):
"""
Properly stops the module.
:param signum: signal number (if called by the signal handler)
"""
# delete all QEMU instances
for qemu_id in self._qemu_instances:
qemu_instance = self._qemu_instances[qemu_id]
qemu_instance.delete()
IModule.stop(self, signum) # this will stop the I/O loop
def get_qemu_instance(self, qemu_id):
"""
Returns a QEMU VM instance.
:param qemu_id: QEMU VM identifier
:returns: QemuVM instance
"""
if qemu_id not in self._qemu_instances:
log.debug("QEMU VM ID {} doesn't exist".format(qemu_id), exc_info=1)
self.send_custom_error("QEMU VM ID {} doesn't exist".format(qemu_id))
return None
return self._qemu_instances[qemu_id]
@IModule.route("qemu.reset")
def reset(self, request):
"""
Resets the module.
:param request: JSON request
"""
# delete all QEMU instances
for qemu_id in self._qemu_instances:
qemu_instance = self._qemu_instances[qemu_id]
qemu_instance.delete()
# resets the instance IDs
QemuVM.reset()
self._qemu_instances.clear()
self._allocated_udp_ports.clear()
self._working_dir = self._projects_dir
log.info("QEMU module has been reset")
@IModule.route("qemu.settings")
def settings(self, request):
"""
Set or update settings.
Optional request parameters:
- working_dir (path to a working directory)
- project_name
- console_start_port_range
- console_end_port_range
- monitor_start_port_range
- monitor_end_port_range
- udp_start_port_range
- udp_end_port_range
:param request: JSON request
"""
if request is None:
self.send_param_error()
return
if "working_dir" in request:
new_working_dir = request["working_dir"]
log.info("this server is local with working directory path to {}".format(new_working_dir))
else:
new_working_dir = os.path.join(self._projects_dir, request["project_name"])
log.info("this server is remote with working directory path to {}".format(new_working_dir))
if self._projects_dir != self._working_dir != new_working_dir:
if not os.path.isdir(new_working_dir):
try:
shutil.move(self._working_dir, new_working_dir)
except OSError as e:
log.error("could not move working directory from {} to {}: {}".format(self._working_dir,
new_working_dir,
e))
return
# update the working directory if it has changed
if self._working_dir != new_working_dir:
self._working_dir = new_working_dir
for qemu_id in self._qemu_instances:
qemu_instance = self._qemu_instances[qemu_id]
qemu_instance.working_dir = os.path.join(self._working_dir, "qemu", "vm-{}".format(qemu_instance.id))
if "console_start_port_range" in request and "console_end_port_range" in request:
self._console_start_port_range = request["console_start_port_range"]
self._console_end_port_range = request["console_end_port_range"]
if "monitor_start_port_range" in request and "monitor_end_port_range" in request:
self._monitor_start_port_range = request["monitor_start_port_range"]
self._monitor_end_port_range = request["monitor_end_port_range"]
if "udp_start_port_range" in request and "udp_end_port_range" in request:
self._udp_start_port_range = request["udp_start_port_range"]
self._udp_end_port_range = request["udp_end_port_range"]
log.debug("received request {}".format(request))
@IModule.route("qemu.create")
def qemu_create(self, request):
"""
Creates a new QEMU VM instance.
Mandatory request parameters:
- name (QEMU VM name)
- qemu_path (path to the Qemu binary)
Optional request parameters:
- console (QEMU VM console port)
- monitor (QEMU VM monitor port)
Response parameters:
- id (QEMU VM instance identifier)
- name (QEMU VM name)
- default settings
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_CREATE_SCHEMA):
return
name = request["name"]
qemu_path = request["qemu_path"]
console = request.get("console")
monitor = request.get("monitor")
qemu_id = request.get("qemu_id")
try:
qemu_instance = QemuVM(name,
qemu_path,
self._working_dir,
self._host,
qemu_id,
console,
self._console_host,
self._console_start_port_range,
self._console_end_port_range,
monitor,
self._monitor_host,
self._monitor_start_port_range,
self._monitor_end_port_range)
except QemuError as e:
self.send_custom_error(str(e))
return
response = {"name": qemu_instance.name,
"id": qemu_instance.id}
defaults = qemu_instance.defaults()
response.update(defaults)
self._qemu_instances[qemu_instance.id] = qemu_instance
self.send_response(response)
@IModule.route("qemu.delete")
def qemu_delete(self, request):
"""
Deletes a QEMU VM instance.
Mandatory request parameters:
- id (QEMU VM instance identifier)
Response parameter:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_DELETE_SCHEMA):
return
# get the instance
qemu_instance = self.get_qemu_instance(request["id"])
if not qemu_instance:
return
try:
qemu_instance.clean_delete()
del self._qemu_instances[request["id"]]
except QemuError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("qemu.update")
def qemu_update(self, request):
"""
Updates a QEMU VM instance
Mandatory request parameters:
- id (QEMU VM instance identifier)
Optional request parameters:
- any setting to update
Response parameters:
- updated settings
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_UPDATE_SCHEMA):
return
# get the instance
qemu_instance = self.get_qemu_instance(request["id"])
if not qemu_instance:
return
# update the QEMU VM settings
response = {}
for name, value in request.items():
if hasattr(qemu_instance, name) and getattr(qemu_instance, name) != value:
try:
setattr(qemu_instance, name, value)
response[name] = value
except QemuError as e:
self.send_custom_error(str(e))
return
self.send_response(response)
@IModule.route("qemu.start")
def qemu_start(self, request):
"""
Starts a QEMU VM instance.
Mandatory request parameters:
- id (QEMU VM instance identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_START_SCHEMA):
return
# get the instance
qemu_instance = self.get_qemu_instance(request["id"])
if not qemu_instance:
return
try:
qemu_instance.start()
except QemuError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("qemu.stop")
def qemu_stop(self, request):
"""
Stops a QEMU VM instance.
Mandatory request parameters:
- id (QEMU VM instance identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_STOP_SCHEMA):
return
# get the instance
qemu_instance = self.get_qemu_instance(request["id"])
if not qemu_instance:
return
try:
qemu_instance.stop()
except QemuError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("qemu.reload")
def qemu_reload(self, request):
"""
Reloads a QEMU VM instance.
Mandatory request parameters:
- id (QEMU VM identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_RELOAD_SCHEMA):
return
# get the instance
qemu_instance = self.get_qemu_instance(request["id"])
if not qemu_instance:
return
try:
qemu_instance.reload()
except QemuError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("qemu.stop")
def qemu_stop(self, request):
"""
Stops a QEMU VM instance.
Mandatory request parameters:
- id (QEMU VM instance identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_STOP_SCHEMA):
return
# get the instance
qemu_instance = self.get_qemu_instance(request["id"])
if not qemu_instance:
return
try:
qemu_instance.stop()
except QemuError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("qemu.suspend")
def qemu_suspend(self, request):
"""
Suspends a QEMU VM instance.
Mandatory request parameters:
- id (QEMU VM instance identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_SUSPEND_SCHEMA):
return
# get the instance
qemu_instance = self.get_qemu_instance(request["id"])
if not qemu_instance:
return
try:
qemu_instance.suspend()
except QemuError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("qemu.allocate_udp_port")
def allocate_udp_port(self, request):
"""
Allocates a UDP port in order to create an UDP NIO.
Mandatory request parameters:
- id (QEMU VM identifier)
- port_id (unique port identifier)
Response parameters:
- port_id (unique port identifier)
- lport (allocated local port)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_ALLOCATE_UDP_PORT_SCHEMA):
return
# get the instance
qemu_instance = self.get_qemu_instance(request["id"])
if not qemu_instance:
return
try:
port = find_unused_port(self._udp_start_port_range,
self._udp_end_port_range,
host=self._host,
socket_type="UDP",
ignore_ports=self._allocated_udp_ports)
except Exception as e:
self.send_custom_error(str(e))
return
self._allocated_udp_ports.append(port)
log.info("{} [id={}] has allocated UDP port {} with host {}".format(qemu_instance.name,
qemu_instance.id,
port,
self._host))
response = {"lport": port,
"port_id": request["port_id"]}
self.send_response(response)
@IModule.route("qemu.add_nio")
def add_nio(self, request):
"""
Adds an NIO (Network Input/Output) for a QEMU VM instance.
Mandatory request parameters:
- id (QEMU VM instance identifier)
- port (port number)
- port_id (unique port identifier)
- nio (one of the following)
- type "nio_udp"
- lport (local port)
- rhost (remote host)
- rport (remote port)
Response parameters:
- port_id (unique port identifier)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_ADD_NIO_SCHEMA):
return
# get the instance
qemu_instance = self.get_qemu_instance(request["id"])
if not qemu_instance:
return
port = request["port"]
try:
nio = None
if request["nio"]["type"] == "nio_udp":
lport = request["nio"]["lport"]
rhost = request["nio"]["rhost"]
rport = request["nio"]["rport"]
try:
#TODO: handle IPv6
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.connect((rhost, rport))
except OSError as e:
raise QemuError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e))
nio = NIO_UDP(lport, rhost, rport)
if not nio:
raise QemuError("Requested NIO does not exist or is not supported: {}".format(request["nio"]["type"]))
except QemuError as e:
self.send_custom_error(str(e))
return
try:
qemu_instance.port_add_nio_binding(port, nio)
except QemuError as e:
self.send_custom_error(str(e))
return
self.send_response({"port_id": request["port_id"]})
@IModule.route("qemu.delete_nio")
def delete_nio(self, request):
"""
Deletes an NIO (Network Input/Output).
Mandatory request parameters:
- id (QEMU VM instance identifier)
- port (port identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_DELETE_NIO_SCHEMA):
return
# get the instance
qemu_instance = self.get_qemu_instance(request["id"])
if not qemu_instance:
return
port = request["port"]
try:
nio = qemu_instance.port_remove_nio_binding(port)
if isinstance(nio, NIO_UDP) and nio.lport in self._allocated_udp_ports:
self._allocated_udp_ports.remove(nio.lport)
except QemuError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
def _get_qemu_version(self, qemu_path):
"""
Gets the Qemu version.
:param qemu_path: path to Qemu
"""
if sys.platform.startswith("win"):
return ""
try:
output = subprocess.check_output([qemu_path, "-version"])
match = re.search("version\s+([0-9a-z\-\.]+)", output.decode("utf-8"))
if match:
version = match.group(1)
return version
else:
raise QemuError("Could not determine the Qemu version for {}".format(qemu_path))
except subprocess.SubprocessError as e:
raise QemuError("Error while looking for the Qemu version: {}".format(e))
@IModule.route("qemu.qemu_list")
def qemu_list(self, request):
"""
Gets QEMU binaries list.
Response parameters:
- List of Qemu binaries
"""
qemus = []
paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep)
# look for Qemu binaries in the current working directory and $PATH
if sys.platform.startswith("win"):
# add specific Windows paths
if hasattr(sys, "frozen"):
# add any qemu dir in the same location as gns3server.exe to the list of paths
exec_dir = os.path.dirname(os.path.abspath(sys.executable))
for f in os.listdir(exec_dir):
if f.lower().startswith("qemu"):
paths.append(os.path.join(exec_dir, f))
if "PROGRAMFILES(X86)" in os.environ and os.path.exists(os.environ["PROGRAMFILES(X86)"]):
paths.append(os.path.join(os.environ["PROGRAMFILES(X86)"], "qemu"))
if "PROGRAMFILES" in os.environ and os.path.exists(os.environ["PROGRAMFILES"]):
paths.append(os.path.join(os.environ["PROGRAMFILES"], "qemu"))
elif sys.platform.startswith("darwin"):
# add specific locations on Mac OS X regardless of what's in $PATH
paths.extend(["/usr/local/bin", "/opt/local/bin"])
if hasattr(sys, "frozen"):
paths.append(os.path.abspath(os.path.join(os.getcwd(), "../../../qemu/bin/")))
for path in paths:
try:
for f in os.listdir(path):
if (f.startswith("qemu-system") or f == "qemu" or f == "qemu.exe") and \
os.access(os.path.join(path, f), os.X_OK) and \
os.path.isfile(os.path.join(path, f)):
qemu_path = os.path.join(path, f)
version = self._get_qemu_version(qemu_path)
qemus.append({"path": qemu_path, "version": version})
except OSError:
continue
response = {"qemus": qemus}
self.send_response(response)
@IModule.route("qemu.echo")
def echo(self, request):
"""
Echo end point for testing purposes.
:param request: JSON request
"""
if request is None:
self.send_param_error()
else:
log.debug("received request {}".format(request))
self.send_response(request)
Whitespace-only changes.
+104
View File
@@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
class Adapter(object):
"""
Base class for adapters.
:param interfaces: number of interfaces supported by this adapter.
"""
def __init__(self, interfaces=1):
self._interfaces = interfaces
self._ports = {}
for port_id in range(0, interfaces):
self._ports[port_id] = None
def removable(self):
"""
Returns True if the adapter can be removed from a slot
and False if not.
:returns: boolean
"""
return True
def port_exists(self, port_id):
"""
Checks if a port exists on this adapter.
:returns: True is the port exists,
False otherwise.
"""
if port_id in self._ports:
return True
return False
def add_nio(self, port_id, nio):
"""
Adds a NIO to a port on this adapter.
:param port_id: port ID (integer)
:param nio: NIO instance
"""
self._ports[port_id] = nio
def remove_nio(self, port_id):
"""
Removes a NIO from a port on this adapter.
:param port_id: port ID (integer)
"""
self._ports[port_id] = None
def get_nio(self, port_id):
"""
Returns the NIO assigned to a port.
:params port_id: port ID (integer)
:returns: NIO instance
"""
return self._ports[port_id]
@property
def ports(self):
"""
Returns port to NIO mapping
:returns: dictionary port -> NIO
"""
return self._ports
@property
def interfaces(self):
"""
Returns the number of interfaces supported by this adapter.
:returns: number of interfaces
"""
return self._interfaces
@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .adapter import Adapter
class EthernetAdapter(Adapter):
"""
QEMU Ethernet adapter.
"""
def __init__(self):
Adapter.__init__(self, interfaces=1)
def __str__(self):
return "QEMU Ethernet adapter"
Whitespace-only changes.
+65
View File
@@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Base interface for NIOs.
"""
class NIO(object):
"""
Network Input/Output.
"""
def __init__(self):
self._capturing = False
self._pcap_output_file = ""
def startPacketCapture(self, pcap_output_file):
"""
:param pcap_output_file: PCAP destination file for the capture
"""
self._capturing = True
self._pcap_output_file = pcap_output_file
def stopPacketCapture(self):
self._capturing = False
self._pcap_output_file = ""
@property
def capturing(self):
"""
Returns either a capture is configured on this NIO.
:returns: boolean
"""
return self._capturing
@property
def pcap_output_file(self):
"""
Returns the path to the PCAP output file.
:returns: path to the PCAP output file
"""
return self._pcap_output_file
+75
View File
@@ -0,0 +1,75 @@
# -*- 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 UDP NIOs.
"""
from .nio import NIO
class NIO_UDP(NIO):
"""
UDP NIO.
:param lport: local port number
:param rhost: remote address/host
:param rport: remote port number
"""
_instance_count = 0
def __init__(self, lport, rhost, rport):
NIO.__init__(self)
self._lport = lport
self._rhost = rhost
self._rport = rport
@property
def lport(self):
"""
Returns the local port
:returns: local port number
"""
return self._lport
@property
def rhost(self):
"""
Returns the remote host
:returns: remote address/host
"""
return self._rhost
@property
def rport(self):
"""
Returns the remote port
:returns: remote port number
"""
return self._rport
def __str__(self):
return "NIO UDP"
+39
View File
@@ -0,0 +1,39 @@
# -*- 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/>.
"""
Custom exceptions for QEMU module.
"""
class QemuError(Exception):
def __init__(self, message, original_exception=None):
Exception.__init__(self, message)
if isinstance(message, Exception):
message = str(message)
self._message = message
self._original_exception = original_exception
def __repr__(self):
return self._message
def __str__(self):
return self._message
File diff suppressed because it is too large. Load diff
+423
View File
@@ -0,0 +1,423 @@
# -*- 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/>.
QEMU_CREATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to create a new QEMU VM instance",
"type": "object",
"properties": {
"name": {
"description": "QEMU VM instance name",
"type": "string",
"minLength": 1,
},
"qemu_path": {
"description": "Path to QEMU",
"type": "string",
"minLength": 1,
},
"qemu_id": {
"description": "QEMU VM instance ID",
"type": "integer"
},
"console": {
"description": "console TCP port",
"minimum": 1,
"maximum": 65535,
"type": "integer"
},
"monitor": {
"description": "monitor TCP port",
"minimum": 1,
"maximum": 65535,
"type": "integer"
},
},
"additionalProperties": False,
"required": ["name", "qemu_path"],
}
QEMU_DELETE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to delete a QEMU VM instance",
"type": "object",
"properties": {
"id": {
"description": "QEMU VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
QEMU_UPDATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to update a QEMU VM instance",
"type": "object",
"properties": {
"id": {
"description": "QEMU VM instance ID",
"type": "integer"
},
"name": {
"description": "QEMU VM instance name",
"type": "string",
"minLength": 1,
},
"qemu_path": {
"description": "path to QEMU",
"type": "string",
"minLength": 1,
},
"hda_disk_image": {
"description": "QEMU hda disk image path",
"type": "string",
},
"hdb_disk_image": {
"description": "QEMU hdb disk image path",
"type": "string",
},
"ram": {
"description": "amount of RAM in MB",
"type": "integer"
},
"adapters": {
"description": "number of adapters",
"type": "integer",
"minimum": 0,
"maximum": 32,
},
"adapter_type": {
"description": "QEMU adapter type",
"type": "string",
"minLength": 1,
},
"console": {
"description": "console TCP port",
"minimum": 1,
"maximum": 65535,
"type": "integer"
},
"monitor": {
"description": "monitor TCP port",
"minimum": 1,
"maximum": 65535,
"type": "integer"
},
"initrd": {
"description": "QEMU initrd path",
"type": "string",
},
"kernel_image": {
"description": "QEMU kernel image path",
"type": "string",
},
"kernel_command_line": {
"description": "QEMU kernel command line",
"type": "string",
},
"cloud_path": {
"description": "Path to the image in the cloud object store",
"type": "string",
},
"legacy_networking": {
"description": "Use QEMU legagy networking commands (-net syntax)",
"type": "boolean",
},
"cpu_throttling": {
"description": "Percentage of CPU allowed for QEMU",
"minimum": 0,
"maximum": 800,
"type": "integer",
},
"process_priority": {
"description": "Process priority for QEMU",
"enum": ["realtime",
"very high",
"high",
"normal",
"low",
"very low"]
},
"options": {
"description": "Additional QEMU options",
"type": "string",
},
},
"additionalProperties": False,
"required": ["id"]
}
QEMU_START_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to start a QEMU VM instance",
"type": "object",
"properties": {
"id": {
"description": "QEMU VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
QEMU_STOP_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to stop a QEMU VM instance",
"type": "object",
"properties": {
"id": {
"description": "QEMU VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
QEMU_SUSPEND_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to suspend a QEMU VM instance",
"type": "object",
"properties": {
"id": {
"description": "QEMU VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
QEMU_RELOAD_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to reload a QEMU VM instance",
"type": "object",
"properties": {
"id": {
"description": "QEMU VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
QEMU_ALLOCATE_UDP_PORT_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to allocate an UDP port for a QEMU VM instance",
"type": "object",
"properties": {
"id": {
"description": "QEMU VM instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the QEMU VM instance",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id", "port_id"]
}
QEMU_ADD_NIO_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to add a NIO for a QEMU VM instance",
"type": "object",
"definitions": {
"UDP": {
"description": "UDP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_udp"]
},
"lport": {
"description": "Local port",
"type": "integer",
"minimum": 1,
"maximum": 65535
},
"rhost": {
"description": "Remote host",
"type": "string",
"minLength": 1
},
"rport": {
"description": "Remote port",
"type": "integer",
"minimum": 1,
"maximum": 65535
}
},
"required": ["type", "lport", "rhost", "rport"],
"additionalProperties": False
},
"Ethernet": {
"description": "Generic Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_generic_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"LinuxEthernet": {
"description": "Linux Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_linux_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"TAP": {
"description": "TAP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_tap"]
},
"tap_device": {
"description": "TAP device name e.g. tap0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "tap_device"],
"additionalProperties": False
},
"UNIX": {
"description": "UNIX Network Input/Output",
"properties": {
"type": {
"enum": ["nio_unix"]
},
"local_file": {
"description": "path to the UNIX socket file (local)",
"type": "string",
"minLength": 1
},
"remote_file": {
"description": "path to the UNIX socket file (remote)",
"type": "string",
"minLength": 1
},
},
"required": ["type", "local_file", "remote_file"],
"additionalProperties": False
},
"VDE": {
"description": "VDE Network Input/Output",
"properties": {
"type": {
"enum": ["nio_vde"]
},
"control_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
"local_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
},
"required": ["type", "control_file", "local_file"],
"additionalProperties": False
},
"NULL": {
"description": "NULL Network Input/Output",
"properties": {
"type": {
"enum": ["nio_null"]
},
},
"required": ["type"],
"additionalProperties": False
},
},
"properties": {
"id": {
"description": "QEMU VM instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the QEMU VM instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 0,
"maximum": 32
},
"nio": {
"type": "object",
"description": "Network Input/Output",
"oneOf": [
{"$ref": "#/definitions/UDP"},
{"$ref": "#/definitions/Ethernet"},
{"$ref": "#/definitions/LinuxEthernet"},
{"$ref": "#/definitions/TAP"},
{"$ref": "#/definitions/UNIX"},
{"$ref": "#/definitions/VDE"},
{"$ref": "#/definitions/NULL"},
]
},
},
"additionalProperties": False,
"required": ["id", "port_id", "port", "nio"]
}
QEMU_DELETE_NIO_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to delete a NIO for a QEMU VM instance",
"type": "object",
"properties": {
"id": {
"description": "QEMU VM instance ID",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 0,
"maximum": 32
},
},
"additionalProperties": False,
"required": ["id", "port"]
}
+799
View File
@@ -0,0 +1,799 @@
# -*- 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/>.
"""
VirtualBox server module.
"""
import sys
import os
import socket
import shutil
import subprocess
from gns3server.modules import IModule
from gns3server.config import Config
from .virtualbox_vm import VirtualBoxVM
from .virtualbox_error import VirtualBoxError
from .nios.nio_udp import NIO_UDP
from ..attic import find_unused_port
from .schemas import VBOX_CREATE_SCHEMA
from .schemas import VBOX_DELETE_SCHEMA
from .schemas import VBOX_UPDATE_SCHEMA
from .schemas import VBOX_START_SCHEMA
from .schemas import VBOX_STOP_SCHEMA
from .schemas import VBOX_SUSPEND_SCHEMA
from .schemas import VBOX_RELOAD_SCHEMA
from .schemas import VBOX_ALLOCATE_UDP_PORT_SCHEMA
from .schemas import VBOX_ADD_NIO_SCHEMA
from .schemas import VBOX_DELETE_NIO_SCHEMA
from .schemas import VBOX_START_CAPTURE_SCHEMA
from .schemas import VBOX_STOP_CAPTURE_SCHEMA
import logging
log = logging.getLogger(__name__)
class VirtualBox(IModule):
"""
VirtualBox module.
:param name: module name
:param args: arguments for the module
:param kwargs: named arguments for the module
"""
def __init__(self, name, *args, **kwargs):
# get the vboxmanage location
self._vboxmanage_path = None
if sys.platform.startswith("win"):
if "VBOX_INSTALL_PATH" in os.environ:
self._vboxmanage_path = os.path.join(os.environ["VBOX_INSTALL_PATH"], "VBoxManage.exe")
elif "VBOX_MSI_INSTALL_PATH" in os.environ:
self._vboxmanage_path = os.path.join(os.environ["VBOX_MSI_INSTALL_PATH"], "VBoxManage.exe")
elif sys.platform.startswith("darwin"):
self._vboxmanage_path = "/Applications/VirtualBox.app/Contents/MacOS/VBoxManage"
else:
config = Config.instance()
vbox_config = config.get_section_config(name.upper())
self._vboxmanage_path = vbox_config.get("vboxmanage_path")
if not self._vboxmanage_path or not os.path.isfile(self._vboxmanage_path):
paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep)
# look for vboxmanage in the current working directory and $PATH
for path in paths:
try:
if "vboxmanage" in [s.lower() for s in os.listdir(path)] and os.access(os.path.join(path, "vboxmanage"), os.X_OK):
self._vboxmanage_path = os.path.join(path, "vboxmanage")
break
except OSError:
continue
if not self._vboxmanage_path:
log.warning("vboxmanage couldn't be found!")
elif not os.access(self._vboxmanage_path, os.X_OK):
log.warning("vboxmanage is not executable")
self._vbox_user = None
# a new process start when calling IModule
IModule.__init__(self, name, *args, **kwargs)
self._vbox_instances = {}
config = Config.instance()
vbox_config = config.get_section_config(name.upper())
self._console_start_port_range = vbox_config.get("console_start_port_range", 3501)
self._console_end_port_range = vbox_config.get("console_end_port_range", 4000)
self._allocated_udp_ports = []
self._udp_start_port_range = vbox_config.get("udp_start_port_range", 35001)
self._udp_end_port_range = vbox_config.get("udp_end_port_range", 35500)
self._host = vbox_config.get("host", kwargs["host"])
self._console_host = vbox_config.get("console_host", kwargs["console_host"])
self._projects_dir = kwargs["projects_dir"]
self._tempdir = kwargs["temp_dir"]
self._working_dir = self._projects_dir
def stop(self, signum=None):
"""
Properly stops the module.
:param signum: signal number (if called by the signal handler)
"""
# delete all VirtualBox instances
for vbox_id in self._vbox_instances:
vbox_instance = self._vbox_instances[vbox_id]
try:
vbox_instance.delete()
except VirtualBoxError:
continue
IModule.stop(self, signum) # this will stop the I/O loop
def get_vbox_instance(self, vbox_id):
"""
Returns a VirtualBox VM instance.
:param vbox_id: VirtualBox VM identifier
:returns: VirtualBoxVM instance
"""
if vbox_id not in self._vbox_instances:
log.debug("VirtualBox VM ID {} doesn't exist".format(vbox_id), exc_info=1)
self.send_custom_error("VirtualBox VM ID {} doesn't exist".format(vbox_id))
return None
return self._vbox_instances[vbox_id]
@IModule.route("virtualbox.reset")
def reset(self, request):
"""
Resets the module.
:param request: JSON request
"""
# delete all VirtualBox instances
for vbox_id in self._vbox_instances:
vbox_instance = self._vbox_instances[vbox_id]
vbox_instance.delete()
# resets the instance IDs
VirtualBoxVM.reset()
self._vbox_instances.clear()
self._allocated_udp_ports.clear()
self._working_dir = self._projects_dir
log.info("VirtualBox module has been reset")
@IModule.route("virtualbox.settings")
def settings(self, request):
"""
Set or update settings.
Optional request parameters:
- working_dir (path to a working directory)
- vboxmanage_path (path to vboxmanage)
- project_name
- console_start_port_range
- console_end_port_range
- udp_start_port_range
- udp_end_port_range
:param request: JSON request
"""
if request is None:
self.send_param_error()
return
if "working_dir" in request:
new_working_dir = request["working_dir"]
log.info("this server is local with working directory path to {}".format(new_working_dir))
else:
new_working_dir = os.path.join(self._projects_dir, request["project_name"])
log.info("this server is remote with working directory path to {}".format(new_working_dir))
if self._projects_dir != self._working_dir != new_working_dir:
if not os.path.isdir(new_working_dir):
try:
shutil.move(self._working_dir, new_working_dir)
except OSError as e:
log.error("could not move working directory from {} to {}: {}".format(self._working_dir,
new_working_dir,
e))
return
# update the working directory if it has changed
if self._working_dir != new_working_dir:
self._working_dir = new_working_dir
for vbox_id in self._vbox_instances:
vbox_instance = self._vbox_instances[vbox_id]
vbox_instance.working_dir = os.path.join(self._working_dir, "vbox", "{}".format(vbox_instance.name))
if "vboxmanage_path" in request:
self._vboxmanage_path = request["vboxmanage_path"]
if "vbox_user" in request:
self._vbox_user = request["vbox_user"]
if "console_start_port_range" in request and "console_end_port_range" in request:
self._console_start_port_range = request["console_start_port_range"]
self._console_end_port_range = request["console_end_port_range"]
if "udp_start_port_range" in request and "udp_end_port_range" in request:
self._udp_start_port_range = request["udp_start_port_range"]
self._udp_end_port_range = request["udp_end_port_range"]
log.debug("received request {}".format(request))
@IModule.route("virtualbox.create")
def vbox_create(self, request):
"""
Creates a new VirtualBox VM instance.
Mandatory request parameters:
- name (VirtualBox VM name)
- vmname (VirtualBox VM name in VirtualBox)
- linked_clone (Flag to create a linked clone)
Optional request parameters:
- console (VirtualBox VM console port)
Response parameters:
- id (VirtualBox VM instance identifier)
- name (VirtualBox VM name)
- default settings
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VBOX_CREATE_SCHEMA):
return
name = request["name"]
vmname = request["vmname"]
linked_clone = request["linked_clone"]
console = request.get("console")
vbox_id = request.get("vbox_id")
try:
if not self._vboxmanage_path or not os.path.exists(self._vboxmanage_path):
raise VirtualBoxError("Could not find VBoxManage, is VirtualBox correctly installed?")
vbox_instance = VirtualBoxVM(self._vboxmanage_path,
self._vbox_user,
name,
vmname,
linked_clone,
self._working_dir,
vbox_id,
console,
self._console_host,
self._console_start_port_range,
self._console_end_port_range)
except VirtualBoxError as e:
self.send_custom_error(str(e))
return
response = {"name": vbox_instance.name,
"id": vbox_instance.id}
defaults = vbox_instance.defaults()
response.update(defaults)
self._vbox_instances[vbox_instance.id] = vbox_instance
self.send_response(response)
@IModule.route("virtualbox.delete")
def vbox_delete(self, request):
"""
Deletes a VirtualBox VM instance.
Mandatory request parameters:
- id (VirtualBox VM instance identifier)
Response parameter:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VBOX_DELETE_SCHEMA):
return
# get the instance
vbox_instance = self.get_vbox_instance(request["id"])
if not vbox_instance:
return
try:
vbox_instance.clean_delete()
del self._vbox_instances[request["id"]]
except VirtualBoxError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("virtualbox.update")
def vbox_update(self, request):
"""
Updates a VirtualBox VM instance
Mandatory request parameters:
- id (VirtualBox VM instance identifier)
Optional request parameters:
- any setting to update
Response parameters:
- updated settings
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VBOX_UPDATE_SCHEMA):
return
# get the instance
vbox_instance = self.get_vbox_instance(request["id"])
if not vbox_instance:
return
# update the VirtualBox VM settings
response = {}
for name, value in request.items():
if hasattr(vbox_instance, name) and getattr(vbox_instance, name) != value:
try:
setattr(vbox_instance, name, value)
response[name] = value
except VirtualBoxError as e:
self.send_custom_error(str(e))
return
self.send_response(response)
@IModule.route("virtualbox.start")
def vbox_start(self, request):
"""
Starts a VirtualBox VM instance.
Mandatory request parameters:
- id (VirtualBox VM instance identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VBOX_START_SCHEMA):
return
# get the instance
vbox_instance = self.get_vbox_instance(request["id"])
if not vbox_instance:
return
try:
vbox_instance.start()
except VirtualBoxError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("virtualbox.stop")
def vbox_stop(self, request):
"""
Stops a VirtualBox VM instance.
Mandatory request parameters:
- id (VirtualBox VM instance identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VBOX_STOP_SCHEMA):
return
# get the instance
vbox_instance = self.get_vbox_instance(request["id"])
if not vbox_instance:
return
try:
vbox_instance.stop()
except VirtualBoxError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("virtualbox.reload")
def vbox_reload(self, request):
"""
Reloads a VirtualBox VM instance.
Mandatory request parameters:
- id (VirtualBox VM identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VBOX_RELOAD_SCHEMA):
return
# get the instance
vbox_instance = self.get_vbox_instance(request["id"])
if not vbox_instance:
return
try:
vbox_instance.reload()
except VirtualBoxError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("virtualbox.stop")
def vbox_stop(self, request):
"""
Stops a VirtualBox VM instance.
Mandatory request parameters:
- id (VirtualBox VM instance identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VBOX_STOP_SCHEMA):
return
# get the instance
vbox_instance = self.get_vbox_instance(request["id"])
if not vbox_instance:
return
try:
vbox_instance.stop()
except VirtualBoxError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("virtualbox.suspend")
def vbox_suspend(self, request):
"""
Suspends a VirtualBox VM instance.
Mandatory request parameters:
- id (VirtualBox VM instance identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VBOX_SUSPEND_SCHEMA):
return
# get the instance
vbox_instance = self.get_vbox_instance(request["id"])
if not vbox_instance:
return
try:
vbox_instance.suspend()
except VirtualBoxError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("virtualbox.allocate_udp_port")
def allocate_udp_port(self, request):
"""
Allocates a UDP port in order to create an UDP NIO.
Mandatory request parameters:
- id (VirtualBox VM identifier)
- port_id (unique port identifier)
Response parameters:
- port_id (unique port identifier)
- lport (allocated local port)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VBOX_ALLOCATE_UDP_PORT_SCHEMA):
return
# get the instance
vbox_instance = self.get_vbox_instance(request["id"])
if not vbox_instance:
return
try:
port = find_unused_port(self._udp_start_port_range,
self._udp_end_port_range,
host=self._host,
socket_type="UDP",
ignore_ports=self._allocated_udp_ports)
except Exception as e:
self.send_custom_error(str(e))
return
self._allocated_udp_ports.append(port)
log.info("{} [id={}] has allocated UDP port {} with host {}".format(vbox_instance.name,
vbox_instance.id,
port,
self._host))
response = {"lport": port,
"port_id": request["port_id"]}
self.send_response(response)
@IModule.route("virtualbox.add_nio")
def add_nio(self, request):
"""
Adds an NIO (Network Input/Output) for a VirtualBox VM instance.
Mandatory request parameters:
- id (VirtualBox VM instance identifier)
- port (port number)
- port_id (unique port identifier)
- nio (one of the following)
- type "nio_udp"
- lport (local port)
- rhost (remote host)
- rport (remote port)
Response parameters:
- port_id (unique port identifier)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VBOX_ADD_NIO_SCHEMA):
return
# get the instance
vbox_instance = self.get_vbox_instance(request["id"])
if not vbox_instance:
return
port = request["port"]
try:
nio = None
if request["nio"]["type"] == "nio_udp":
lport = request["nio"]["lport"]
rhost = request["nio"]["rhost"]
rport = request["nio"]["rport"]
try:
#TODO: handle IPv6
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.connect((rhost, rport))
except OSError as e:
raise VirtualBoxError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e))
nio = NIO_UDP(lport, rhost, rport)
if not nio:
raise VirtualBoxError("Requested NIO does not exist or is not supported: {}".format(request["nio"]["type"]))
except VirtualBoxError as e:
self.send_custom_error(str(e))
return
try:
vbox_instance.port_add_nio_binding(port, nio)
except VirtualBoxError as e:
self.send_custom_error(str(e))
return
self.send_response({"port_id": request["port_id"]})
@IModule.route("virtualbox.delete_nio")
def delete_nio(self, request):
"""
Deletes an NIO (Network Input/Output).
Mandatory request parameters:
- id (VirtualBox instance identifier)
- port (port identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VBOX_DELETE_NIO_SCHEMA):
return
# get the instance
vbox_instance = self.get_vbox_instance(request["id"])
if not vbox_instance:
return
port = request["port"]
try:
nio = vbox_instance.port_remove_nio_binding(port)
if isinstance(nio, NIO_UDP) and nio.lport in self._allocated_udp_ports:
self._allocated_udp_ports.remove(nio.lport)
except VirtualBoxError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("virtualbox.start_capture")
def vbox_start_capture(self, request):
"""
Starts a packet capture.
Mandatory request parameters:
- id (VirtualBox VM identifier)
- port (port number)
- port_id (port identifier)
- capture_file_name
Response parameters:
- port_id (port identifier)
- capture_file_path (path to the capture file)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VBOX_START_CAPTURE_SCHEMA):
return
# get the instance
vbox_instance = self.get_vbox_instance(request["id"])
if not vbox_instance:
return
port = request["port"]
capture_file_name = request["capture_file_name"]
try:
capture_file_path = os.path.join(self._working_dir, "captures", capture_file_name)
vbox_instance.start_capture(port, capture_file_path)
except VirtualBoxError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"],
"capture_file_path": capture_file_path}
self.send_response(response)
@IModule.route("virtualbox.stop_capture")
def vbox_stop_capture(self, request):
"""
Stops a packet capture.
Mandatory request parameters:
- id (VirtualBox VM identifier)
- port (port number)
- port_id (port identifier)
Response parameters:
- port_id (port identifier)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VBOX_STOP_CAPTURE_SCHEMA):
return
# get the instance
vbox_instance = self.get_vbox_instance(request["id"])
if not vbox_instance:
return
port = request["port"]
try:
vbox_instance.stop_capture(port)
except VirtualBoxError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"]}
self.send_response(response)
def _execute_vboxmanage(self, user, command):
"""
Executes VBoxManage and return its result.
:param command: command to execute (list)
:returns: VBoxManage output
"""
try:
if not user.strip() or sys.platform.startswith("win"):
result = subprocess.check_output(command, stderr=subprocess.STDOUT, timeout=60)
else:
sudo_command = "sudo -i -u " + user.strip() + " " + " ".join(command)
result = subprocess.check_output(sudo_command, stderr=subprocess.STDOUT, shell=True, timeout=60)
except (OSError, subprocess.SubprocessError) as e:
raise VirtualBoxError("Could not execute VBoxManage {}".format(e))
return result.decode("utf-8", errors="ignore")
@IModule.route("virtualbox.vm_list")
def vm_list(self, request):
"""
Gets VirtualBox VM list.
Response parameters:
- Server address/host
- List of VM names
"""
try:
if request and "vboxmanage_path" in request:
vboxmanage_path = request["vboxmanage_path"]
else:
vboxmanage_path = self._vboxmanage_path
if request and "vbox_user" in request:
vbox_user = request["vbox_user"]
else:
vbox_user = self._vbox_user
if not vboxmanage_path or not os.path.exists(vboxmanage_path):
raise VirtualBoxError("Could not find VBoxManage, is VirtualBox correctly installed?")
command = [vboxmanage_path, "--nologo", "list", "vms"]
result = self._execute_vboxmanage(vbox_user, command)
except VirtualBoxError as e:
self.send_custom_error(str(e))
return
vms = []
for line in result.splitlines():
vmname, uuid = line.rsplit(' ', 1)
vmname = vmname.strip('"')
if vmname == "<inaccessible>":
continue # ignore inaccessible VMs
try:
extra_data = self._execute_vboxmanage(vbox_user, [vboxmanage_path, "getextradata", vmname, "GNS3/Clone"]).strip()
except VirtualBoxError as e:
self.send_custom_error(str(e))
return
if not extra_data == "Value: yes":
vms.append(vmname)
response = {"vms": vms}
self.send_response(response)
@IModule.route("virtualbox.echo")
def echo(self, request):
"""
Echo end point for testing purposes.
:param request: JSON request
"""
if request is None:
self.send_param_error()
else:
log.debug("received request {}".format(request))
self.send_response(request)
Whitespace-only changes.
@@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
class Adapter(object):
"""
Base class for adapters.
:param interfaces: number of interfaces supported by this adapter.
"""
def __init__(self, interfaces=1):
self._interfaces = interfaces
self._ports = {}
for port_id in range(0, interfaces):
self._ports[port_id] = None
def removable(self):
"""
Returns True if the adapter can be removed from a slot
and False if not.
:returns: boolean
"""
return True
def port_exists(self, port_id):
"""
Checks if a port exists on this adapter.
:returns: True is the port exists,
False otherwise.
"""
if port_id in self._ports:
return True
return False
def add_nio(self, port_id, nio):
"""
Adds a NIO to a port on this adapter.
:param port_id: port ID (integer)
:param nio: NIO instance
"""
self._ports[port_id] = nio
def remove_nio(self, port_id):
"""
Removes a NIO from a port on this adapter.
:param port_id: port ID (integer)
"""
self._ports[port_id] = None
def get_nio(self, port_id):
"""
Returns the NIO assigned to a port.
:params port_id: port ID (integer)
:returns: NIO instance
"""
return self._ports[port_id]
@property
def ports(self):
"""
Returns port to NIO mapping
:returns: dictionary port -> NIO
"""
return self._ports
@property
def interfaces(self):
"""
Returns the number of interfaces supported by this adapter.
:returns: number of interfaces
"""
return self._interfaces
@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .adapter import Adapter
class EthernetAdapter(Adapter):
"""
VirtualBox Ethernet adapter.
"""
def __init__(self):
Adapter.__init__(self, interfaces=1)
def __str__(self):
return "VirtualBox Ethernet adapter"
Whitespace-only changes.
+65
View File
@@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Base interface for NIOs.
"""
class NIO(object):
"""
Network Input/Output.
"""
def __init__(self):
self._capturing = False
self._pcap_output_file = ""
def startPacketCapture(self, pcap_output_file):
"""
:param pcap_output_file: PCAP destination file for the capture
"""
self._capturing = True
self._pcap_output_file = pcap_output_file
def stopPacketCapture(self):
self._capturing = False
self._pcap_output_file = ""
@property
def capturing(self):
"""
Returns either a capture is configured on this NIO.
:returns: boolean
"""
return self._capturing
@property
def pcap_output_file(self):
"""
Returns the path to the PCAP output file.
:returns: path to the PCAP output file
"""
return self._pcap_output_file
@@ -0,0 +1,75 @@
# -*- 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 UDP NIOs.
"""
from .nio import NIO
class NIO_UDP(NIO):
"""
UDP NIO.
:param lport: local port number
:param rhost: remote address/host
:param rport: remote port number
"""
_instance_count = 0
def __init__(self, lport, rhost, rport):
NIO.__init__(self)
self._lport = lport
self._rhost = rhost
self._rport = rport
@property
def lport(self):
"""
Returns the local port
:returns: local port number
"""
return self._lport
@property
def rhost(self):
"""
Returns the remote host
:returns: remote address/host
"""
return self._rhost
@property
def rport(self):
"""
Returns the remote port
:returns: remote port number
"""
return self._rport
def __str__(self):
return "NIO UDP"
+432
View File
@@ -0,0 +1,432 @@
# -*- 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/>.
VBOX_CREATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to create a new VirtualBox VM instance",
"type": "object",
"properties": {
"name": {
"description": "VirtualBox VM instance name",
"type": "string",
"minLength": 1,
},
"vmname": {
"description": "VirtualBox VM name (in VirtualBox itself)",
"type": "string",
"minLength": 1,
},
"linked_clone": {
"description": "either the VM is a linked clone or not",
"type": "boolean"
},
"vbox_id": {
"description": "VirtualBox VM instance ID",
"type": "integer"
},
"console": {
"description": "console TCP port",
"minimum": 1,
"maximum": 65535,
"type": "integer"
},
},
"additionalProperties": False,
"required": ["name", "vmname"],
}
VBOX_DELETE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to delete a VirtualBox VM instance",
"type": "object",
"properties": {
"id": {
"description": "VirtualBox VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
VBOX_UPDATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to update a VirtualBox VM instance",
"type": "object",
"properties": {
"id": {
"description": "VirtualBox VM instance ID",
"type": "integer"
},
"name": {
"description": "VirtualBox VM instance name",
"type": "string",
"minLength": 1,
},
"vmname": {
"description": "VirtualBox VM name (in VirtualBox itself)",
"type": "string",
"minLength": 1,
},
"adapters": {
"description": "number of adapters",
"type": "integer",
"minimum": 1,
"maximum": 36, # maximum given by the ICH9 chipset in VirtualBox
},
"adapter_start_index": {
"description": "adapter index from which to start using adapters",
"type": "integer",
"minimum": 0,
"maximum": 35, # maximum given by the ICH9 chipset in VirtualBox
},
"adapter_type": {
"description": "VirtualBox adapter type",
"type": "string",
"minLength": 1,
},
"console": {
"description": "console TCP port",
"minimum": 1,
"maximum": 65535,
"type": "integer"
},
"enable_remote_console": {
"description": "enable the remote console",
"type": "boolean"
},
"headless": {
"description": "headless mode",
"type": "boolean"
},
},
"additionalProperties": False,
"required": ["id"]
}
VBOX_START_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to start a VirtualBox VM instance",
"type": "object",
"properties": {
"id": {
"description": "VirtualBox VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
VBOX_STOP_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to stop a VirtualBox VM instance",
"type": "object",
"properties": {
"id": {
"description": "VirtualBox VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
VBOX_SUSPEND_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to suspend a VirtualBox VM instance",
"type": "object",
"properties": {
"id": {
"description": "VirtualBox VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
VBOX_RELOAD_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to reload a VirtualBox VM instance",
"type": "object",
"properties": {
"id": {
"description": "VirtualBox VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
VBOX_ALLOCATE_UDP_PORT_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to allocate an UDP port for a VirtualBox VM instance",
"type": "object",
"properties": {
"id": {
"description": "VirtualBox VM instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the VirtualBox VM instance",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id", "port_id"]
}
VBOX_ADD_NIO_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to add a NIO for a VirtualBox VM instance",
"type": "object",
"definitions": {
"UDP": {
"description": "UDP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_udp"]
},
"lport": {
"description": "Local port",
"type": "integer",
"minimum": 1,
"maximum": 65535
},
"rhost": {
"description": "Remote host",
"type": "string",
"minLength": 1
},
"rport": {
"description": "Remote port",
"type": "integer",
"minimum": 1,
"maximum": 65535
}
},
"required": ["type", "lport", "rhost", "rport"],
"additionalProperties": False
},
"Ethernet": {
"description": "Generic Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_generic_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"LinuxEthernet": {
"description": "Linux Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_linux_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"TAP": {
"description": "TAP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_tap"]
},
"tap_device": {
"description": "TAP device name e.g. tap0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "tap_device"],
"additionalProperties": False
},
"UNIX": {
"description": "UNIX Network Input/Output",
"properties": {
"type": {
"enum": ["nio_unix"]
},
"local_file": {
"description": "path to the UNIX socket file (local)",
"type": "string",
"minLength": 1
},
"remote_file": {
"description": "path to the UNIX socket file (remote)",
"type": "string",
"minLength": 1
},
},
"required": ["type", "local_file", "remote_file"],
"additionalProperties": False
},
"VDE": {
"description": "VDE Network Input/Output",
"properties": {
"type": {
"enum": ["nio_vde"]
},
"control_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
"local_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
},
"required": ["type", "control_file", "local_file"],
"additionalProperties": False
},
"NULL": {
"description": "NULL Network Input/Output",
"properties": {
"type": {
"enum": ["nio_null"]
},
},
"required": ["type"],
"additionalProperties": False
},
},
"properties": {
"id": {
"description": "VirtualBox VM instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the VirtualBox VM instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 0,
"maximum": 36 # maximum given by the ICH9 chipset in VirtualBox
},
"nio": {
"type": "object",
"description": "Network Input/Output",
"oneOf": [
{"$ref": "#/definitions/UDP"},
{"$ref": "#/definitions/Ethernet"},
{"$ref": "#/definitions/LinuxEthernet"},
{"$ref": "#/definitions/TAP"},
{"$ref": "#/definitions/UNIX"},
{"$ref": "#/definitions/VDE"},
{"$ref": "#/definitions/NULL"},
]
},
},
"additionalProperties": False,
"required": ["id", "port_id", "port", "nio"]
}
VBOX_DELETE_NIO_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to delete a NIO for a VirtualBox VM instance",
"type": "object",
"properties": {
"id": {
"description": "VirtualBox VM instance ID",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 0,
"maximum": 36 # maximum given by the ICH9 chipset in VirtualBox
},
},
"additionalProperties": False,
"required": ["id", "port"]
}
VBOX_START_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to start a packet capture on a VirtualBox VM instance port",
"type": "object",
"properties": {
"id": {
"description": "VirtualBox VM instance ID",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 0,
"maximum": 36 # maximum given by the ICH9 chipset in VirtualBox
},
"port_id": {
"description": "Unique port identifier for the VirtualBox VM instance",
"type": "integer"
},
"capture_file_name": {
"description": "Capture file name",
"type": "string",
"minLength": 1,
},
},
"additionalProperties": False,
"required": ["id", "port", "port_id", "capture_file_name"]
}
VBOX_STOP_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to stop a packet capture on a VirtualBox VM instance port",
"type": "object",
"properties": {
"id": {
"description": "VirtualBox VM instance ID",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 0,
"maximum": 36 # maximum given by the ICH9 chipset in VirtualBox
},
"port_id": {
"description": "Unique port identifier for the VirtualBox VM instance",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id", "port", "port_id"]
}
@@ -0,0 +1,444 @@
# -*- 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/>.
import sys
import time
import threading
import socket
import select
import logging
log = logging.getLogger(__name__)
if sys.platform.startswith("win"):
import win32pipe
import win32file
class TelnetServer(threading.Thread):
"""
Mini Telnet Server.
:param vm_name: Virtual machine name
:param pipe_path: path to VM pipe (UNIX socket on Linux/UNIX, Named Pipe on Windows)
:param host: server host
:param port: server port
"""
def __init__(self, vm_name, pipe_path, host, port):
self._vm_name = vm_name
self._pipe = pipe_path
self._host = host
self._port = port
self._reader_thread = None
self._use_thread = False
self._write_lock = threading.Lock()
self._clients = {}
self._timeout = 1
self._alive = True
if sys.platform.startswith("win"):
# we must a thread for reading the pipe on Windows because it is a Named Pipe and it cannot be monitored by select()
self._use_thread = True
try:
if ":" in self._host:
# IPv6 address support
self._server_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
else:
self._server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self._server_socket.bind((self._host, self._port))
self._server_socket.listen(socket.SOMAXCONN)
except OSError as e:
log.critical("unable to create a server socket: {}".format(e))
return
threading.Thread.__init__(self)
log.info("Telnet server initialized, waiting for clients on {}:{}".format(self._host, self._port))
def run(self):
"""
Thread loop.
"""
while True:
recv_list = [self._server_socket.fileno()]
if not self._use_thread:
recv_list.append(self._pipe.fileno())
for client in self._clients.values():
if client.is_active():
recv_list.append(client.socket().fileno())
else:
del self._clients[client.socket().fileno()]
try:
client.socket().shutdown(socket.SHUT_RDWR)
except OSError as e:
log.warn("shutdown: {}".format(e))
client.socket().close()
break
try:
rlist, slist, elist = select.select(recv_list, [], [], self._timeout)
except OSError as e:
log.critical("fatal select error: {}".format(e))
return False
if not self._alive:
log.info("Telnet server for {} is exiting".format(self._vm_name))
return True
for sock_fileno in rlist:
if sock_fileno == self._server_socket.fileno():
try:
sock, addr = self._server_socket.accept()
host, port = addr
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
log.info("new client {}:{} has connected".format(host, port))
except OSError as e:
log.error("could not accept new client: {}".format(e))
continue
new_client = TelnetClient(self._vm_name, sock, host, port)
self._clients[sock.fileno()] = new_client
if self._use_thread and not self._reader_thread:
self._reader_thread = threading.Thread(target=self._reader, daemon=True)
self._reader_thread.start()
elif not self._use_thread and sock_fileno == self._pipe.fileno():
data = self._read_from_pipe()
if not data:
log.warning("pipe has been closed!")
return False
for client in self._clients.values():
try:
client.send(data)
except OSError as e:
log.debug(e)
client.deactivate()
elif sock_fileno in self._clients:
try:
data = self._clients[sock_fileno].socket_recv()
if not data:
continue
# For some reason, windows likes to send "cr/lf" when you send a "cr".
# Strip that so we don't get a double prompt.
data = data.replace(b"\r\n", b"\n")
self._write_to_pipe(data)
except Exception as msg:
log.info(msg)
self._clients[sock_fileno].deactivate()
def _write_to_pipe(self, data):
"""
Writes data to the pipe.
:param data: data to write
"""
if sys.platform.startswith('win'):
win32file.WriteFile(self._pipe, data)
else:
self._pipe.sendall(data)
def _read_from_pipe(self):
"""
Reads data from the pipe.
:returns: data
"""
if sys.platform.startswith('win'):
(read, num_avail, num_message) = win32pipe.PeekNamedPipe(self._pipe, 0)
if num_avail > 0:
(error_code, output) = win32file.ReadFile(self._pipe, num_avail, None)
return output
return b""
else:
return self._pipe.recv(1024)
def _reader(self):
"""
Loops forever and copy everything from the pipe to the socket.
"""
log.debug("reader thread has started")
while self._alive:
try:
data = self._read_from_pipe()
if not data and not sys.platform.startswith('win'):
log.debug("pipe has been closed! (no data)")
break
self._write_lock.acquire()
try:
for client in self._clients.values():
client.send(data)
finally:
self._write_lock.release()
if sys.platform.startswith('win'):
# sleep every 10 ms
time.sleep(0.01)
except Exception as e:
log.debug("pipe has been closed! {}".format(e))
break
log.debug("reader thread exited")
self.stop()
def stop(self):
"""
Stops the server.
"""
if self._alive:
self._alive = False
for client in self._clients.values():
client.socket().close()
client.deactivate()
# Mostly from https://code.google.com/p/miniboa/source/browse/trunk/miniboa/telnet.py
# Telnet Commands
SE = 240 # End of sub-negotiation parameters
NOP = 241 # No operation
DATMK = 242 # Data stream portion of a sync.
BREAK = 243 # NVT Character BRK
IP = 244 # Interrupt Process
AO = 245 # Abort Output
AYT = 246 # Are you there
EC = 247 # Erase Character
EL = 248 # Erase Line
GA = 249 # The Go Ahead Signal
SB = 250 # Sub-option to follow
WILL = 251 # Will; request or confirm option begin
WONT = 252 # Wont; deny option request
DO = 253 # Do = Request or confirm remote option
DONT = 254 # Don't = Demand or confirm option halt
IAC = 255 # Interpret as Command
SEND = 1 # Sub-process negotiation SEND command
IS = 0 # Sub-process negotiation IS command
# Telnet Options
BINARY = 0 # Transmit Binary
ECHO = 1 # Echo characters back to sender
RECON = 2 # Reconnection
SGA = 3 # Suppress Go-Ahead
TMARK = 6 # Timing Mark
TTYPE = 24 # Terminal Type
NAWS = 31 # Negotiate About Window Size
LINEMO = 34 # Line Mode
class TelnetClient(object):
"""
Represents a Telnet client connection.
:param vm_name: VM name
:param sock: socket connection
:param host: IP of the Telnet client
:param port: port of the Telnet client
"""
def __init__(self, vm_name, sock, host, port):
self._active = True
self._sock = sock
self._host = host
self._port = port
sock.send(bytes([IAC, WILL, ECHO,
IAC, WILL, SGA,
IAC, WILL, BINARY,
IAC, DO, BINARY]))
welcome_msg = "{} console is now available... Press RETURN to get started.\r\n".format(vm_name)
sock.send(welcome_msg.encode('utf-8'))
def is_active(self):
"""
Returns either the client is active or not.
:return: boolean
"""
return self._active
def socket(self):
"""
Returns the socket for this Telnet client.
:returns: socket instance.
"""
return self._sock
def send(self, data):
"""
Sends data to the remote end.
:param data: data to send
"""
try:
self._sock.send(data)
except OSError as e:
self._active = False
raise Exception("Socket send: {}".format(e))
def deactivate(self):
"""
Sets the client to disconnect on the next server poll.
"""
self._active = False
def socket_recv(self):
"""
Called by Telnet Server when data is ready.
"""
try:
buf = self._sock.recv(1024)
except BlockingIOError:
return None
except ConnectionResetError:
buf = b''
# is the connection closed?
if not buf:
raise Exception("connection closed by {}:{}".format(self._host, self._port))
# Process and remove any telnet commands from the buffer
if IAC in buf:
buf = self._IAC_parser(buf)
return buf
def _read_block(self, bufsize):
"""
Reads a block for data from the socket.
:param bufsize: size of the buffer
:returns: data read
"""
buf = self._sock.recv(1024, socket.MSG_WAITALL)
# If we don't get everything we were looking for then the
# client probably disconnected.
if len(buf) < bufsize:
raise Exception("connection closed by {}:{}".format(self._host, self._port))
return buf
def _IAC_parser(self, buf):
"""
Processes and removes any Telnet commands from the buffer.
:param buf: buffer
:returns: buffer minus Telnet commands
"""
skip_to = 0
while self._active:
# Locate an IAC to process
iac_loc = buf.find(IAC, skip_to)
if iac_loc < 0:
break
# Get the TELNET command
iac_cmd = bytearray([IAC])
try:
iac_cmd.append(buf[iac_loc + 1])
except IndexError:
buf.extend(self._read_block(1))
iac_cmd.append(buf[iac_loc + 1])
# Is this just a 2-byte TELNET command?
if iac_cmd[1] not in [WILL, WONT, DO, DONT]:
if iac_cmd[1] == AYT:
log.debug("Telnet server received Are-You-There (AYT)")
self._sock.send(b'\r\nYour Are-You-There received. I am here.\r\n')
elif iac_cmd[1] == IAC:
# It's data, not an IAC
iac_cmd.pop()
# This prevents the 0xff from being
# interrupted as yet another IAC
skip_to = iac_loc + 1
log.debug("Received IAC IAC")
elif iac_cmd[1] == NOP:
pass
else:
log.debug("Unhandled telnet command: "
"{0:#x} {1:#x}".format(*iac_cmd))
# This must be a 3-byte TELNET command
else:
try:
iac_cmd.append(buf[iac_loc + 2])
except IndexError:
buf.extend(self._read_block(1))
iac_cmd.append(buf[iac_loc + 2])
# We do ECHO, SGA, and BINARY. Period.
if iac_cmd[1] == DO and iac_cmd[2] not in [ECHO, SGA, BINARY]:
self._sock.send(bytes([IAC, WONT, iac_cmd[2]]))
log.debug("Telnet WON'T {:#x}".format(iac_cmd[2]))
else:
log.debug("Unhandled telnet command: "
"{0:#x} {1:#x} {2:#x}".format(*iac_cmd))
# Remove the entire TELNET command from the buffer
buf = buf.replace(iac_cmd, b'', 1)
# Return the new copy of the buffer, minus telnet commands
return buf
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO)
if sys.platform.startswith('win'):
import msvcrt
pipe_name = r'\\.\pipe\VBOX\Linux_Microcore_4.7.1'
pipe = open(pipe_name, 'a+b')
telnet_server = TelnetServer("VBOX", msvcrt.get_osfhandle(pipe.fileno()), "127.0.0.1", 3900)
else:
pipe_name = "/tmp/pipe_test"
try:
unix_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
unix_socket.connect(pipe_name)
except OSError as e:
print("Could not connect to UNIX socket {}: {}".format(pipe_name, e))
sys.exit(False)
telnet_server = TelnetServer("VBOX", unix_socket, "127.0.0.1", 3900)
telnet_server.setDaemon(True)
telnet_server.start()
try:
telnet_server.join()
except KeyboardInterrupt:
telnet_server.stop()
telnet_server.join(timeout=3)
@@ -0,0 +1,39 @@
# -*- 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/>.
"""
Custom exceptions for VirtualBox module.
"""
class VirtualBoxError(Exception):
def __init__(self, message, original_exception=None):
Exception.__init__(self, message)
if isinstance(message, Exception):
message = str(message)
self._message = message
self._original_exception = original_exception
def __repr__(self):
return self._message
def __str__(self):
return self._message
File diff suppressed because it is too large. Load diff
+652
View File
@@ -0,0 +1,652 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
VPCS server module.
"""
import os
import base64
import socket
import shutil
from gns3server.modules import IModule
from gns3server.config import Config
from .vpcs_device import VPCSDevice
from .vpcs_error import VPCSError
from .nios.nio_udp import NIO_UDP
from .nios.nio_tap import NIO_TAP
from ..attic import find_unused_port
from .schemas import VPCS_CREATE_SCHEMA
from .schemas import VPCS_DELETE_SCHEMA
from .schemas import VPCS_UPDATE_SCHEMA
from .schemas import VPCS_START_SCHEMA
from .schemas import VPCS_STOP_SCHEMA
from .schemas import VPCS_RELOAD_SCHEMA
from .schemas import VPCS_ALLOCATE_UDP_PORT_SCHEMA
from .schemas import VPCS_ADD_NIO_SCHEMA
from .schemas import VPCS_DELETE_NIO_SCHEMA
from .schemas import VPCS_EXPORT_CONFIG_SCHEMA
import logging
log = logging.getLogger(__name__)
class VPCS(IModule):
"""
VPCS module.
:param name: module name
:param args: arguments for the module
:param kwargs: named arguments for the module
"""
def __init__(self, name, *args, **kwargs):
# get the VPCS location
config = Config.instance()
vpcs_config = config.get_section_config(name.upper())
self._vpcs = vpcs_config.get("vpcs_path")
if not self._vpcs or not os.path.isfile(self._vpcs):
paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep)
# look for VPCS in the current working directory and $PATH
for path in paths:
try:
if "vpcs" in os.listdir(path) and os.access(os.path.join(path, "vpcs"), os.X_OK):
self._vpcs = os.path.join(path, "vpcs")
break
except OSError:
continue
if not self._vpcs:
log.warning("VPCS binary couldn't be found!")
elif not os.access(self._vpcs, os.X_OK):
log.warning("VPCS is not executable")
# a new process start when calling IModule
IModule.__init__(self, name, *args, **kwargs)
self._vpcs_instances = {}
self._console_start_port_range = vpcs_config.get("console_start_port_range", 4501)
self._console_end_port_range = vpcs_config.get("console_end_port_range", 5000)
self._allocated_udp_ports = []
self._udp_start_port_range = vpcs_config.get("udp_start_port_range", 20501)
self._udp_end_port_range = vpcs_config.get("udp_end_port_range", 21000)
self._host = vpcs_config.get("host", kwargs["host"])
self._console_host = vpcs_config.get("console_host", kwargs["console_host"])
self._projects_dir = kwargs["projects_dir"]
self._tempdir = kwargs["temp_dir"]
self._working_dir = self._projects_dir
def stop(self, signum=None):
"""
Properly stops the module.
:param signum: signal number (if called by the signal handler)
"""
# delete all VPCS instances
for vpcs_id in self._vpcs_instances:
vpcs_instance = self._vpcs_instances[vpcs_id]
vpcs_instance.delete()
IModule.stop(self, signum) # this will stop the I/O loop
def get_vpcs_instance(self, vpcs_id):
"""
Returns a VPCS device instance.
:param vpcs_id: VPCS device identifier
:returns: VPCSDevice instance
"""
if vpcs_id not in self._vpcs_instances:
log.debug("VPCS device ID {} doesn't exist".format(vpcs_id), exc_info=1)
self.send_custom_error("VPCS device ID {} doesn't exist".format(vpcs_id))
return None
return self._vpcs_instances[vpcs_id]
@IModule.route("vpcs.reset")
def reset(self, request):
"""
Resets the module.
:param request: JSON request
"""
# delete all vpcs instances
for vpcs_id in self._vpcs_instances:
vpcs_instance = self._vpcs_instances[vpcs_id]
vpcs_instance.delete()
# resets the instance IDs
VPCSDevice.reset()
self._vpcs_instances.clear()
self._allocated_udp_ports.clear()
self._working_dir = self._projects_dir
log.info("VPCS module has been reset")
@IModule.route("vpcs.settings")
def settings(self, request):
"""
Set or update settings.
Optional request parameters:
- path (path to vpcs)
- working_dir (path to a working directory)
- project_name
- console_start_port_range
- console_end_port_range
- udp_start_port_range
- udp_end_port_range
:param request: JSON request
"""
if request is None:
self.send_param_error()
return
if "path" in request and request["path"]:
self._vpcs = request["path"]
log.info("VPCS path set to {}".format(self._vpcs))
for vpcs_id in self._vpcs_instances:
vpcs_instance = self._vpcs_instances[vpcs_id]
vpcs_instance.path = self._vpcs
if "working_dir" in request:
new_working_dir = request["working_dir"]
log.info("this server is local with working directory path to {}".format(new_working_dir))
else:
new_working_dir = os.path.join(self._projects_dir, request["project_name"])
log.info("this server is remote with working directory path to {}".format(new_working_dir))
if self._projects_dir != self._working_dir != new_working_dir:
if not os.path.isdir(new_working_dir):
try:
shutil.move(self._working_dir, new_working_dir)
except OSError as e:
log.error("could not move working directory from {} to {}: {}".format(self._working_dir,
new_working_dir,
e))
return
# update the working directory if it has changed
if self._working_dir != new_working_dir:
self._working_dir = new_working_dir
for vpcs_id in self._vpcs_instances:
vpcs_instance = self._vpcs_instances[vpcs_id]
vpcs_instance.working_dir = os.path.join(self._working_dir, "vpcs", "pc-{}".format(vpcs_instance.id))
if "console_start_port_range" in request and "console_end_port_range" in request:
self._console_start_port_range = request["console_start_port_range"]
self._console_end_port_range = request["console_end_port_range"]
if "udp_start_port_range" in request and "udp_end_port_range" in request:
self._udp_start_port_range = request["udp_start_port_range"]
self._udp_end_port_range = request["udp_end_port_range"]
log.debug("received request {}".format(request))
@IModule.route("vpcs.create")
def vpcs_create(self, request):
"""
Creates a new VPCS instance.
Mandatory request parameters:
- name (VPCS name)
Optional request parameters:
- console (VPCS console port)
Response parameters:
- id (VPCS instance identifier)
- name (VPCS name)
- default settings
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VPCS_CREATE_SCHEMA):
return
name = request["name"]
console = request.get("console")
vpcs_id = request.get("vpcs_id")
try:
if not self._vpcs:
raise VPCSError("No path to a VPCS executable has been set")
vpcs_instance = VPCSDevice(name,
self._vpcs,
self._working_dir,
vpcs_id,
console,
self._console_host,
self._console_start_port_range,
self._console_end_port_range)
except VPCSError as e:
self.send_custom_error(str(e))
return
response = {"name": vpcs_instance.name,
"id": vpcs_instance.id}
defaults = vpcs_instance.defaults()
response.update(defaults)
self._vpcs_instances[vpcs_instance.id] = vpcs_instance
self.send_response(response)
@IModule.route("vpcs.delete")
def vpcs_delete(self, request):
"""
Deletes a VPCS instance.
Mandatory request parameters:
- id (VPCS instance identifier)
Response parameter:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VPCS_DELETE_SCHEMA):
return
# get the instance
vpcs_instance = self.get_vpcs_instance(request["id"])
if not vpcs_instance:
return
try:
vpcs_instance.clean_delete()
del self._vpcs_instances[request["id"]]
except VPCSError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("vpcs.update")
def vpcs_update(self, request):
"""
Updates a VPCS instance
Mandatory request parameters:
- id (VPCS instance identifier)
Optional request parameters:
- any setting to update
- script_file_base64 (base64 encoded)
Response parameters:
- updated settings
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VPCS_UPDATE_SCHEMA):
return
# get the instance
vpcs_instance = self.get_vpcs_instance(request["id"])
if not vpcs_instance:
return
config_path = os.path.join(vpcs_instance.working_dir, "startup.vpc")
try:
if "script_file_base64" in request:
# a new startup-config has been pushed
config = base64.decodebytes(request["script_file_base64"].encode("utf-8")).decode("utf-8")
config = config.replace("\r", "")
config = config.replace('%h', vpcs_instance.name)
try:
with open(config_path, "w") as f:
log.info("saving script file to {}".format(config_path))
f.write(config)
except OSError as e:
raise VPCSError("Could not save the configuration {}: {}".format(config_path, e))
# update the request with the new local startup-config path
request["script_file"] = os.path.basename(config_path)
elif "script_file" in request:
if os.path.isfile(request["script_file"]) and request["script_file"] != config_path:
# this is a local file set in the GUI
try:
with open(request["script_file"], "r", errors="replace") as f:
config = f.read()
with open(config_path, "w") as f:
config = config.replace("\r", "")
config = config.replace('%h', vpcs_instance.name)
f.write(config)
request["script_file"] = os.path.basename(config_path)
except OSError as e:
raise VPCSError("Could not save the configuration from {} to {}: {}".format(request["script_file"], config_path, e))
elif not os.path.isfile(config_path):
raise VPCSError("Startup-config {} could not be found on this server".format(request["script_file"]))
except VPCSError as e:
self.send_custom_error(str(e))
return
# update the VPCS settings
response = {}
for name, value in request.items():
if hasattr(vpcs_instance, name) and getattr(vpcs_instance, name) != value:
try:
setattr(vpcs_instance, name, value)
response[name] = value
except VPCSError as e:
self.send_custom_error(str(e))
return
self.send_response(response)
@IModule.route("vpcs.start")
def vpcs_start(self, request):
"""
Starts a VPCS instance.
Mandatory request parameters:
- id (VPCS instance identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VPCS_START_SCHEMA):
return
# get the instance
vpcs_instance = self.get_vpcs_instance(request["id"])
if not vpcs_instance:
return
try:
vpcs_instance.start()
except VPCSError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("vpcs.stop")
def vpcs_stop(self, request):
"""
Stops a VPCS instance.
Mandatory request parameters:
- id (VPCS instance identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VPCS_STOP_SCHEMA):
return
# get the instance
vpcs_instance = self.get_vpcs_instance(request["id"])
if not vpcs_instance:
return
try:
vpcs_instance.stop()
except VPCSError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("vpcs.reload")
def vpcs_reload(self, request):
"""
Reloads a VPCS instance.
Mandatory request parameters:
- id (VPCS identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VPCS_RELOAD_SCHEMA):
return
# get the instance
vpcs_instance = self.get_vpcs_instance(request["id"])
if not vpcs_instance:
return
try:
if vpcs_instance.is_running():
vpcs_instance.stop()
vpcs_instance.start()
except VPCSError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("vpcs.allocate_udp_port")
def allocate_udp_port(self, request):
"""
Allocates a UDP port in order to create an UDP NIO.
Mandatory request parameters:
- id (VPCS identifier)
- port_id (unique port identifier)
Response parameters:
- port_id (unique port identifier)
- lport (allocated local port)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VPCS_ALLOCATE_UDP_PORT_SCHEMA):
return
# get the instance
vpcs_instance = self.get_vpcs_instance(request["id"])
if not vpcs_instance:
return
try:
port = find_unused_port(self._udp_start_port_range,
self._udp_end_port_range,
host=self._host,
socket_type="UDP",
ignore_ports=self._allocated_udp_ports)
except Exception as e:
self.send_custom_error(str(e))
return
self._allocated_udp_ports.append(port)
log.info("{} [id={}] has allocated UDP port {} with host {}".format(vpcs_instance.name,
vpcs_instance.id,
port,
self._host))
response = {"lport": port,
"port_id": request["port_id"]}
self.send_response(response)
@IModule.route("vpcs.add_nio")
def add_nio(self, request):
"""
Adds an NIO (Network Input/Output) for a VPCS instance.
Mandatory request parameters:
- id (VPCS instance identifier)
- port (port number)
- port_id (unique port identifier)
- nio (one of the following)
- type "nio_udp"
- lport (local port)
- rhost (remote host)
- rport (remote port)
- type "nio_tap"
- tap_device (TAP device name e.g. tap0)
Response parameters:
- port_id (unique port identifier)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VPCS_ADD_NIO_SCHEMA):
return
# get the instance
vpcs_instance = self.get_vpcs_instance(request["id"])
if not vpcs_instance:
return
port = request["port"]
try:
nio = None
if request["nio"]["type"] == "nio_udp":
lport = request["nio"]["lport"]
rhost = request["nio"]["rhost"]
rport = request["nio"]["rport"]
try:
#TODO: handle IPv6
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.connect((rhost, rport))
except OSError as e:
raise VPCSError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e))
nio = NIO_UDP(lport, rhost, rport)
elif request["nio"]["type"] == "nio_tap":
tap_device = request["nio"]["tap_device"]
if not self.has_privileged_access(self._vpcs):
raise VPCSError("{} has no privileged access to {}.".format(self._vpcs, tap_device))
nio = NIO_TAP(tap_device)
if not nio:
raise VPCSError("Requested NIO does not exist or is not supported: {}".format(request["nio"]["type"]))
except VPCSError as e:
self.send_custom_error(str(e))
return
try:
vpcs_instance.port_add_nio_binding(port, nio)
except VPCSError as e:
self.send_custom_error(str(e))
return
self.send_response({"port_id": request["port_id"]})
@IModule.route("vpcs.delete_nio")
def delete_nio(self, request):
"""
Deletes an NIO (Network Input/Output).
Mandatory request parameters:
- id (VPCS instance identifier)
- port (port identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VPCS_DELETE_NIO_SCHEMA):
return
# get the instance
vpcs_instance = self.get_vpcs_instance(request["id"])
if not vpcs_instance:
return
port = request["port"]
try:
nio = vpcs_instance.port_remove_nio_binding(port)
if isinstance(nio, NIO_UDP) and nio.lport in self._allocated_udp_ports:
self._allocated_udp_ports.remove(nio.lport)
except VPCSError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("vpcs.export_config")
def export_config(self, request):
"""
Exports the script file from a VPCS instance.
Mandatory request parameters:
- id (vm identifier)
Response parameters:
- script_file_base64 (script file base64 encoded)
- False if no configuration can be exported
"""
# validate the request
if not self.validate_request(request, VPCS_EXPORT_CONFIG_SCHEMA):
return
# get the instance
vpcs_instance = self.get_vpcs_instance(request["id"])
if not vpcs_instance:
return
response = {}
script_file_path = os.path.join(vpcs_instance.working_dir, vpcs_instance.script_file)
try:
with open(script_file_path, "rb") as f:
config = f.read()
response["script_file_base64"] = base64.encodebytes(config).decode("utf-8")
except OSError as e:
self.send_custom_error("unable to export the script file: {}".format(e))
return
if not response:
self.send_response(False)
else:
self.send_response(response)
@IModule.route("vpcs.echo")
def echo(self, request):
"""
Echo end point for testing purposes.
:param request: JSON request
"""
if request is None:
self.send_param_error()
else:
log.debug("received request {}".format(request))
self.send_response(request)
Whitespace-only changes.
+104
View File
@@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
class Adapter(object):
"""
Base class for adapters.
:param interfaces: number of interfaces supported by this adapter.
"""
def __init__(self, interfaces=1):
self._interfaces = interfaces
self._ports = {}
for port_id in range(0, interfaces):
self._ports[port_id] = None
def removable(self):
"""
Returns True if the adapter can be removed from a slot
and False if not.
:returns: boolean
"""
return True
def port_exists(self, port_id):
"""
Checks if a port exists on this adapter.
:returns: True is the port exists,
False otherwise.
"""
if port_id in self._ports:
return True
return False
def add_nio(self, port_id, nio):
"""
Adds a NIO to a port on this adapter.
:param port_id: port ID (integer)
:param nio: NIO instance
"""
self._ports[port_id] = nio
def remove_nio(self, port_id):
"""
Removes a NIO from a port on this adapter.
:param port_id: port ID (integer)
"""
self._ports[port_id] = None
def get_nio(self, port_id):
"""
Returns the NIO assigned to a port.
:params port_id: port ID (integer)
:returns: NIO instance
"""
return self._ports[port_id]
@property
def ports(self):
"""
Returns port to NIO mapping
:returns: dictionary port -> NIO
"""
return self._ports
@property
def interfaces(self):
"""
Returns the number of interfaces supported by this adapter.
:returns: number of interfaces
"""
return self._interfaces
Loaded 100 of 124 files, more files were not shown because too many files have changed in this diff. Show more