Compare commits

...

256 Commits
v1.1 ... v1.2.2

Author SHA1 Message Date
Jeremy
9b7a2cda15 Bump version to 1.2.2 2015-01-15 17:44:10 -07:00
Jeremy
d0d60d26da Fixes problem when comparing directories on Windows. 2015-01-15 16:01:06 -07:00
Jeremy
b2c14c1218 Copy images to their default directory (if chosen by the user). No more symlinks. 2015-01-15 15:26:56 -07:00
Jeremy
2d59dc2c72 Fixes bug when IOS router slot1 doesn't exist. 2015-01-15 11:24:07 -07:00
Jeremy
bb48cdf0a0 Update the VM name in VirtualBox for linked clones. 2015-01-14 16:48:32 -07:00
Jeremy
e00dcbcd92 Do not make SuperPutty the default console on Windows. 2015-01-12 17:48:32 -07:00
Jeremy
5ab7d6e94e Fixes resource path when frozen. 2015-01-12 17:23:51 -07:00
Jeremy
d1da9adc88 Adds Julien in the About dialog. 2015-01-12 16:50:26 -07:00
Jeremy
1455babd62 Bump version to 1.2.2.dev2 2015-01-12 16:26:40 -07:00
Jeremy
fc8529323c Fixes bugs when changing the hostname for Dynamips switches and hub. 2015-01-12 15:07:48 -07:00
Jeremy
f9130794ee Fixes bug when changing the hostname of a cloud or host. 2015-01-12 14:50:57 -07:00
Jeremy
e62d6c0edd Adds change hostname action and VirtualBox name in tooltip. 2015-01-11 18:25:36 -07:00
Jeremy
8d1dc4b090 Fixes blocked console. 2015-01-11 14:40:46 -07:00
Jeremy
acf6cf6ea2 Fixes base config dir path. 2015-01-08 12:42:01 -07:00
Jeremy
d9ee44f90a Default jungle news image. 2015-01-07 17:52:11 -07:00
Jeremy
bd6da5db9a Updates default jungle news image. 2015-01-07 16:03:06 -07:00
Jeremy
4b3ade9b48 Fixes default news loading on Windows. 2015-01-06 19:18:33 -07:00
Jeremy
9daed7e0d4 Adds short port names to the topology summary. 2015-01-06 18:41:44 -07:00
Jeremy Grossmann
4ef61e7af1 Merge pull request #159 from shmygov/vboxuser
Run VirtualBox as another user
2015-01-05 16:19:10 -07:00
Jeremy
851f2d0517 Put base configs into a base_configs directory. 2015-01-05 15:32:01 -07:00
Jeremy
5e5e04de8e Fixes #167 2015-01-04 19:01:33 -07:00
Jeremy
e8b2f952af Support for IOURC file on the server side. 2015-01-04 15:59:00 -07:00
Jeremy
85b5d10e5a Bump the maximum network adapters to 32 for Qemu. 2015-01-03 16:16:07 -07:00
Jeremy
b916ca7bfb EtherSwitch routers can be added and configured like other IOS routers. 2014-12-31 12:29:38 -07:00
Jeremy
46190154f6 Fixes broken file browsers in QEMU wizard. 2014-12-30 17:20:18 -07:00
Jeremy Grossmann
194552d2d3 Merge pull request #172 from planctechnologies/gns-137a
Fix cloud project load
2014-12-28 13:58:27 -07:00
Jerry Seutter
a736cbc4d5 Fix cloud project load 2014-12-28 13:22:00 -07:00
Jeremy
0310ecdfd0 Import & export config options in contextual device menu. 2014-12-26 20:33:35 -07:00
Jeremy
28bbc8bbe9 Refactor how to deal with initial base configs for IOS, IOU and VPCS. 2014-12-24 18:02:00 -07:00
Jeremy
f36ef66623 Copy base config templates to the directory where GNS3 settings are stored. 2014-12-24 17:19:16 -07:00
Jeremy
16ba51aa8c 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
Jeremy
52847d1fad Auto start project support. 2014-12-24 14:13:59 -07:00
Jeremy
4733cc8a3e Fixes SuperPutty command line to connect to devices with spaces in their hostname. 2014-12-24 13:51:47 -07:00
Jeremy
aecf61135f Merge remote-tracking branch 'origin/master' 2014-12-24 13:12:19 -07:00
Jeremy
252d86eb70 Avoid uninitialized nodes to be saved in the project file. 2014-12-24 13:12:10 -07:00
grossmj
438b0fe9d3 Adds more network interface options to the Qemu VM configuration Ui as well as descriptions for all NICs. 2014-12-23 18:34:40 -07:00
grossmj
f6c58b5a28 Gives the possibility to apply or not the same text to all selected items when editing notes. 2014-12-23 15:46:13 -07:00
Jeremy Grossmann
dee2f94c38 Merge pull request #171 from shmygov/suspendqemu
Add QEMU monitor port to control running QEMU VMs.
2014-12-23 12:28:05 -07:00
grossmj
1fc4dec5ca Fixes bug when importing Host node with UDP NIOs. 2014-12-23 11:24:28 -07:00
Dmitry Shmygov
d7ab12bc61 Add QEMU monitor port to control running QEMU VMs 2014-12-23 15:11:21 +03:00
Jeremy
1f50612a16 VPCS multi-host support. 2014-12-22 19:08:56 -07:00
Jeremy Grossmann
43715f1e34 Merge pull request #170 from noplay/fix_travis
Fix travis build
2014-12-22 09:20:54 -07:00
Julien Duponchelle
1bf0ff69d8 Fix travis build
The travis build is now working. But tests are failling, i
think it's normal and now we need to fix them.

I commented a test in cloud inspector because the test
run forever.
2014-12-22 10:51:54 +01:00
Jeremy
abf992dfb8 Fixes problem with SuperPutty. Make it the default on Windows. 2014-12-20 17:21:33 -07:00
Jeremy
707dfee696 Prevents application to crash on Windows when importing GNS3 config file. 2014-12-20 16:47:02 -07:00
Jeremy
e8c4f059c0 Set 5 seconds timeout for local server connection. 2014-12-19 17:40:58 -07:00
Jeremy
1a6f80f2df Auxiliary console support for IOS routers. 2014-12-19 15:43:15 -07:00
Jeremy
47ae310ac7 Check if any device runs and warn the user before closing a project. 2014-12-17 17:53:27 -07:00
Jeremy
923c61f9c7 Restore the debug level status when starting. 2014-12-17 17:26:51 -07:00
Jeremy
18b8c558cd Merge remote-tracking branch 'origin/master' 2014-12-17 16:09:20 -07:00
Jeremy
84a091e380 Fixes resource access on Mac OS X. Adds a base configs dir in the GNS3 project directory and copy IOS base configs there. 2014-12-17 16:09:08 -07:00
grossmj
1399098e30 Automatically select the symbol and category corresponding the edited item in the symbol selection dialog. 2014-12-16 21:51:06 -07:00
Jeremy
593f8add5d Auto screenshot when saving a project. 2014-12-15 18:14:33 -07:00
Jeremy
2f26624f29 Sync MainWindow Ui. 2014-12-15 17:08:18 -07:00
Jeremy
14c901d219 Merge pull request: Migrate cloud project to local #169 2014-12-15 17:05:53 -07:00
Jeremy
2b2e45ca45 Merge pull request: Move local project to cloud #168 2014-12-15 17:00:23 -07:00
Jeremy
868e9a322e Scale SVG image to icon sizes. Fixes #124. 2014-12-15 16:53:13 -07:00
Jeremy Grossmann
d2f3f58de1 Merge pull request #166 from planctechnologies/gns-167
Fix ASA devices in qemu
2014-12-13 13:00:56 -07:00
Jeremy Grossmann
a69b3fcd11 Merge pull request #165 from planctechnologies/gns-165
Run VPCS devices on cloud servers
2014-12-13 13:00:29 -07:00
Jerry Seutter
a3505ee7f2 Spelling fix 2014-12-12 16:44:06 -07:00
Jerry Seutter
efe5e0e2c4 Update vpcs download location, fix error message 2014-12-12 16:42:47 -07:00
Jeremy
8745527c5d Catch OSError exception for subprocess calls. 2014-12-11 12:15:24 -07:00
grossmj
78b40df71e Have the classic icons a little less dark. 2014-12-10 15:20:03 -07:00
Jerry Seutter
9675619ab9 Run ASA devices 2014-12-09 13:38:21 -07:00
Jeremy Grossmann
68ca2c2be6 Merge pull request #162 from dlintott/fix_161
Fix logic when checking for transparency or border_style.
2014-12-09 12:35:29 -07:00
Daniel Lintott
835ecbb410 Fix logic when checking for transparency or border_style.
A value of 0 is valid for both transparency and border_style. Python evaluates 0 to be False.

Fixes #161
2014-12-09 15:22:20 +00:00
Jeremy Grossmann
22756a3c13 Merge pull request #160 from planctechnologies/dev
Warn when deleting cloud instance with running devices
2014-12-08 18:13:11 -07:00
Jerry Seutter
3c2fb04ed8 Added VPCS devices to cloud projects. 2014-12-08 17:20:54 -07:00
Jeremy
db5fb840a6 Darken classic style icons. 2014-12-08 14:34:15 -07:00
Jerry Seutter
65b05d707b Merge branch 'master' into dev 2014-12-08 14:26:09 -07:00
Dmitry Shmygov
1b972df7e2 Run VirtualBox as another user 2014-12-06 01:44:48 +03:00
Jeremy
40266c275d Bump version to 1.2.2.dev1 2014-12-05 13:53:31 -07:00
Jeremy
197db35c80 Disable cloud. 2014-12-04 12:50:50 -07:00
Jeremy
8771e1ace7 Bump version to 1.2.1 2014-12-04 12:49:40 -07:00
Jeremy
e6e1275ad2 Fixes missing options. 2014-12-04 12:35:36 -07:00
Jeremy
417dc0859b 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
806e1f29bb Bump version to 1.2.1.dev2 2014-12-02 18:52:28 -07:00
Jeremy
9724f5769d Support for CPU throttling and process priority for Qemu. 2014-12-02 18:12:37 -07:00
Jeremy
5a0c8914c4 Rename IOSv symbols + improve support for ASA on Windows. 2014-12-02 14:49:04 -07:00
Jeremy
c2e0a30da8 Merge pull request #157. 2014-12-02 12:37:21 -07:00
Jeremy
45ae4f20a0 Merge remote-tracking branch 'origin/master' 2014-12-02 12:34:06 -07:00
Jeremy
398826d7ef IOSv and IOSv-L2 integration. 2014-12-02 12:33:49 -07:00
grossmj
14e5f3bf74 Merge remote-tracking branch 'origin/master' 2014-12-02 11:51:32 -07:00
grossmj
57b1cbf41b Fixes SecureCRT command line. 2014-12-02 11:51:17 -07:00
Julien Duponchelle
51a0d88b9b Revert "Add start instructions for MacOSX"
This reverts commit 16d4d7d1ea.
2014-12-01 22:57:17 +01:00
Julien Duponchelle
16d4d7d1ea Add start instructions for MacOSX 2014-12-01 21:26:16 +01:00
Jerry Seutter
2affb1513d Merge branch 'dev' of github.com:planctechnologies/gns3-gui into dev 2014-12-01 09:42:21 -07:00
Jerry Seutter
214d4b2a9e Grammar fixes 2014-12-01 09:40:02 -07:00
jseutter
2f486d979b Merge pull request #48 from planctechnologies/gns-133
Gns 133 Deleting an instance orphans devices on canvas
2014-12-01 09:37:00 -07:00
grossmj
023c5fb99a Install scripts for Mac OS X. 2014-11-30 19:37:15 -07:00
grossmj
329ed371f9 Adds default path for VBoxManage on Mac OS X. 2014-11-29 16:42:57 -07:00
grossmj
0577bfbde3 Support for older Qemu versions like the 0.11.0 on Windows. 2014-11-29 14:11:51 -07:00
grossmj
3ce5c35143 Use PROGRAMFILES and PROGRAMFILES(X86) environment variables for console command lines on Windows. 2014-11-28 17:57:54 -07:00
Massimiliano Pippi
9258ef4bb3 do nothing if server is None 2014-11-28 17:08:39 +01:00
Massimiliano Pippi
e02facc170 remove devices when removing instances 2014-11-28 16:58:33 +01:00
Massimiliano Pippi
4dc76926ea remove nodes when removing instances 2014-11-28 16:56:05 +01:00
Massimiliano Pippi
ba6be7e987 utility methods for shutting down cloud servers 2014-11-28 16:55:36 +01:00
grossmj
6b2da1ec97 Fixes port sorting issues. 2014-11-27 11:34:01 -07:00
Jeremy Grossmann
6aa1b515c7 Merge pull request #154 from dlintott/master
Validate the Idle-PC value and display an error if invalid
2014-11-25 13:55:16 -07:00
Daniel Lintott
5d64ec1c8f Validate the Idle-PC value and display an error if invalid 2014-11-25 20:34:43 +00:00
Jeremy
e1b81fb931 Fixes PyQt module usage. 2014-11-25 11:54:35 -07:00
Jeremy Grossmann
c91752598c Merge pull request #153 from planctechnologies/dev
Remove image access server references
2014-11-25 11:51:07 -07:00
grossmj
708515925f Do not put space in VirtualBox linked VM names. 2014-11-24 20:11:34 -07:00
Jeremy
a61745f868 Fixes C7200 IO cards insert/remove issues and makes C7200-IO-FE the default. 2014-11-24 17:02:00 -07:00
Jeremy
2ca7af34df Load project from command line. Fixes #151. 2014-11-24 14:21:51 -07:00
jseutter
bd7e6f4e3e Merge pull request #47 from planctechnologies/gns-136
Gns 136 gui won't exit when telnet session open to device
2014-11-24 10:49:51 -07:00
jseutter
32e86d461e Merge pull request #46 from planctechnologies/gns-132
Gns 132 Remove IAS (Image Access Server) code
2014-11-24 10:23:37 -07:00
Massimiliano Pippi
66ea5ad979 close cloud server connections when gns3 exits 2014-11-24 12:47:56 +01:00
Massimiliano Pippi
ebfa80d444 return the list of cloud servers as a property 2014-11-24 12:47:06 +01:00
Massimiliano Pippi
4247cc819d removed references to the IAS server 2014-11-24 12:18:42 +01:00
Massimiliano Pippi
1f9f5bd734 hardcode Ubuntu image in cloud preferences 2014-11-24 12:11:36 +01:00
Jeremy
426d791eea Fixes #48, #152. 2014-11-23 19:26:37 -07:00
grossmj
30d99b812c Merge remote-tracking branch 'origin/master' 2014-11-23 19:04:35 -07:00
grossmj
981e55bc4d Use the Qt binding abstraction layer. 2014-11-23 19:04:11 -07:00
Jeremy
bab1090a25 Use SubprocessError to catch Subprocess exceptions. 2014-11-22 17:45:04 -07:00
Jeremy
84b5181fd8 Support for full screen mode (View -> Fullscreen). 2014-11-21 16:52:06 -07:00
Jeremy
222ebb58c0 Bump to version 1.2.1.dev1 and fixes vboxmanage lookup on Windows. 2014-11-20 19:01:00 -07:00
Jeremy
319ac95f8a Bump to version 1.2 2014-11-19 19:28:21 -07:00
Jeremy
0370d3084f Change how the local server is shutdown on Linux/OSX.
Some more style tweaks.
2014-11-19 19:11:51 -07:00
Jeremy
30cd030943 Charcoal style tweak for Mac OS X. 2014-11-19 18:08:51 -07:00
Jeremy
ee98a78264 Style improvements. 2014-11-19 17:34:02 -07:00
Jeremy
2abf316192 Classic style. 2014-11-19 14:55:28 -07:00
Jeremy Grossmann
25d1abcd6c Merge pull request #147 from vikram186/ui_fix
module/ios_preferences: Fix button state
2014-11-19 14:05:04 -07:00
Vikram Narayanan
3b823d9696 module/ios_preferences: Fix button state
Make the setEnabled to False when the list is empty

Signed-off-by: Vikram Narayanan <vikram186@gmail.com>
2014-11-19 21:03:24 +01:00
grossmj
7e13c2c67a Restore dock widgets. 2014-11-19 10:22:09 -07:00
Jerry Seutter
f5ad3a6a2e Merge branch 'master' into dev 2014-11-18 16:21:39 -07:00
Jerry Seutter
422af60827 Enabling cloud functionality 2014-11-18 16:21:17 -07:00
Jerry Seutter
9127321540 Merge branch 'master' into dev 2014-11-18 16:18:16 -07:00
Jeremy
ad57e3e9b6 Improvement for the charcoal style & style options move to general preferences. 2014-11-17 19:29:22 -07:00
Jeremy
ceacb5aceb Adds back missing start, stop and pause icons for the contextual menu. 2014-11-17 12:29:42 -07:00
jseutter
dcae059480 Merge pull request #44 from planctechnologies/gns-130
Gns 130: QThreads missing a parent QObject
2014-11-17 09:24:53 -07:00
jseutter
6d6603e013 Merge pull request #45 from planctechnologies/gns-131
Fix bug where new instances would not build.
2014-11-17 09:24:35 -07:00
Massimiliano Pippi
90fe8078c3 fixed transitions for custom instance states, fixes gns-131 2014-11-17 16:47:56 +01:00
Massimiliano Pippi
367fb32114 instance QThreads passing a parent 2014-11-17 14:24:03 +01:00
Massimiliano Pippi
b2b82afd71 fixed super() call, propagate parent 2014-11-17 14:23:52 +01:00
Jeremy
dc9f012d25 Bump to version 1.2.dev3 2014-11-15 16:47:30 -07:00
Jeremy
a21a397397 Linked clone support for VirtualBox (still problems with temporary projects). 2014-11-15 16:05:55 -07:00
Jeremy
fa86a04acc Fixes "don't show this again" for the getting started dialog. 2014-11-15 12:42:21 -07:00
Jeremy
4cf3f595d6 Fixes remote server issue when creating a new project while already in a project. 2014-11-14 19:59:06 -07:00
Jeremy
e619e56537 Shows cancel button in Wizards on OSX. Fixes crash on Windows 32-bit. 2014-11-14 10:18:36 -07:00
Jeremy Grossmann
0c9c81e9dc Merge pull request #142 from planctechnologies/pr3
Add support for Qemu devices on cloud instances (gui)
2014-11-12 21:20:51 -07:00
Jeremy Grossmann
3b5184b007 Merge pull request #141 from planctechnologies/pr2
Support IOU devices on cloud instances
2014-11-12 21:20:24 -07:00
Jeremy
0c6bfdafc3 Prepare new dark style. 2014-11-12 19:47:49 -07:00
Jerry Seutter
f64838ac59 Fix pull branch 2014-11-12 14:18:16 -07:00
Nasrullah Taha
53cfec5ce0 Merge pull request #43 from planctechnologies/gns-129
Move image path manipulation to server side
2014-11-12 11:56:42 -07:00
Nasrullah Taha
c9dfd99697 Merge branch 'dev' of github.com:planctechnologies/gns3-gui into dev 2014-11-12 11:41:30 -07:00
Jerry Seutter
3b29e898e4 Merge branch 'dev' of github.com:planctechnologies/gns3-gui into dev 2014-11-12 11:07:24 -07:00
Jerry Seutter
e89ad3ec4f Handle pre-existing ssh keys when creating an instance 2014-11-12 11:07:07 -07:00
Nasrullah Taha
c2a193cb8a Merge branch 'gns-125' into dev 2014-11-12 11:00:15 -07:00
Nasrullah Taha
26b668099c Merge pull request #38 from planctechnologies/gns-123
Add support for running IOU devices on cloud instances
2014-11-12 10:53:31 -07:00
Jeremy
f67f0f3768 Option to test IOS image from the wizard. 2014-11-10 18:55:41 -07:00
Jeremy
860873ac21 Adds gns3-converter as a dependency and finish integration. 2014-11-10 14:58:28 -07:00
Jeremy Grossmann
0472e4fddd Merge pull request #140 from planctechnologies/pr1
Instance inspector improvements
2014-11-10 11:56:28 -07:00
Jerry Seutter
4aa8f6f49a Do path manipulation server side, and remove UploadFileThread class 2014-11-10 11:30:11 -07:00
jseutter
4de3c78cfe Merge pull request #40 from planctechnologies/gns-106
Gns 106 list_instance failure causes missing server instances
2014-11-10 11:01:59 -07:00
jseutter
7df9316539 Merge pull request #42 from planctechnologies/gns-37a
Highlight devices running on instance
2014-11-10 11:00:15 -07:00
jseutter
c2b7a8e86f Merge pull request #41 from planctechnologies/gns-37
Gns 34 -  display the number of devices running on the instance
2014-11-10 09:21:02 -07:00
Massimiliano Pippi
144576ee86 emit signal when table is clicked, not when selection changes 2014-11-10 17:19:38 +01:00
Massimiliano Pippi
cabd57e5ab select nodes running on the selected cloud instance 2014-11-10 15:50:38 +01:00
Massimiliano Pippi
06c2d8d3e6 signal when an instance was selected/deselected 2014-11-10 15:50:38 +01:00
Massimiliano Pippi
f0845e2fff show number of devices running on each cloud instance, fixes gns-34 2014-11-10 11:45:35 +01:00
Massimiliano Pippi
e48d9d76b4 pass the instance id all the way down to the cloud server 2014-11-10 11:44:43 +01:00
Massimiliano Pippi
b250af6f26 populate model lazily, working even when first call to list_instances fails 2014-11-10 10:42:56 +01:00
Massimiliano Pippi
a7688cec8b return None instead of raising KeyError 2014-11-10 10:31:10 +01:00
Jeremy
ec07873990 Option to allow console connections to any local IP address when using the local server. 2014-11-09 23:01:13 -07:00
Jeremy
75cb485e6f Allows Qemu VM to have 0 interface. 2014-11-09 18:27:40 -07:00
Jeremy
4a7cc14270 Adds "open a project" and "recent projects" buttons to the new project dialog.
Option to deactivate the new project dialog at startup.
2014-11-09 18:27:16 -07:00
Jeremy
eca99ccd2c New host node (cloud with all Ethernet & TAP interfaces added). 2014-11-09 16:09:42 -07:00
Jeremy Grossmann
dd03139706 Merge pull request #138 from planctechnologies/dev
Fix cloud instance ready indicator
2014-11-09 11:51:49 -07:00
Jeremy
8ad391d2ab Base for VirtualBox linked clones (not completed yet). 2014-11-09 11:50:47 -07:00
Jerry Seutter
f7e53f685b Add support for running Qemu devices on a cloud instance 2014-11-08 09:02:51 -07:00
Jerry Seutter
0698b3942b Merge branch 'dev' into gns-125 2014-11-06 18:06:59 -07:00
Jerry Seutter
d866cedbca Merge branch 'master' into dev 2014-11-06 18:06:49 -07:00
Jerry Seutter
6105a1629f Merge branch 'dev' into gns-123 2014-11-06 16:28:12 -07:00
jseutter
8a49d90400 Merge pull request #37 from planctechnologies/gns-126
On cloud instances, only show the green icon when the build process is complete
2014-11-06 16:25:26 -07:00
Jerry Seutter
10dfd59203 Add support for running IOU devices on cloud instances 2014-11-06 16:07:40 -07:00
grossmj
afa91ef2f3 Merge remote-tracking branch 'origin/master' 2014-11-06 13:59:05 -07:00
grossmj
862d97b0a4 Rename "enable console" to "remote console". 2014-11-06 13:56:19 -07:00
Massimiliano Pippi
05a437e2fc fixed instance state handling 2014-11-06 09:33:13 +01:00
Massimiliano Pippi
6b679fc89e added a method to retrieve an instance by id 2014-11-06 09:33:13 +01:00
Jerry Seutter
9f3d831d4c Fixed bug when opening existing cloud projects 2014-11-05 10:07:13 -07:00
Jeremy Grossmann
1403c35443 Merge pull request #137 from planctechnologies/dev
Better tracking of live cloud instances
2014-11-05 09:25:12 -07:00
Jerry Seutter
d440d10371 Remove commented out code 2014-11-05 09:21:28 -07:00
jseutter
58a218f826 Merge pull request #36 from planctechnologies/gns-127
Better tracking of live cloud instances
2014-11-05 09:20:01 -07:00
Jerry Seutter
0756f9104e Call super in __init__ to keep Qt object tree intact 2014-11-05 09:19:45 -07:00
Jeremy
2847296cd4 Settings management refactoring and checks for name collisions. 2014-11-04 18:10:58 -07:00
Jeremy
61317fced0 New news dock widget. 2014-11-04 14:17:33 -07:00
Jeremy Grossmann
767d756ed4 Merge pull request #136 from planctechnologies/dev
Add a ENABLE_CLOUD global, add telnet over ssh support
2014-11-04 10:37:11 -07:00
grossmj
43f1c04d3e Merge remote-tracking branch 'origin/master' 2014-11-03 22:24:47 -07:00
grossmj
c985298c9a Fixes bug when editing a Qemu VM configured to run on a remote server. 2014-11-03 22:24:29 -07:00
Jerry Seutter
8e9574871b Better tracking of live cloud instances 2014-11-03 19:51:13 -07:00
jseutter
3536c0fc19 Merge pull request #35 from planctechnologies/gns-109
Gns 109 / Gns 95: Set up ssh tunnel for telnet session
2014-11-03 19:40:14 -07:00
Jeremy Grossmann
d196db8303 Merge pull request #135 from planctechnologies/cloud-run-merge
Run IOS devices on a cloud instance
2014-11-03 19:18:52 -07:00
Jeremy
18a4eeb6e9 Integrate GNS3 converter. 2014-11-03 17:35:23 -07:00
Jerry Seutter
cb29466e43 Add a ENABLE_CLOUD global 2014-11-03 16:40:42 -07:00
Jerry Seutter
58ad493e9e Merge branch 'master' into dev 2014-11-03 15:40:17 -07:00
grossmj
36b8092e53 Check for duplicate node names in Preferences. 2014-11-03 15:06:08 -07:00
Jerry Seutter
855b9901c7 Remove old comment 2014-11-03 11:45:13 -07:00
grossmj
f0f110b277 Show the Wireshark path when failing to open it. 2014-11-02 10:10:13 -07:00
grossmj
f0e51c59ff Make sure local paths are not sent to remote servers (i.e. Dynamips path). 2014-11-01 16:51:31 -06:00
Jeremy Grossmann
37b1c839e9 Merge pull request #134 from planctechnologies/cloud-export
Project import/export/delete in cloud files
2014-11-01 11:20:06 -06:00
Jerry Seutter
c4226315a5 Merge branch 'master' into cloud-export 2014-10-31 14:08:58 -06:00
Massimiliano Pippi
9fbf593db8 create a new endpoint on the tunnel when connecting via telnet to cloud devices 2014-10-31 17:10:55 +01:00
Massimiliano Pippi
79feec83fb return Endpoint instance instead of Endpoint ID when creating a tunnel 2014-10-31 17:09:34 +01:00
Jerry Seutter
b3b934847d Merge branch 'master' into dev 2014-10-31 10:03:04 -06:00
Massimiliano Pippi
9d71161a32 property to retrieve the tunnel 2014-10-31 16:38:52 +01:00
Massimiliano Pippi
7485fb968a disconnect tunnel when wss is closed 2014-10-31 16:38:39 +01:00
Massimiliano Pippi
4a39aad83d pass the ssh pkey all the way down to the wss and setup a tunnel upon wss connection 2014-10-31 16:35:19 +01:00
Massimiliano Pippi
56faee748e using relative import 2014-10-31 15:09:21 +01:00
Massimiliano Pippi
6577340e05 determine if node is on the cloud 2014-10-31 14:48:50 +01:00
Massimiliano Pippi
d5eef5a5f6 make iTerm work in a separate thread 2014-10-31 13:02:34 +01:00
Massimiliano Pippi
3347180a5a support Terminal sessions in a separate thread 2014-10-31 12:56:36 +01:00
Massimiliano Pippi
c4396fcac0 pass a callback to the telnet command 2014-10-31 12:56:04 +01:00
Massimiliano Pippi
418b912379 run telnet console in a separate thread 2014-10-31 12:39:16 +01:00
grossmj
7b6e32373a Fixes issue when getting the VirtualBox VM list. 2014-10-30 21:10:14 -06:00
Jeremy
c54de58a0d New VirtualBox support (under testing). 2014-10-30 18:53:17 -06:00
Jerry Seutter
7c21c7b29f Merge branch 'dev' of github.com:planctechnologies/gns3-gui into dev 2014-10-30 17:17:31 -06:00
Jerry Seutter
57ee86d251 Fix bug where images not found
Improve ssh_to_server script.
2014-10-30 17:15:59 -06:00
Nasrullah Taha
7c94595313 Merge pull request #34 from planctechnologies/gns-121
Start ios devices on a cloud instance
2014-10-30 13:16:10 -06:00
Jerry Seutter
2bbfefb5e1 Fix merge-induced error 2014-10-30 12:23:31 -06:00
Nasrullah Taha
e9850ee962 -adding root logger handler when no default exists
-fixes to ssh_to_server script for linux
-fixed callback for upload image thread
2014-10-30 12:10:14 -06:00
Jerry Seutter
4828b3a73b Fix bug where image wasn't being downloaded from cloud files 2014-10-29 15:18:23 -06:00
Jerry Seutter
de5e43578a Fix upload path 2014-10-29 10:36:44 -06:00
Jerry Seutter
15380ae80a Code cleanup 2014-10-28 12:11:22 -06:00
Jerry Seutter
e39c1f9579 Merge branch 'gns-110' into gns-121 2014-10-28 11:33:50 -06:00
Jerry Seutter
c0a7ed7a2c Merge branch 'dev' into gns-110 2014-10-28 11:00:14 -06:00
Jerry Seutter
e2be6bd0a9 Merge branch 'master' into dev 2014-10-28 09:32:38 -06:00
Jerry Seutter
6513acf141 Merge branch 'dev' into gns-118 2014-10-28 09:06:27 -06:00
grossmj
1c4ce90093 qemu-system-i386 is the new default on 32-bit platforms. 2014-10-27 21:58:32 -06:00
grossmj
adace0d6a1 Fixes platform detection issue with Cisco IOS image file name. 2014-10-27 19:58:07 -06:00
Jerry Seutter
a960ee05ce Fix missing dynamips symlink 2014-10-27 18:33:03 -06:00
Jerry Seutter
0b46653c79 Pull the server code from github. 2014-10-27 18:23:10 -06:00
Jerry Seutter
120cb89526 IOS devices can be deployed on cloud instances. 2014-10-27 18:11:39 -06:00
Jeremy
72c48968df Update README. 2014-10-27 15:58:13 -06:00
grossmj
2195a6199c Delay (default 500 ms) when Telneting to all nodes. 2014-10-26 19:28:08 -06:00
grossmj
509d5f8b82 Bump version to 1.2.dev1 2014-10-25 18:01:14 -06:00
jseutter
35fe0514ba Merge pull request #33 from planctechnologies/gns-60
GNS-60 Delete cloud project
2014-10-24 12:15:47 -06:00
Nasrullah Taha
98fd7b73b2 added delete cloud project option 2014-10-23 23:43:31 -06:00
Nasrullah Taha
00dfddde8a fixed tests broken by 9d0bd31 2014-10-22 15:37:07 -06:00
Nasrullah Taha
9d0bd31eea added project import dialog 2014-10-22 15:03:13 -06:00
Massimiliano Pippi
b4e42f50ec redirect std error for gns3-server 2014-10-22 18:35:51 +02:00
Nasrullah Taha
c8ef818e1c merged dev into gns-59 2014-10-21 16:52:34 -06:00
Nasrullah Taha
d59a2bd7f9 added menu action and empty slot for importing project 2014-10-21 16:43:40 -06:00
Nasrullah Taha
e6cb49a19b added signals to update ui on progress 2014-10-21 16:43:03 -06:00
Jerry Seutter
fa8c166c18 Merge branch 'gns-121' into gns-110 2014-10-21 15:33:37 -06:00
Jerry Seutter
5ad6433e91 Handle opening existing projects 2014-10-21 15:17:49 -06:00
Jerry Seutter
ff4af1e22d Reuse cloud instances that are running when the gui is restarted 2014-10-21 14:39:08 -06:00
Jerry Seutter
8a02288e28 Merge branch 'dev' into gns-121 2014-10-21 10:38:27 -06:00
Jerry Seutter
bf1a797519 Build instance dynamically from default ubuntu image (gns-121) 2014-10-20 17:07:15 -06:00
Nasrullah Taha
fddbe69ba0 added downloading image files used by the imported project 2014-10-17 10:38:38 -06:00
Nasrullah Taha
1a154df621 changed download file method to skip downloading files if the same file already exists at the same local path 2014-10-16 13:15:08 -06:00
Jerry Seutter
e527b13930 Merge remote-tracking branch 'origin/gns-59' into gns-110 2014-10-15 15:34:32 -06:00
Jerry Seutter
aa3ec13e45 Move get_provider() out of utils.py 2014-10-15 15:33:51 -06:00
Nasrullah Taha
b7d7706682 added download file method to BaseCloudCtrl 2014-10-15 12:55:12 -06:00
Nasrullah Taha
20bb456fa8 added method to BaseCloudCtrl that lists project zip files in projects folder in cloud storage 2014-10-14 16:13:57 -06:00
Jerry Seutter
865ae8445d Merge branch 'dev' into gns-118 2014-10-10 09:54:53 -06:00
318 changed files with 45917 additions and 32473 deletions

View File

@@ -4,15 +4,32 @@ python:
- "3.3"
- "3.4"
install:
- "pip install -r requirements.txt --use-mirrors"
- "pip install tox"
cache:
directories:
- build
script: "python setup.py test"
branches:
only:
- master
before_install:
- mkdir -p build
- sudo apt-get install -qqm wget build-essential python3-dev
- sudo apt-get install -qqm python3-pyqt4 python3-sip-dev
# install SIP
- cd build
- wget --quiet --output-document=sip.tar.gz http://downloads.sourceforge.net/project/pyqt/sip/sip-4.15.3/sip-4.15.3.tar.gz
- tar -xf sip.tar.gz
- cd sip-4.15.3
- python -B configure.py
- make
- sudo make install
# install PyQt
- cd $TRAVIS_BUILD_DIR/build
- wget --quiet --output-document=pyqt.tar.gz http://downloads.sourceforge.net/project/pyqt/PyQt4/PyQt-4.10.3/PyQt-x11-gpl-4.10.3.tar.gz
- tar -xf pyqt.tar.gz
- cd PyQt-x11-gpl-4.10.3
- python -B configure.py --confirm-license
- make
- sudo make install
- python -c 'import PyQt4' # Check if it's ok
- cd $TRAVIS_BUILD_DIR
notifications:
email: false
@@ -22,3 +39,9 @@ notifications:
on_success: change
on_failure: always
install:
- "pip install -r dev-requirements.txt"
script: "xvfb-run py.test --verbose" # Run tests in a fake X server

View File

@@ -1,20 +1,31 @@
GNS3-gui
========
New GNS3 GUI repository (beta stage).
GNS3 GUI repository (beta stage).
Warning: this is not the repository for the stable version of GNS3 (0.8.6), please go to the gns3-legacy repository for it.
Linux (Debian based)
--------------------
Linux/Unix
----------
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)
- PyQt must be installed, to install on Debian-like Linux: sudo apt-get install python3-pyqt4
- Dynamips version 0.2.11 or above (http://github.com/GNS3/dynamips)
- Python 3.3 or above
- Setuptools
- PyQt libraries
- Apache Libcloud library
- Requests library
- Paramiko library
The following commands will install some of these dependencies:
.. code:: bash
sudo apt-get install python3-setuptools
sudo apt-get install python3-pyqt4
Finally these commands will install the GUI as well as the rest of the dependencies:
.. code:: bash
@@ -34,7 +45,7 @@ Please use our DMG package or you can manually install using the following steps
`First install homebrew <http://brew.sh/>`_.
Then install GNS3 dependencies.
Then install the GNS3 dependencies.
.. code:: bash

View File

@@ -26,10 +26,10 @@ from collections import namedtuple
import hashlib
import os
import logging
from io import StringIO
from io import StringIO, BytesIO
from libcloud.compute.base import NodeAuthSSHKey
from libcloud.storage.types import ContainerAlreadyExistsError
from libcloud.storage.types import ContainerAlreadyExistsError, ContainerDoesNotExistError, ObjectDoesNotExistError
from .exceptions import ItemNotFound, KeyPairExists, MethodNotAllowed
from .exceptions import OverLimit, BadRequest, ServiceUnavailable
@@ -141,7 +141,7 @@ class BaseCloudCtrl(object):
self._handle_exception(status, error_text)
else:
log.error("create_instance method raised an exception: {}".format(e))
log.error('image id {}'.format(image))
log.error('image id {}'.format(image_id))
def delete_instance(self, instance):
""" Delete the specified instance. Returns True or False. """
@@ -170,10 +170,7 @@ class BaseCloudCtrl(object):
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))
return self.driver.list_nodes()
def create_key_pair(self, name):
@@ -216,11 +213,11 @@ class BaseCloudCtrl(object):
return self.driver.list_key_pairs()
def upload_file(self, file_path, folder):
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 folder: folder in cloud storage to save file in
: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:
@@ -231,7 +228,6 @@ class BaseCloudCtrl(object):
with open(file_path, 'rb') as file:
local_file_hash = hashlib.md5(file.read()).hexdigest()
cloud_object_name = folder + '/' + os.path.basename(file_path)
cloud_hash_name = cloud_object_name + '.md5'
cloud_objects = [obj.name for obj in gns3_container.list_objects()]
@@ -250,3 +246,93 @@ class BaseCloudCtrl(object):
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

View File

@@ -17,7 +17,7 @@
""" Interacts with Rackspace API to create and manage cloud instances. """
from gns3.cloud.base_cloud_ctrl import BaseCloudCtrl
from .base_cloud_ctrl import BaseCloudCtrl
import json
import requests
from libcloud.compute.drivers.rackspace import ENDPOINT_ARGS_MAP
@@ -42,11 +42,9 @@ class RackspaceCtrl(BaseCloudCtrl):
""" Controller class for interacting with Rackspace API. """
def __init__(self, username, api_key, gns3_ias_url):
def __init__(self, username, api_key, *args, **kwargs):
super(RackspaceCtrl, self).__init__(username, api_key)
self.gns3_ias_url = gns3_ias_url
# 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)
@@ -225,54 +223,37 @@ class RackspaceCtrl(BaseCloudCtrl):
self.region = region
return True
def _get_shared_images(self, username, region, gns3_version):
"""
Given a GNS3 version, ask gns3-ias to share compatible images
Response:
[{"created_at": "", "schema": "", "status": "", "member_id": "", "image_id": "", "updated_at": ""},]
or, if access was already asked
[{"image_id": "", "member_id": "", "status": "ALREADYREQUESTED"},]
"""
endpoint = self.gns3_ias_url+"/images/grant_access"
params = {
"user_id": username,
"user_region": region.upper(),
"gns3_version": gns3_version,
}
try:
response = requests.get(endpoint, params=params)
except requests.ConnectionError:
raise ApiError("Unable to connect to IAS")
status = response.status_code
if status == 200:
return response.json()
elif status == 404:
raise ItemNotFound()
else:
raise ApiError("IAS status code: %d" % status)
def list_images(self):
"""
Return a dictionary containing RackSpace server images
retrieved from gns3-ias server
"""
if not (self.tenant_id and self.region):
return {}
try:
shared_images = self._get_shared_images(self.tenant_id, self.region, __version__)
images = {}
for i in shared_images:
images[i['image_id']] = i['image_name']
return images
except ItemNotFound:
return {}
except ApiError as e:
log.error('Error while retrieving image list: %s' % e)
return {}
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

View File

@@ -1,16 +1,17 @@
from contextlib import contextmanager
import io
import json
from socket import error as socket_error
import logging
import os
import zipfile
import tempfile
import time
import zipfile
from PyQt4 import QtCore
from PyQt4.QtCore import QThread
from PyQt4.QtCore import pyqtSignal
from ..qt import QtCore
from .rackspace_ctrl import RackspaceCtrl
from .exceptions import KeyPairExists
from .rackspace_ctrl import get_provider
from ..topology import Topology
from ..servers import Servers
@@ -45,94 +46,73 @@ def ssh_client(host, key_string):
client.connect(hostname=host, username="root", pkey=key)
yield client
except socket_error as e:
log.error("SSH connection error to {}: {}".format(host, e))
log.debug("SSH connection socket error to {}: {}".format(host, e))
yield None
except Exception as e:
log.debug("SSH connection error to {}: {}".format(host, e))
yield None
finally:
client.close()
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']
ias_url = cloud_settings['gns3_ias_url']
except KeyError as e:
log.error("Unable to create cloud provider: {}".format(e))
return
provider = RackspaceCtrl(username, apikey, ias_url)
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
class ListInstancesThread(QThread):
class ListInstancesThread(QtCore.QThread):
"""
Helper class to retrieve data from the provider in a separate thread,
avoid freezing the gui
"""
instancesReady = pyqtSignal(object)
instancesReady = QtCore.pyqtSignal(object)
def __init__(self, parent, provider):
super(QThread, self).__init__(parent)
super().__init__(parent)
self._provider = provider
def run(self):
try:
instances = self._provider.list_instances()
log.debug('Instance list:')
for instance in instances:
log.debug(' name={}, state={}'.format(instance.name, instance.state))
log.debug('Instance list: {}'.format([(i.name, i.state) for i in instances]))
self.instancesReady.emit(instances)
except Exception as e:
log.info('list_instances error: {}'.format(e))
class CreateInstanceThread(QThread):
class CreateInstanceThread(QtCore.QThread):
"""
Helper class to create instances in a separate thread
"""
instanceCreated = pyqtSignal(object, object)
instanceCreated = QtCore.pyqtSignal(object, object)
def __init__(self, parent, provider, name, flavor_id, image_id):
super(QThread, self).__init__(parent)
super().__init__(parent)
self._provider = provider
self._name = name
self._flavor_id = flavor_id
self._image_id = image_id
def run(self):
k = self._provider.create_key_pair(self._name)
log.debug("Creating cloud keypair with name {}".format(self._name))
try:
k = self._provider.create_key_pair(self._name)
except KeyPairExists:
log.debug("Cloud keypair with name {} exists. Recreating.".format(self._name))
# delete keypairs if they already exist
self._provider.delete_key_pair_by_name(self._name)
k = self._provider.create_key_pair(self._name)
log.debug("Creating cloud server with name {}".format(self._name))
i = self._provider.create_instance(self._name, self._flavor_id, self._image_id, k)
log.debug("Cloud server {} created".format(self._name))
self.instanceCreated.emit(i, k)
class DeleteInstanceThread(QThread):
class DeleteInstanceThread(QtCore.QThread):
"""
Helper class to remove an instance in a separate thread
"""
instanceDeleted = pyqtSignal(object)
instanceDeleted = QtCore.pyqtSignal(object)
def __init__(self, parent, provider, instance):
super(QThread, self).__init__(parent)
super().__init__(parent)
self._provider = provider
self._instance = instance
@@ -141,15 +121,60 @@ class DeleteInstanceThread(QThread):
self.instanceDeleted.emit(self._instance)
class StartGNS3ServerThread(QThread):
class StartGNS3ServerThread(QtCore.QThread):
"""
Perform an SSH connection to the instances in a separate thread,
outside the GUI event loop, and start GNS3 server
"""
gns3server_started = pyqtSignal(str, str, str)
gns3server_started = QtCore.pyqtSignal(str, str, str)
# This is for testing without pushing to github
# commands = '''
# DEBIAN_FRONTEND=noninteractive dpkg --configure -a
# DEBIAN_FRONTEND=noninteractive dpkg --add-architecture i386
# DEBIAN_FRONTEND=noninteractive apt-get -y update
# DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confnew" --force-yes -fuy dist-upgrade
# DEBIAN_FRONTEND=noninteractive apt-get -y install git python3-setuptools python3-netifaces python3-pip python3-zmq dynamips qemu-system
# DEBIAN_FRONTEND=noninteractive apt-get -y install libc6:i386 libstdc++6:i386 libssl1.0.0:i386
# ln -s /lib/i386-linux-gnu/libcrypto.so.1.0.0 /lib/i386-linux-gnu/libcrypto.so.4
# mkdir -p /opt/gns3
# tar xzf /tmp/gns3-server.tgz -C /opt/gns3
# cd /opt/gns3/gns3-server; pip3 install -r dev-requirements.txt
# cd /opt/gns3/gns3-server; python3 ./setup.py install
# ln -sf /usr/bin/dynamips /usr/local/bin/dynamips
# wget 'https://github.com/GNS3/iouyap/releases/download/0.95/iouyap.tar.gz'
# python -c 'import struct; open("/etc/hostid", "w").write(struct.pack("i", 00000000))'
# hostname gns3-iouvm
# tar xzf iouyap.tar.gz -C /usr/local/bin
# killall python3 gns3server gns3dms
# '''
commands = '''
DEBIAN_FRONTEND=noninteractive dpkg --configure -a
DEBIAN_FRONTEND=noninteractive dpkg --add-architecture i386
DEBIAN_FRONTEND=noninteractive apt-get -y update
DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confnew" --force-yes -fuy dist-upgrade
DEBIAN_FRONTEND=noninteractive apt-get -y install git python3-setuptools python3-netifaces python3-pip python3-zmq dynamips qemu-system
DEBIAN_FRONTEND=noninteractive apt-get -y install libc6:i386 libstdc++6:i386 libssl1.0.0:i386
ln -s /lib/i386-linux-gnu/libcrypto.so.1.0.0 /lib/i386-linux-gnu/libcrypto.so.4
mkdir -p /opt/gns3
cd /opt/gns3; git clone https://github.com/planctechnologies/gns3-server.git
cd /opt/gns3/gns3-server; git checkout dev; git pull
cd /opt/gns3/gns3-server; pip3 install -r dev-requirements.txt
cd /opt/gns3/gns3-server; python3 ./setup.py install
ln -sf /usr/bin/dynamips /usr/local/bin/dynamips
wget 'https://github.com/GNS3/iouyap/releases/download/0.95/iouyap-64-bit.tar.gz'
tar xzf iouyap-64-bit.tar.gz -C /usr/local/bin
python -c 'import struct; open("/etc/hostid", "w").write(struct.pack("i", 00000000))'
hostname gns3-iouvm # set hostname for iou
wget -O vpcs http://sourceforge.net/projects/vpcs/files/0.6/vpcs_0.6_Linux64/download
cp vpcs /usr/local/bin/vpcs
chmod a+x /usr/local/bin/vpcs
killall python3 gns3server gns3dms
'''
def __init__(self, parent, host, private_key_string, server_id, username, api_key, region, dead_time):
super(QThread, self).__init__(parent)
super().__init__(parent)
self._host = host
self._private_key_string = private_key_string
self._server_id = server_id
@@ -158,40 +183,86 @@ class StartGNS3ServerThread(QThread):
self._region = region
self._dead_time = dead_time
def exec_command(self, client, cmd, wait_time=-1):
cmd += '; exit $?'
stdout_data = b''
stderr_data = b''
log.debug('cmd: {}'.format(cmd))
# Send the command (non-blocking)
stdin, stdout, stderr = client.exec_command(cmd)
# Wait for the command to terminate
wait = int(wait_time)
while not stdout.channel.exit_status_ready() and wait != 0:
time.sleep(1)
wait -= 1
stdout_data = stdout.read()
stderr_data = stderr.read()
log.debug('exit status: {}'.format(stdout.channel.exit_status))
log.debug('stdout: {}'.format(stdout_data.decode('utf-8')))
log.debug('stderr: {}'.format(stderr_data.decode('utf-8')))
return stdout_data, stderr_data
def run(self):
with ssh_client(self._host, self._private_key_string) as client:
if client is not None:
# We might be attempting a connection before the instance is fully booted, so retry
# when the ssh connection fails.
ssh_connected = False
while not ssh_connected:
with ssh_client(self._host, self._private_key_string) as client:
if client is None:
time.sleep(1)
continue
ssh_connected = True
# This is for testing without pushing to github
# os.system('rm -rf /tmp/gns3-server')
# os.system('cp -a /Users/jseutter/projects/gns3-server /tmp/gns3-server')
# os.system('cd /tmp; tar czf /tmp/gns3-server.tgz gns3-server')
# sftp = client.open_sftp()
# sftp.put('/tmp/gns3-server.tgz', '/tmp/gns3-server.tgz')
# sftp.close()
for cmd in [l for l in self.commands.splitlines() if l.strip()]:
self.exec_command(client, cmd)
data = {
'instance_id': self._server_id,
'cloud_user_name': self._username,
'cloud_api_key': self._api_key,
'region': self._region,
'cloud_region': self._region,
'dead_time': self._dead_time,
}
# TODO: Properly escape the data portion of the command line
start_cmd = '/usr/bin/python3 /opt/gns3/gns3-server/gns3server/start_server.py -d -v --data="{}" 2>/tmp/gns3_stderr.log'.format(data)
log.debug(start_cmd)
stdin, stdout, stderr = client.exec_command(start_cmd)
response = stdout.read().decode('ascii')
log.debug('ssh response: {}'.format(response))
start_cmd = '/usr/bin/python3 /opt/gns3/gns3-server/gns3server/start_server.py -d -v --ip={} --data="{}" 2>/tmp/gns3-stderr.log'.format(self._host, data)
stdout, stderr = self.exec_command(client, start_cmd, wait_time=15)
response = stdout.decode('utf-8')
self.gns3server_started.emit(str(self._server_id), str(self._host), str(response))
class WSConnectThread(QThread):
class WSConnectThread(QtCore.QThread):
"""
Establish a websocket connection with the remote gns3server
instance. Run outside the GUI event loop.
"""
established = pyqtSignal(str)
established = QtCore.pyqtSignal(str)
def __init__(self, parent, provider, server_id, host, port, ca_file):
super(QThread, self).__init__(parent)
def __init__(self, parent, provider, server_id, host, port, ca_file,
auth_user, auth_password, ssh_pkey, instance_id):
super().__init__(parent)
self._provider = provider
self._server_id = server_id
self._host = host
self._port = port
self._ca_file = ca_file
self._auth_user = auth_user
self._auth_password = auth_password
self._ssh_pkey = ssh_pkey
self._instance_id = instance_id
def run(self):
"""
@@ -200,7 +271,9 @@ class WSConnectThread(QThread):
log.debug('WSConnectThread.run() begin')
servers = Servers.instance()
server = servers.getCloudServer(self._host, self._port, self._ca_file)
server = servers.getCloudServer(self._host, self._port, self._ca_file,
self._auth_user, self._auth_password, self._ssh_pkey,
self._instance_id)
log.debug('after getCloudServer call. {}'.format(server))
self.established.emit(str(self._server_id))
@@ -209,7 +282,7 @@ class WSConnectThread(QThread):
self.established.emit(self._server_id)
class UploadProjectThread(QThread):
class UploadProjectThread(QtCore.QThread):
"""
Zip and Upload project to the cloud
"""
@@ -219,10 +292,11 @@ class UploadProjectThread(QThread):
completed = QtCore.pyqtSignal()
update = QtCore.pyqtSignal(int)
def __init__(self, project_settings, cloud_settings):
super().__init__()
self.project_settings = project_settings
def __init__(self, parent, cloud_settings, project_path, images_path):
super().__init__(parent)
self.cloud_settings = cloud_settings
self.project_path = project_path
self.images_path = images_path
def run(self):
try:
@@ -234,7 +308,7 @@ class UploadProjectThread(QThread):
self.update.emit(10) # update progress to 10%
provider = get_provider(self.cloud_settings)
provider.upload_file(zipped_project_file, 'projects')
provider.upload_file(zipped_project_file, 'projects/' + os.path.basename(zipped_project_file))
self.update.emit(20) # update progress to 20%
@@ -242,22 +316,22 @@ class UploadProjectThread(QThread):
images = set([node.settings()["image"] for node in topology.nodes() if 'image' in node.settings()])
for i, image in enumerate(images):
provider.upload_file(image, 'images')
provider.upload_file(image, 'images/' + os.path.relpath(image, self.images_path))
self.update.emit(20 + (float(i) / len(images) * 80))
self.completed.emit()
except Exception as e:
log.exception("Error exporting project to cloud")
self.error.emit("Error exporting project {}".format(str(e)), True)
self.error.emit("Error exporting project: {}".format(e), True)
def zip_project_dir(self):
"""
Zips project files
:return: path to zipped project file
"""
project_name = os.path.basename(self.project_settings["project_path"])
project_name = os.path.basename(self.project_path)
output_filename = os.path.join(tempfile.gettempdir(), project_name + ".zip")
project_dir = os.path.dirname(self.project_settings["project_path"])
project_dir = os.path.dirname(self.project_path)
relroot = os.path.abspath(os.path.join(project_dir, os.pardir))
with zipfile.ZipFile(output_filename, "w", zipfile.ZIP_DEFLATED) as zip_file:
for root, dirs, files in os.walk(project_dir):
@@ -281,3 +355,171 @@ class UploadProjectThread(QThread):
def stop(self):
self.quit()
class UploadFilesThread(QtCore.QThread):
"""
Uploads files to cloud files
:param cloud_settings:
:param files_to_upload: list of tuples of (file path, file name to save in cloud)
"""
error = QtCore.pyqtSignal(str, bool)
completed = QtCore.pyqtSignal()
update = QtCore.pyqtSignal(int)
def __init__(self, parent, cloud_settings, files_to_upload):
super().__init__(parent)
self._cloud_settings = cloud_settings
self._files_to_upload = files_to_upload
def run(self):
self.update.emit(0)
try:
for i, file_to_upload in enumerate(self._files_to_upload):
provider = get_provider(self._cloud_settings)
log.debug('Uploading image {} to cloud as {}'.format(file_to_upload[0], file_to_upload[1]))
provider.upload_file(file_to_upload[0], file_to_upload[1])
self.update.emit((i+1) * 100 / len(self._files_to_upload))
log.debug('Uploading image completed')
except Exception as e:
log.exception("Error uploading images to cloud")
self.error.emit("Error uploading images: {}".format(e), True)
self.completed.emit()
def stop(self):
self.quit()
class DownloadProjectThread(QtCore.QThread):
"""
Downloads project from cloud storage
"""
# signals to update the progress dialog.
error = QtCore.pyqtSignal(str, bool)
completed = QtCore.pyqtSignal()
update = QtCore.pyqtSignal(int)
def __init__(self, parent, cloud_project_file_name, project_dest_path, images_dest_path, cloud_settings):
super().__init__(parent)
self.project_name = cloud_project_file_name
self.project_dest_path = project_dest_path
self.images_dest_path = images_dest_path
self.cloud_settings = cloud_settings
def run(self):
try:
self.update.emit(0)
provider = get_provider(self.cloud_settings)
zip_file = provider.download_file(self.project_name)
zip_file = zipfile.ZipFile(zip_file, mode='r')
zip_file.extractall(self.project_dest_path)
zip_file.close()
project_name = zip_file.namelist()[0].strip('/')
self.update.emit(20)
with open(os.path.join(self.project_dest_path, project_name, project_name + '.gns3'), 'r') as f:
project_settings = json.loads(f.read())
images = set()
for node in project_settings["topology"].get("nodes", []):
if "properties" in node and "image" in node["properties"]:
images.add(node["properties"]["image"])
image_names_in_cloud = provider.find_storage_image_names(images)
for i, image in enumerate(images):
dest_path = os.path.join(self.images_dest_path, *image_names_in_cloud[image].split('/')[1:])
if not os.path.exists(os.path.dirname(dest_path)):
os.makedirs(os.path.dirname(dest_path))
provider.download_file(image_names_in_cloud[image], dest_path)
self.update.emit(20 + (float(i) / len(images) * 80))
self.completed.emit()
except Exception as e:
log.exception("Error importing project from cloud")
self.error.emit("Error importing project: {}".format(e), True)
def stop(self):
self.quit()
class DownloadImagesThread(QtCore.QThread):
"""
Downloads multiple files from cloud files
"""
error = QtCore.pyqtSignal(str, bool)
completed = QtCore.pyqtSignal()
update = QtCore.pyqtSignal(int)
def __init__(self, cloud_settings, images_dest_path, image_names):
super().__init__()
self._cloud_settings = cloud_settings
self._images_dest_path = images_dest_path
self._image_names = image_names
def run(self):
self.update.emit(0)
try:
provider = get_provider(self._cloud_settings)
image_names_in_cloud = provider.find_storage_image_names(self._image_names)
for i, image in enumerate(self._image_names):
dest_path = os.path.join(self._images_dest_path, *image_names_in_cloud[image].split('/')[1:])
if not os.path.exists(os.path.dirname(dest_path)):
os.makedirs(os.path.dirname(dest_path))
provider.download_file(image_names_in_cloud[image], dest_path)
self.update.emit(i * 100 / len(self._image_names))
self.completed.emit()
except Exception as e:
log.exception("Error importing project from cloud")
self.error.emit("Error importing project: {}".format(e), True)
def stop(self):
self.quit()
class DeleteProjectThread(QtCore.QThread):
"""
Deletes project from cloud storage
"""
# signals to update the progress dialog.
error = QtCore.pyqtSignal(str, bool)
completed = QtCore.pyqtSignal()
update = QtCore.pyqtSignal(int)
def __init__(self, parent, project_file_name, cloud_settings):
super().__init__(parent)
self.project_file_name = project_file_name
self.cloud_settings = cloud_settings
def run(self):
try:
provider = get_provider(self.cloud_settings)
provider.delete_file(self.project_file_name)
self.completed.emit()
except Exception as e:
log.exception("Error deleting project")
self.error.emit("Error deleting project: {}".format(e), True)
def stop(self):
pass
def get_cloud_projects(cloud_settings):
provider = get_provider(cloud_settings)
return provider.list_projects()

253
gns3/cloud_builder.py Normal file
View File

@@ -0,0 +1,253 @@
# -*- 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 PyQt4.QtCore import pyqtSignal
from PyQt4.QtCore import QThread
import ast
import logging
import os
import time
from .cloud.utils import ssh_client
from .cloud.exceptions import KeyPairExists
from .servers import Servers
from .topology import Topology
log = logging.getLogger(__name__)
class CloudBuilder(QThread):
"""
"""
# Notify with progress amount and instance_id
progressUpdate = pyqtSignal(object, str)
# Notify with current state and instance_id
stateChange = pyqtSignal(object, str)
# Notify when instance is ready with instance_id
buildComplete = pyqtSignal(str)
# Notify when the instance has been created with instance and keypair
instanceCreated = pyqtSignal(object, object)
# Notify when the public ip is available with ip and instance_id
instanceHasIP = pyqtSignal(str, str)
# Notify when instance id exists with builder and instance_id
instanceIdExists = pyqtSignal(object, str)
def __init__(self, parent, cloud_provider, ca_dir):
super(QThread, self).__init__(parent)
# Store our parent so it can be passed to threads we spawn.
self._parent = parent
self._provider = cloud_provider
self._ca_dir = ca_dir
self._start_at_create = False
self._start_at_setup = False
self._instance = None
def startAtCreate(self, instance_name, flavor_id, image_id):
self._start_at_create = True
self._instance_name = instance_name
self._flavor_id = flavor_id
self._image_id = image_id
def startAtSetup(self, instance, keypair):
self._start_at_setup = True
self._instance = instance
self._key_pair = keypair
def run(self):
try:
log.debug('CloudBuilder.run')
if self._start_at_create:
log.debug('CloudBuilder._start_at_create')
self._createInstance(self._provider, self._instance_name, self._flavor_id,
self._image_id)
log.debug('got here 3')
if self._start_at_setup:
log.debug('CloudBuilder start at setup')
self._instanceCreated(self._instance, self._key_pair)
except Exception:
log.exception("CloudBuilder trapped an exception:")
log.error('CloudBuilder stopped in error state.')
def _createInstance(self, provider, name, flavor_id, image_id):
log.debug("Creating cloud keypair with name {}".format(name))
key_pair = None
while key_pair is None:
try:
key_pair = provider.create_key_pair(name)
except KeyPairExists:
log.debug("Deleting old key pair with name {}.".format(name))
self._provider.delete_key_pair_by_name(name)
except Exception as e:
log.debug("create_key_pair exception {}".format(e))
log.debug("Creating cloud server with name {}".format(name))
instance = None
while instance is None:
try:
instance = self._provider.create_instance(name, flavor_id, image_id, key_pair)
except Exception as e:
log.debug("create_instance exception {}".format(e))
log.debug("Cloud server {} created".format(name))
self._instanceCreated(instance, key_pair)
def _instanceCreated(self, instance, key_pair):
log.debug('CloudBuilder._instanceCreated {}'.format(instance.id))
self._instance = instance
self._instance_id = instance.id
self._key_pair = key_pair
self.instanceIdExists.emit(self, instance.id)
self.instanceCreated.emit(instance, key_pair)
self._waitForPublicIP()
def _waitForPublicIP(self):
public_ip = None
while public_ip is None:
time.sleep(10)
try:
instance = self._provider.get_instance(self._instance)
# Look for public ip address
for ip in instance.public_ips:
# Don't use the ipv6 address
if ':' not in ip:
public_ip = ip
break
except Exception as e:
log.debug('list_instances error: {}'.format(e))
# updated info, keep it.
self._instance = instance
self._public_ip = public_ip
self.instanceHasIP.emit(self._public_ip, self._instance.id)
time.sleep(60)
self._startGNS3Server(1800)
def _startGNS3Server(self, dead_time):
commands = '''
DEBIAN_FRONTEND=noninteractive dpkg --configure -a
DEBIAN_FRONTEND=noninteractive dpkg --add-architecture i386
DEBIAN_FRONTEND=noninteractive apt-get -y update
DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confnew" --force-yes -fuy dist-upgrade
DEBIAN_FRONTEND=noninteractive apt-get -y install git python3-setuptools python3-netifaces python3-pip python3-zmq dynamips qemu-system
DEBIAN_FRONTEND=noninteractive apt-get -y install libc6:i386 libstdc++6:i386 libssl1.0.0:i386
ln -s /lib/i386-linux-gnu/libcrypto.so.1.0.0 /lib/i386-linux-gnu/libcrypto.so.4
mkdir -p /opt/gns3
cd /opt/gns3; git clone https://github.com/planctechnologies/gns3-server.git
cd /opt/gns3/gns3-server; git checkout dev; git pull
cd /opt/gns3/gns3-server; pip3 install -r dev-requirements.txt
cd /opt/gns3/gns3-server; python3 ./setup.py install
ln -sf /usr/bin/dynamips /usr/local/bin/dynamips
wget 'https://github.com/GNS3/iouyap/releases/download/0.95/iouyap.tar.gz'
tar xzf iouyap.tar.gz -C /usr/local/bin
python -c 'import struct; open("/etc/hostid", "w").write(struct.pack("i", 00000000))'
hostname gns3-iouvm # set hostname for iou
wget 'http://downloads.sourceforge.net/project/vpcs/0.6/vpcs_0.6_Linux64'
cp vpcs_0.6_Linux64 /usr/local/bin/vpcs
chmod a+x /usr/local/bin/vpcs
killall python3 gns3server gns3dms
'''
def exec_command(client, cmd, wait_time=-1):
cmd += '; exit $?'
stdout_data = b''
stderr_data = b''
log.debug('cmd: {}'.format(cmd))
# Send the command (non-blocking)
stdin, stdout, stderr = client.exec_command(cmd)
# Wait for the command to terminate
wait = int(wait_time)
while not stdout.channel.exit_status_ready() and wait != 0:
time.sleep(1)
wait -= 1
stdout_data = stdout.read()
stderr_data = stderr.read()
log.debug('exit status: {}'.format(stdout.channel.exit_status))
log.debug('stdout: {}'.format(stdout_data.decode('utf-8')))
log.debug('stderr: {}'.format(stderr_data.decode('utf-8')))
return stdout_data, stderr_data
# We might be attempting a connection before the instance is fully booted, so retry
# when the ssh connection fails.
ssh_connected = False
response = None
while not ssh_connected:
with ssh_client(self._public_ip, self._key_pair.private_key) as client:
if client is None:
time.sleep(1)
continue
ssh_connected = True
for cmd in [l for l in commands.splitlines() if l.strip()]:
exec_command(client, cmd)
data = {
'instance_id': self._instance_id,
'cloud_user_name': self._provider.username,
'cloud_api_key': self._provider.api_key,
'cloud_region': self._provider.region,
'dead_time': dead_time,
}
# TODO: Properly escape the data portion of the command line
start_cmd = '/usr/bin/python3 /opt/gns3/gns3-server/gns3server/start_server.py -d -v --ip={} --data="{}" 2>/tmp/gns3-stderr.log'.format(self._public_ip, data)
stdout, stderr = exec_command(client, start_cmd, wait_time=15)
response = stdout.decode('utf-8')
log.debug(response)
data = ast.literal_eval(response)
# TODO: have the server return the port it is running on
port = 8000
username = data['WEB_USERNAME']
password = data['WEB_PASSWORD']
ssl_cert = ''.join(data['SSL_CRT'])
ca_filename = 'cloud_server_{}.crt'.format(self._public_ip)
ca_dir = self._ca_dir
ca_file = os.path.join(ca_dir, ca_filename)
try:
os.makedirs(ca_dir)
except FileExistsError:
pass
with open(ca_file, 'wb') as ca_fh:
ca_fh.write(ssl_cert.encode('utf-8'))
topology = Topology.instance()
top_instance = topology.getInstance(self._instance_id)
top_instance.set_later_attributes(self._public_ip, port, ssl_cert, ca_file)
servers = Servers.instance()
server = servers.getCloudServer(self._public_ip, port, ca_file, username, password,
self._key_pair.private_key, self._instance_id)
servers.save()
log.debug('Cloud server gns3server started.')
self.buildComplete.emit(self._instance_id)

View File

@@ -1,40 +1,37 @@
# -*- coding: utf-8 -*-
import ast
from collections import namedtuple
import logging
from PyQt4.QtGui import QWidget
from PyQt4.QtGui import QIcon
from PyQt4.QtGui import QMenu
from PyQt4.QtGui import QAction
from PyQt4.QtGui import QInputDialog
from PyQt4.QtCore import QAbstractTableModel
from PyQt4.QtCore import QModelIndex
from PyQt4.QtCore import QTimer
from PyQt4.QtCore import pyqtSignal
from PyQt4.Qt import Qt
import os
import json
from .cloud.utils import (ListInstancesThread, CreateInstanceThread, DeleteInstanceThread,
StartGNS3ServerThread, WSConnectThread)
from libcloud.compute.types import NodeState
from .qt import QtCore, QtGui
from .cloud.utils import (ListInstancesThread, DeleteInstanceThread)
from .topology import Topology
from .servers import Servers
# this widget was promoted on Creator, must use absolute imports
from gns3.ui.cloud_inspector_view_ui import Ui_CloudInspectorView
from gns3.cloud_builder import CloudBuilder
from gns3.cloud_instances import CloudInstances
log = logging.getLogger(__name__)
POLLING_TIMER = 10000 # in milliseconds
class RunningInstanceState():
class RunningInstanceState(NodeState):
"""
GNS3 states for running instances
"""
IDLE = 0
GNS3SERVER_STARTED = 1
WS_CONNECTED = 2
GNS3SERVER_STARTING = -1
GNS3SERVER_STARTED = -2
WS_CONNECTED = -3
class InstanceTableModel(QAbstractTableModel):
class InstanceTableModel(QtCore.QAbstractTableModel):
"""
A custom table model storing data of cloud instances
"""
@@ -55,16 +52,18 @@ class InstanceTableModel(QAbstractTableModel):
self._ids = []
self.reset()
def _get_status_icon_path(self, state):
def _get_status_icon_path(self, instance):
"""
Return a string pointing to the graphic resource
"""
if state == NodeState.RUNNING:
if instance.state == RunningInstanceState.WS_CONNECTED:
return ':/icons/led_green.svg'
elif state in (NodeState.REBOOTING, NodeState.PENDING, NodeState.UNKNOWN):
return ':/icons/led_yellow.svg'
else:
elif instance.state in (RunningInstanceState.STOPPED,
RunningInstanceState.TERMINATED,
RunningInstanceState.UNKNOWN):
return ':/icons/led_red.svg'
else:
return ':/icons/led_yellow.svg'
def rowCount(self, QModelIndex_parent=None, *args, **kwargs):
return len(self._instances)
@@ -76,12 +75,12 @@ class InstanceTableModel(QAbstractTableModel):
instance = self._instances.get(self._ids[index.row()])
col = index.column()
if role == Qt.DecorationRole:
if role == QtCore.Qt.DecorationRole:
if col == 1:
# status
return QIcon(self._get_status_icon_path(instance.state))
return QtGui.QIcon(self._get_status_icon_path(instance))
elif role == Qt.DisplayRole:
elif role == QtCore.Qt.DisplayRole:
if col == 0:
# name
return instance.name
@@ -98,11 +97,17 @@ class InstanceTableModel(QAbstractTableModel):
return 'Unknown'
elif col == 3:
# devices
return 0
count = 0
topology = Topology.instance()
for node in topology.nodes():
id = node._server.instance_id or 0
if instance.id == id:
count += 1
return count
return None
def headerData(self, section, orientation, role=None):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
try:
return self._header_data[section]
except IndexError:
@@ -110,9 +115,9 @@ class InstanceTableModel(QAbstractTableModel):
return super(InstanceTableModel, self).headerData(section, orientation, role)
def addInstance(self, instance):
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
if not len(self._instances):
self.beginInsertColumns(QModelIndex(), 0, self._width-1)
self.beginInsertColumns(QtCore.QModelIndex(), 0, self._width-1)
self.endInsertColumns()
self._ids.append(instance.id)
self._instances[instance.id] = instance
@@ -133,7 +138,7 @@ class InstanceTableModel(QAbstractTableModel):
def removeInstanceById(self, instance_id):
try:
index = self._ids.index(instance_id)
self.beginRemoveRows(QModelIndex(), index, index)
self.beginRemoveRows(QtCore.QModelIndex(), index, index)
del self._instances[instance_id]
del self._ids[index]
self.endRemoveRows()
@@ -156,10 +161,10 @@ class InstanceTableModel(QAbstractTableModel):
self.addInstance(instance)
def getInstanceById(self, instance_id):
return self._instances[instance_id]
return self._instances.get(instance_id, None)
class CloudInspectorView(QWidget, Ui_CloudInspectorView):
class CloudInspectorView(QtGui.QWidget, Ui_CloudInspectorView):
"""
Table view showing data coming from InstanceTableModel
@@ -167,10 +172,10 @@ class CloudInspectorView(QWidget, Ui_CloudInspectorView):
instanceSelected(int) Emitted when users click and select an instance on the inspector.
Param int is the ID of the instance
"""
instanceSelected = pyqtSignal(int)
instanceSelected = QtCore.pyqtSignal(str)
def __init__(self, parent):
super(QWidget, self).__init__(parent)
super(QtGui.QWidget, self).__init__(parent)
self.setupUi(self)
self._provider = None
@@ -181,21 +186,21 @@ class CloudInspectorView(QWidget, Ui_CloudInspectorView):
self._model = InstanceTableModel() # shortcut for self.uiInstancesTableView.model()
self.uiInstancesTableView.setModel(self._model)
self.uiInstancesTableView.verticalHeader().hide()
self.uiInstancesTableView.setContextMenuPolicy(Qt.CustomContextMenu)
self.uiInstancesTableView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.uiInstancesTableView.horizontalHeader().setStretchLastSection(True)
# connections
self.uiInstancesTableView.customContextMenuRequested.connect(self._contextMenu)
self.uiInstancesTableView.selectionModel().currentRowChanged.connect(self._rowChanged)
self.uiInstancesTableView.clicked.connect(self._rowChanged)
self.uiCreateInstanceButton.clicked.connect(self._create_new_instance)
self._pollingTimer = QTimer(self)
self._pollingTimer = QtCore.QTimer(self)
self._pollingTimer.timeout.connect(self._polling_slot)
# map flavor ids to combobox indexes
self.flavor_index_id = []
# internal status for running instances
self._running_instances = {}
# A dictionary of {image_id, CloudBuilder}
self._builders = {}
def _get_flavor_index(self, flavor_id):
try:
@@ -203,23 +208,20 @@ class CloudInspectorView(QWidget, Ui_CloudInspectorView):
except ValueError:
return -1
def load(self, main_win, instances):
def load(self, main_win, instance_ids):
"""
Fill the model data layer with instances retrieved through libcloud
Fill the model data layer with instance info loaded from the topology file
"""
self._main_window = main_win
self._provider = main_win.cloudProvider
self._settings = main_win.cloudSettings()
log.info('CloudInspectorView.load')
# TODO: If a network error occurs in the first ListInstances call,
# the instance will *never* appear in the cloud inspector and will
# not get cleaned up when the gui exits. Fix this bug.
for i in instances:
self._project_instances_id.append(i["id"])
for instance_id in instance_ids:
self._project_instances_id.append(instance_id)
update_thread = ListInstancesThread(self, self._provider)
update_thread.instancesReady.connect(self._populate_model)
update_thread.instancesReady.connect(self._update_model)
update_thread.start()
self._pollingTimer.start(POLLING_TIMER)
# fill sizes comboboxes
@@ -246,10 +248,10 @@ class CloudInspectorView(QWidget, Ui_CloudInspectorView):
def _contextMenu(self, pos):
# create actions
delete_action = QAction("Delete", self)
delete_action = QtGui.QAction("Delete", self)
delete_action.triggered.connect(self._deleteSelectedInstance)
# create context menu and add actions
menu = QMenu(self.uiInstancesTableView)
menu = QtGui.QMenu(self.uiInstancesTableView)
menu.addAction(delete_action)
# show the menu
menu.popup(self.uiInstancesTableView.viewport().mapToGlobal(pos))
@@ -262,20 +264,42 @@ class CloudInspectorView(QWidget, Ui_CloudInspectorView):
if len(sel) and self._provider is not None:
index = sel[0].row()
instance = self._model.getInstance(index)
delete_thread = DeleteInstanceThread(self, self._provider, instance)
delete_thread.instanceDeleted.connect(self._main_window.remove_instance_from_project)
delete_thread.start()
instance.name = 'Deleting...'
self._model.updateInstanceFields(instance, ['name',])
# warn user this is destructive
msg = "Do you want to remove the instance and any devices running on it?"
proceed = QtGui.QMessageBox.question(self, 'Warning', msg,
QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)
def _rowChanged(self, current, previous):
if proceed == QtGui.QMessageBox.Yes:
# disconnect and remove the server
servers = Servers.instance()
cs = servers.cloudServerById(instance.id)
if cs is not None:
servers.removeCloudServer(cs)
# remove instance from the the topology
topology = Topology.instance()
topology.removeInstance(instance.id)
delete_thread = DeleteInstanceThread(self, self._provider, instance)
delete_thread.instanceDeleted.connect(self._main_window.remove_instance_from_project)
delete_thread.start()
instance.name = 'Deleting...'
self._model.updateInstanceFields(instance, ['name'])
def _rowChanged(self, index):
"""
This slot is invoked every time users change the current selected row on the
inspector
"""
if current.isValid():
instance = self._model.getInstance(current.row())
selection = self.uiInstancesTableView.selectionModel().selection()
if selection.isEmpty():
return
item = selection.indexes()[0]
if item.isValid():
instance = self._model.getInstance(item.row())
self.instanceSelected.emit(instance.id)
def _polling_slot(self):
@@ -289,107 +313,62 @@ class CloudInspectorView(QWidget, Ui_CloudInspectorView):
update_thread.instancesReady.connect(self._update_model)
update_thread.start()
def _gns3server_started_slot(self, id, host_ip, start_response):
def _instanceBuilt(self, id):
"""
This slot is called when the StartGNS3ServerThread succesfully started
the server.
:param id: the id of the instance
:param host_ip: the host ip of the instance
:param start_response: the output of the server start script on the remote host
This slot is called when instance has finished building.
"""
self._running_instances[id] = RunningInstanceState.GNS3SERVER_STARTED
data = ast.literal_eval(start_response)
instance = self._model.getInstanceById(id)
instance.state = RunningInstanceState.WS_CONNECTED
self._model.updateInstanceFields(instance, ['state'])
# TODO: have the server return the port it is running on
port = 8000
username = data['WEB_USERNAME']
password = data['WEB_PASSWORD']
ssl_cert = ''.join(data['SSL_CRT'])
# TODO: Store the cert file in an appropriate spot
ca_file = '/tmp/cloud_server_{}.crt'.format(host_ip)
open(ca_file, 'w').write(ssl_cert)
log.debug('Cloud server gns3server started.')
wss_thread = WSConnectThread(self, self._provider, id, host_ip, port, ca_file)
wss_thread.established.connect(self._wss_connected_slot)
wss_thread.start()
def _wss_connected_slot(self, id):
"""
This slot is called when the WSConnectThread succesfully connected to
the websocket on the remote host
"""
self._running_instances[id] = RunningInstanceState.WS_CONNECTED
def _get_public_ip(self, ip_list):
"""
Pick the ipv4 address from the list of ip addresses that the instance
has.
"""
for ip in ip_list:
log.debug('Cloud server ip {}'.format(ip))
# Don't use the ipv6 address
if ':' not in ip:
log.debug('Chose {} as public ip'.format(ip))
return ip
return None
if self._main_window.loading_cloud_project:
project_settings = self._main_window.projectSettings()
path = project_settings.get("project_path")
with open(path, "r") as f:
json_topology = json.load(f)
topology = Topology.instance()
topology.load(json_topology)
self._main_window.loading_cloud_project = False
def _update_model(self, instances):
if not instances:
return
# filter instances for current project
# Filter instances to only those in the current project
project_instances = [i for i in instances if i.id in self._project_instances_id]
for i in project_instances:
self._model.updateInstanceFields(i, ['state'])
# cleanup removed instances
# populate underlying model if this is the first call
if self._model.rowCount() == 0 and len(project_instances) > 0:
self._populate_model(project_instances)
self._rebuild_instances(project_instances)
instance_manager = CloudInstances.instance()
instance_manager.update_instances(instances)
# Clean up removed instances
real = set(i.id for i in project_instances)
current = set(self._model.instanceIds)
for i in current.difference(real):
self._model.removeInstanceById(i)
self.uiInstancesTableView.resizeColumnsToContents()
# handle state for running instances
topology = Topology.instance()
# Update instance status
for i in project_instances:
if i.state != NodeState.RUNNING:
self._running_instances = {}
continue
# get the customized instance state from self._model
model_instance = self._model.getInstanceById(i.id)
topology_instance = topology.getInstance(i.id)
if topology_instance is None:
continue
state = self._running_instances.setdefault(i.id, RunningInstanceState.IDLE)
if state == RunningInstanceState.IDLE:
public_ip = self._get_public_ip(i.public_ips)
# start GNS3 server and deadman switch
ssh_thread = StartGNS3ServerThread(
self, public_ip, topology_instance.private_key, i.id,
self._provider.username, self._provider.api_key, self._provider.region,
1800)
ssh_thread.gns3server_started.connect(self._gns3server_started_slot)
ssh_thread.start()
elif state == RunningInstanceState.GNS3SERVER_STARTED:
# start WSS connection
# TODO: Don't think this is used, remove.
# wss_thread = WSConnectThread(self, i.id)
# wss_thread.established.connect(self._wss_connected_slot)
# wss_thread.start()
pass
# update model instance state if needed
if i.state != RunningInstanceState.RUNNING:
self._model.updateInstanceFields(i, ['state'])
def _populate_model(self, instances):
log.info('CloudInspectorView._populate_model')
self._model.flavors = self._provider.list_flavors()
# filter instances for current project
project_instances = [i for i in instances if i.id in self._project_instances_id]
for i in project_instances:
self._model.addInstance(i)
for inst in instances:
self._model.addInstance(inst)
self.uiInstancesTableView.resizeColumnsToContents()
def _create_new_instance(self):
@@ -397,12 +376,50 @@ class CloudInspectorView(QWidget, Ui_CloudInspectorView):
flavor_id = self.flavor_index_id[idx]
image_id = self._settings['default_image']
name, ok = QInputDialog.getText(self,
"New instance",
"Choose a name for the instance and press Ok,\n"
"then wait for the instance to appear in the inspector.")
name, ok = QtGui.QInputDialog.getText(self,
"New instance",
"Choose a name for the instance and press Ok,\n"
"then wait for the instance to appear in the inspector.")
if ok:
create_thread = CreateInstanceThread(self, self._provider, name, flavor_id, image_id)
create_thread.instanceCreated.connect(self._main_window.add_instance_to_project)
create_thread.start()
self.createInstance(name, flavor_id, image_id)
def createInstance(self, instance_name, flavor_id, image_id):
if not instance_name.endswith("-gns3"):
instance_name += "-gns3"
# TODO: Add a keys_dir to projectSettings
ca_dir = os.path.join(self._main_window.projectSettings()["project_files_dir"], "keys")
builder = CloudBuilder(self, self._provider, ca_dir)
builder.startAtCreate(instance_name, flavor_id, image_id)
builder.instanceCreated.connect(self._main_window.add_instance_to_project)
builder.instanceCreated.connect(CloudInstances.instance().add_instance)
builder.instanceIdExists.connect(self._associateBuilderWithInstance)
builder.instanceHasIP.connect(CloudInstances.instance().update_host_for_instance)
builder.buildComplete.connect(self._instanceBuilt)
builder.start()
return builder
def _associateBuilderWithInstance(self, builder, instance_id):
self._builders[instance_id] = builder
def _rebuild_instances(self, instances):
# TODO: Add a keys_dir to projectSettings
ca_dir = os.path.join(self._main_window.projectSettings()["project_files_dir"], "keys")
for instance in instances:
log.debug('CloudInspectorView._rebuild_instances {}'.format(instance.name))
builder = CloudBuilder(self, self._provider, ca_dir)
cloud_instance = CloudInstances.instance().get_instance(instance.id)
public_key = cloud_instance.public_key
private_key = cloud_instance.private_key
# Fake a KeyPair object because we don't store it.
keypair = namedtuple('KeyPair', ['private_key', 'public_key'])(private_key, public_key)
builder.startAtSetup(instance, keypair)
builder.instanceCreated.connect(self._main_window.add_instance_to_project)
builder.instanceCreated.connect(CloudInstances.instance().add_instance)
builder.instanceIdExists.connect(self._associateBuilderWithInstance)
builder.instanceHasIP.connect(CloudInstances.instance().update_host_for_instance)
builder.buildComplete.connect(self._instanceBuilt)
builder.start()
return builder

154
gns3/cloud_instances.py Normal file
View File

@@ -0,0 +1,154 @@
# -*- 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/>.
"""
Keeps track of all cloud instances the app has started.
"""
from .qt import QtCore
from gns3.topology import TopologyInstance
import logging
log = logging.getLogger(__name__)
class CloudInstances(QtCore.QObject):
"""
This class stores the instances that gns3 gui has started. This can be different than the list
of instances in the topology that can be changed when switching projects. This list is not touched
when switching projects and is stored in the .ini file.
"""
def __init__(self, *args, **kwargs):
super(CloudInstances, self).__init__(*args, **kwargs)
self._instances = []
@staticmethod
def instance():
"""
Singleton to return only one instance of CloudInstances.
:returns: instance of CloudInstances
"""
if not hasattr(CloudInstances, "_instance"):
CloudInstances._instance = CloudInstances()
return CloudInstances._instance
@property
def instances(self):
return self._instances
def clear(self):
self._instances.clear()
def add(self, topology_instance):
self._instances.append(topology_instance)
def add_instance(self, instance, keypair):
if instance is None:
return
existing = self.get_instance(instance.id)
if existing is None:
ti = TopologyInstance(instance.name, instance.id, instance.extra['flavorId'],
instance.extra['imageId'], keypair.private_key, keypair.public_key)
self._instances.append(ti)
self.save()
def update_instances(self, instances):
"""
Compare with the existing list of instances to purge instances that no
longer exist.
"""
save_needed = False
# Look for instances that have been deleted
for stored in self._instances:
found = False
for dynamic in instances:
if stored.id == dynamic.id:
found = True
break
if not found:
self._instances.remove(stored)
save_needed = True
if save_needed:
self.save()
def update_host_for_instance(self, host, instance_id):
"""
Update the public IP for the instance.
"""
for instance in self.instances:
if instance.id == instance_id:
if instance.host != host:
instance.host = host
self.save()
def save(self):
"""
Save the list of cloud instances to the config file
"""
log.debug('Saving cloud instances')
settings = QtCore.QSettings()
settings.beginGroup("CloudInstances")
settings.remove("")
# Save the instances
settings.beginWriteArray("cloud_instance", len(self._instances))
index = 0
for instance in self._instances:
settings.setArrayIndex(index)
for name in instance.fields():
value = getattr(instance, name) if not None else ""
log.debug('{}={}'.format(name, str(value)[0:60]))
settings.setValue(name, value)
index += 1
settings.endArray()
settings.endGroup()
def load(self):
"""
Load instance info from the config file to the topology
"""
log.debug('Loading cloud instances')
settings = QtCore.QSettings()
settings.beginGroup("CloudInstances")
# Load the instances
size = settings.beginReadArray("cloud_instance")
for index in range(0, size):
settings.setArrayIndex(index)
info = {}
for name in TopologyInstance.fields():
value = settings.value(name, "")
log.debug('{}={}'.format(name, str(value)[0:60]))
info[name] = value
ti = TopologyInstance(**info)
self._instances.append(ti)
def get_instance(self, instance_id):
"""
Retrieve a TopologyInstance objects if present
"""
for i in self._instances:
if i.id == instance_id:
return i
return None

View File

@@ -30,83 +30,83 @@ ip tcp synwait-time 5
!
interface Ethernet0/0
no ip address
shutdown
no shutdown
duplex auto
!
interface Ethernet0/1
no ip address
shutdown
no shutdown
duplex auto
!
interface Ethernet0/2
no ip address
shutdown
no shutdown
duplex auto
!
interface Ethernet0/3
no ip address
shutdown
no shutdown
duplex auto
!
interface Ethernet1/0
no ip address
shutdown
no shutdown
duplex auto
!
interface Ethernet1/1
no ip address
shutdown
no shutdown
duplex auto
!
interface Ethernet1/2
no ip address
shutdown
no shutdown
duplex auto
!
interface Ethernet1/3
no ip address
shutdown
no shutdown
duplex auto
!
interface Serial2/0
interface Ethernet2/0
no ip address
shutdown
serial restart-delay 0
no shutdown
duplex auto
!
interface Serial2/1
interface Ethernet2/1
no ip address
shutdown
serial restart-delay 0
no shutdown
duplex auto
!
interface Serial2/2
interface Ethernet2/2
no ip address
shutdown
serial restart-delay 0
no shutdown
duplex auto
!
interface Serial2/3
interface Ethernet2/3
no ip address
shutdown
serial restart-delay 0
no shutdown
duplex auto
!
interface Serial3/0
interface Ethernet3/0
no ip address
shutdown
serial restart-delay 0
no shutdown
duplex auto
!
interface Serial3/1
interface Ethernet3/1
no ip address
shutdown
serial restart-delay 0
no shutdown
duplex auto
!
interface Serial3/2
interface Ethernet3/2
no ip address
shutdown
serial restart-delay 0
no shutdown
duplex auto
!
interface Serial3/3
interface Ethernet3/3
no ip address
shutdown
serial restart-delay 0
no shutdown
duplex auto
!
interface Vlan1
no ip address

View File

@@ -218,6 +218,8 @@ class ConsoleCmd(cmd.Cmd):
else:
print("Activating debugging")
root.addHandler(ch)
from .main_window import MainWindow
MainWindow.instance().setSettings({"debug_level": level})
except:
print(self.do_debug.__doc__)
else:

View File

@@ -17,10 +17,10 @@
import os
import sys
import pkg_resources
from ..qt import QtCore, QtGui, QtWebKit
from ..ui.getting_started_dialog_ui import Ui_GettingStartedDialog
from ..utils.get_resource import get_resource
class GettingStartedDialog(QtGui.QDialog, Ui_GettingStartedDialog):
@@ -39,7 +39,7 @@ class GettingStartedDialog(QtGui.QDialog, Ui_GettingStartedDialog):
self.uiWebView.page().setLinkDelegationPolicy(QtWebKit.QWebPage.DelegateAllLinks)
self.uiWebView.linkClicked.connect(self._urlClickedSlot)
self.uiWebView.loadFinished.connect(self._loadFinishedSlot)
self.uiCheckBox.setChecked(QtCore.QSettings().value("GUI/show_getting_started_dialog", True, type=bool))
self.uiCheckBox.setChecked(QtCore.QSettings().value("GUI/hide_getting_started_dialog", False, type=bool))
self._timer = QtCore.QTimer(self)
self._timer.timeout.connect(self._loadFinishedSlot)
self._timer.setSingleShot(True)
@@ -53,7 +53,7 @@ class GettingStartedDialog(QtGui.QDialog, Ui_GettingStartedDialog):
:returns: boolean
"""
return self.uiCheckBox.isChecked()
return not self.uiCheckBox.isChecked()
def done(self, result):
"""
@@ -62,7 +62,7 @@ class GettingStartedDialog(QtGui.QDialog, Ui_GettingStartedDialog):
:param result: ignored
"""
QtCore.QSettings().setValue("GUI/show_getting_started_dialog", self.uiCheckBox.isChecked())
QtCore.QSettings().setValue("GUI/hide_getting_started_dialog", self.uiCheckBox.isChecked())
QtGui.QDialog.done(self, result)
def _urlClickedSlot(self, url):
@@ -87,14 +87,10 @@ class GettingStartedDialog(QtGui.QDialog, Ui_GettingStartedDialog):
self._timer.timeout.disconnect()
if result is False:
# load a local resource if the page is not available
resource_name = os.path.join("static", "getting_started.html")
getting_started = None
if hasattr(sys, "frozen") and os.path.isfile(resource_name):
getting_started = os.path.normpath(resource_name)
elif pkg_resources.resource_exists("gns3", resource_name):
getting_started_page = pkg_resources.resource_filename("gns3", resource_name)
getting_started = os.path.normpath(getting_started_page)
if getting_started:
getting_started = get_resource(os.path.join("static", "getting_started.html"))
if getting_started and not (sys.platform.startswith("win") and not sys.maxsize > 2 ** 32):
# do not show the page on Windows 32-bit (crash when no Internet connection)
self.uiWebView.load(QtCore.QUrl("file://{}".format(getting_started)))
else:
self.uiCheckBox.setChecked(True)
self.accept()

View File

@@ -0,0 +1,69 @@
"""
Dialog for importing cloud projects
"""
from ..ui.import_cloud_project_dialog_ui import Ui_ImportCloudProjectDialog
from ..qt import QtGui
from ..cloud.utils import get_cloud_projects, DownloadProjectThread, DeleteProjectThread
from ..utils.progress_dialog import ProgressDialog
class ImportCloudProjectDialog(QtGui.QDialog, Ui_ImportCloudProjectDialog):
"""
Import cloud project dialog implementation.
"""
def __init__(self, parent, project_dest_path, images_dest_path, cloud_settings):
QtGui.QDialog.__init__(self, parent)
self.setupUi(self)
self.project_dest_path = project_dest_path
self.images_dest_path = images_dest_path
self.cloud_settings = cloud_settings
self.uiImportProjectAction.clicked.connect(self._importProject)
self.uiDeleteProjectAction.clicked.connect(self._deleteProject)
self._listCloudProjects()
def _listCloudProjects(self):
self.listWidget.clear()
self.projects = get_cloud_projects(self.cloud_settings)
self.listWidget.addItems(list(self.projects.keys()))
def _importProject(self):
project_file_name = self.projects[self.listWidget.currentItem().text()]
download_thread = DownloadProjectThread(
self,
project_file_name,
self.project_dest_path,
self.images_dest_path,
self.cloud_settings
)
progress_dialog = ProgressDialog(download_thread, "Importing project", "Downloading project files...", "Cancel",
parent=self.parent())
progress_dialog.show()
progress_dialog.exec_()
self.close()
def _deleteProject(self):
project_file_name = self.projects[self.listWidget.currentItem().text()]
button_clicked = QtGui.QMessageBox.question(
self,
"Delete project",
"Are you sure you want to delete project " + self.listWidget.currentItem().text(),
QtGui.QMessageBox.Yes | QtGui.QMessageBox.No,
QtGui.QMessageBox.Yes
)
if button_clicked == QtGui.QMessageBox.Yes:
delete_project_thread = DeleteProjectThread(self, project_file_name, self.cloud_settings)
progress_dialog = ProgressDialog(delete_project_thread, "Deleting project", "Deleting project files...",
"Cancel", parent=self)
progress_dialog.show()
progress_dialog.exec_()
self._listCloudProjects()

View File

@@ -19,14 +19,19 @@ import os
import shutil
from ..qt import QtCore, QtGui
from ..ui.new_project_dialog_ui import Ui_NewProjectDialog
from ..settings import ENABLE_CLOUD
class NewProjectDialog(QtGui.QDialog, Ui_NewProjectDialog):
"""
New project dialog.
:param parent: parent widget.
:param showed_from_startup: boolean to indicate if this dialog
has been opened automatically when GNS3 started.
"""
def __init__(self, parent):
def __init__(self, parent, showed_from_startup=False):
QtGui.QDialog.__init__(self, parent)
self.setupUi(self)
@@ -39,6 +44,14 @@ class NewProjectDialog(QtGui.QDialog, Ui_NewProjectDialog):
self.uiNameLineEdit.textEdited.connect(self._projectNameSlot)
self.uiLocationBrowserToolButton.clicked.connect(self._projectPathSlot)
self.uiOpenProjectPushButton.clicked.connect(self._openProjectActionSlot)
self.uiRecentProjectsPushButton.clicked.connect(self._showRecentProjectsSlot)
if not ENABLE_CLOUD:
self.uiCloudRadioButton.hide()
if not showed_from_startup:
self.uiOpenProjectPushButton.hide()
self.uiRecentProjectsPushButton.hide()
def keyPressEvent(self, e):
"""
@@ -68,6 +81,35 @@ class NewProjectDialog(QtGui.QDialog, Ui_NewProjectDialog):
return self._project_settings
def _menuTriggeredSlot(self, action):
"""
Closes this dialog when a recent project
has been opened.
:param action: ignored.
"""
self.reject()
def _openProjectActionSlot(self):
"""
Opens a project and closes this dialog.
"""
self._main_window.openProjectActionSlot()
self.reject()
def _showRecentProjectsSlot(self):
"""
lot to show all the recent projects in a menu.
"""
menu = QtGui.QMenu()
menu.triggered.connect(self._menuTriggeredSlot)
for action in self._main_window._recent_file_actions:
menu.addAction(action)
menu.exec_(QtGui.QCursor.pos())
def done(self, result):
if result:

View File

@@ -26,6 +26,7 @@ from ..pages.general_preferences_page import GeneralPreferencesPage
from ..pages.cloud_preferences_page import CloudPreferencesPage
from ..pages.packet_capture_preferences_page import PacketCapturePreferencesPage
from ..modules import MODULES
from ..settings import ENABLE_CLOUD
class PreferencesDialog(QtGui.QDialog, Ui_PreferencesDialog):
@@ -58,8 +59,9 @@ class PreferencesDialog(QtGui.QDialog, Ui_PreferencesDialog):
GeneralPreferencesPage,
ServerPreferencesPage,
PacketCapturePreferencesPage,
#CloudPreferencesPage,
]
if ENABLE_CLOUD:
pages.append(CloudPreferencesPage)
for page in pages:
preferences_page = page()
@@ -87,6 +89,9 @@ class PreferencesDialog(QtGui.QDialog, Ui_PreferencesDialog):
if cls is preference_pages[0]:
parent = item
# expand all items by default
self.uiTreeWidget.expandAll()
def _showPreferencesPageSlot(self, current, previous):
"""
Shows a preference page in the current dialog.

View File

@@ -32,7 +32,7 @@ class SymbolSelectionDialog(QtGui.QDialog, Ui_SymbolSelectionDialog):
:param items: list of items
"""
def __init__(self, parent, items=None):
def __init__(self, parent, items=None, symbol=None, category=None):
QtGui.QDialog.__init__(self, parent)
self.setupUi(self)
@@ -40,6 +40,8 @@ class SymbolSelectionDialog(QtGui.QDialog, Ui_SymbolSelectionDialog):
self._items = items
self.uiButtonBox.button(QtGui.QDialogButtonBox.Apply).clicked.connect(self._applyPreferencesSlot)
selected_symbol = symbol
selected_category = category
if not self._items:
self.uiButtonBox.button(QtGui.QDialogButtonBox.Apply).hide()
@@ -50,20 +52,39 @@ class SymbolSelectionDialog(QtGui.QDialog, Ui_SymbolSelectionDialog):
"Security devices": Node.security_devices
}
index = 0
for name, category in categories.items():
self.uiCategoryComboBox.addItem(name, category)
if category == selected_category:
self.uiCategoryComboBox.setCurrentIndex(index)
index += 1
else:
self.uiCategoryLabel.hide()
self.uiCategoryComboBox.hide()
custom_symbol = items[0].defaultRenderer().objectName()
if not custom_symbol:
symbol_name = items[0].node().defaultSymbol()
else:
symbol_name = custom_symbol
selected_symbol = symbol_name
self.uiSymbolListWidget.setIconSize(QtCore.QSize(64, 64))
symbol_resources = QtCore.QResource(":/symbols")
for symbol in symbol_resources.children():
if symbol.endswith('.normal.svg'):
if symbol.endswith(".normal.svg"):
name = symbol[:-11]
item = QtGui.QListWidgetItem(self.uiSymbolListWidget)
item.setText(name)
item.setIcon(QtGui.QIcon(':/symbols/' + symbol))
resource_path = ":/symbols/" + symbol
svg_renderer = QtSvg.QSvgRenderer(resource_path)
if resource_path == selected_symbol:
self.uiSymbolListWidget.setCurrentItem(item)
image = QtGui.QImage(64, 64, QtGui.QImage.Format_ARGB32)
# Set the ARGB to 0 to prevent rendering artifacts
image.fill(0x00000000)
svg_renderer.render(QtGui.QPainter(image))
icon = QtGui.QIcon(QtGui.QPixmap.fromImage(image))
item.setIcon(icon)
def _applyPreferencesSlot(self):
"""

View File

@@ -53,6 +53,10 @@ class TextEditorDialog(QtGui.QDialog, Ui_TextEditorDialog):
if not first_item.editable():
self.uiPlainTextEdit.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)
if len(self._items) == 1:
self.uiApplyTextToAllItemsCheckBox.setChecked(True)
self.uiApplyTextToAllItemsCheckBox.hide()
def _setFontSlot(self):
"""
Slot to select the font.
@@ -82,7 +86,7 @@ class TextEditorDialog(QtGui.QDialog, Ui_TextEditorDialog):
item.setDefaultTextColor(self._color)
item.setFont(self.uiPlainTextEdit.font())
item.setRotation(self.uiRotationSpinBox.value())
if item.editable():
if item.editable() and self.uiApplyTextToAllItemsCheckBox.isChecked():
item.setPlainText(self.uiPlainTextEdit.toPlainText())
def done(self, result):

View File

@@ -19,6 +19,7 @@
Graphical view on the scene where items are drawn.
"""
import logging
import os
import pickle
@@ -52,6 +53,8 @@ from .items.rectangle_item import RectangleItem
from .items.ellipse_item import EllipseItem
from .items.image_item import ImageItem
log = logging.getLogger(__name__)
class GraphicsView(QtGui.QGraphicsView):
"""
@@ -605,23 +608,8 @@ class GraphicsView(QtGui.QGraphicsView):
item = self.itemAt(event.pos())
if not self._adding_link and isinstance(item, NodeItem) and item.node().initialized():
item.setSelected(True)
if isinstance(item, NodeItem) and hasattr(item.node(), "console") and item.node().initialized() and item.node().status() == Node.started:
if hasattr(item.node(), "serialConsole") and item.node().serialConsole():
try:
from .serial_console import serialConsole
serialConsole(item.node().name())
except (OSError, ValueError) as e:
QtGui.QMessageBox.critical(self, "Console", "Cannot start serial console application: {}".format(e))
else:
node = item.node()
name = node.name()
console_port = node.console()
console_host = node.server().host
try:
from .telnet_console import telnetConsole
telnetConsole(name, console_host, console_port)
except (OSError, ValueError) as e:
QtGui.QMessageBox.critical(self, "Console", "Cannot start console application: {}".format(e))
if isinstance(item, NodeItem):
self.consoleToNode(item.node())
else:
self.configureSlot()
else:
@@ -724,19 +712,42 @@ class GraphicsView(QtGui.QGraphicsView):
configure_action.triggered.connect(self.configureActionSlot)
menu.addAction(configure_action)
# Action: Change hostname
change_hostname_action = QtGui.QAction("Change hostname", menu)
change_hostname_action.setIcon(QtGui.QIcon(':/icons/show-hostname.svg'))
self.connect(change_hostname_action, QtCore.SIGNAL('triggered()'), self.changeHostnameActionSlot)
menu.addAction(change_hostname_action)
# Action: Change symbol
change_symbol_action = QtGui.QAction("Change symbol", menu)
change_symbol_action.setIcon(QtGui.QIcon(':/icons/node_conception.svg'))
self.connect(change_symbol_action, QtCore.SIGNAL('triggered()'), self.changeSymbolActionSlot)
menu.addAction(change_symbol_action)
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "console"), items)):
console_action = QtGui.QAction("Console", menu)
console_action.setIcon(QtGui.QIcon(':/icons/console.svg'))
console_action.triggered.connect(self.consoleActionSlot)
menu.addAction(console_action)
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "auxConsole"), items)):
aux_console_action = QtGui.QAction("Auxiliary console", menu)
aux_console_action.setIcon(QtGui.QIcon(':/icons/aux-console.svg'))
aux_console_action.triggered.connect(self.auxConsoleActionSlot)
menu.addAction(aux_console_action)
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "importConfig"), items)):
import_config_action = QtGui.QAction("Import config", menu)
import_config_action.setIcon(QtGui.QIcon(':/icons/import_config.svg'))
import_config_action.triggered.connect(self.importConfigActionSlot)
menu.addAction(import_config_action)
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "exportConfig"), items)):
export_config_action = QtGui.QAction("Export config", menu)
export_config_action.setIcon(QtGui.QIcon(':/icons/export_config.svg'))
export_config_action.triggered.connect(self.exportConfigActionSlot)
menu.addAction(export_config_action)
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "startPacketCapture"), items)):
capture_action = QtGui.QAction("Capture", menu)
capture_action.setIcon(QtGui.QIcon(':/icons/inspect.svg'))
@@ -751,7 +762,7 @@ class GraphicsView(QtGui.QGraphicsView):
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "start"), items)):
start_action = QtGui.QAction("Start", menu)
start_action.setIcon(QtGui.QIcon(':/icons/play.svg'))
start_action.setIcon(QtGui.QIcon(':/icons/start.svg'))
start_action.triggered.connect(self.startActionSlot)
menu.addAction(start_action)
@@ -863,6 +874,22 @@ class GraphicsView(QtGui.QGraphicsView):
if items:
self.configureSlot(items)
def changeHostnameActionSlot(self):
"""
Slot to receive events from the change hostname action in the
contextual menu.
"""
for item in self.scene().selectedItems():
if isinstance(item, NodeItem) and item.node().initialized():
new_hostname, ok = QtGui.QInputDialog.getText(self, "Change hostname", "Hostname:", QtGui.QLineEdit.Normal, item.node().name())
if ok:
if hasattr(item.node(), "validateHostname"):
if not item.node().validateHostname(new_hostname):
QtGui.QMessageBox.critical(self, "Change hostname", "Invalid name detected for this node: {}".format(new_hostname))
continue
item.node().update({"name": new_hostname})
def changeSymbolActionSlot(self):
"""
Slot to receive events from the change symbol action in the
@@ -878,33 +905,152 @@ class GraphicsView(QtGui.QGraphicsView):
dialog.show()
dialog.exec_()
def consoleToNode(self, node, aux=False):
"""
Start a console application to connect to a node.
:param node: Node instance
:param aux: auxiliary console mode
:returns: False if the console application could not be started
"""
if not hasattr(node, "console") or not node.initialized() or node.status() != Node.started:
# returns True to ignore this node.
return True
if aux and not hasattr(node, "auxConsole"):
# returns True to ignore this node.
return True
if hasattr(node, "serialConsole") and node.serialConsole():
try:
from .serial_console import serialConsole
serialConsole(node.name())
except (OSError, ValueError) as e:
QtGui.QMessageBox.critical(self, "Console", "Cannot start serial console application: {}".format(e))
return False
else:
name = node.name()
if aux:
console_port = node.auxConsole()
else:
console_port = node.console()
console_host = node.server().host
try:
from .telnet_console import telnetConsole
telnet_callback = None
try:
if node.server().isCloud():
# override target host with localhost
console_host = '127.0.0.1'
ep = node.server().tunnel.add_endpoint(console_host, console_port)
# override target port with local tunneled port
local_addr, _ = ep.get()
console_port = local_addr[1]
def cb(*args, **kwargs):
node.server().tunnel.remove_endpoint(ep)
log.debug('Console DONE on port {}'.format(args[2]))
telnet_callback = cb
except AttributeError:
pass
telnetConsole(name, console_host, console_port, telnet_callback)
except (OSError, ValueError) as e:
QtGui.QMessageBox.critical(self, "Console", "Cannot start console application: {}".format(e))
return False
return True
def consoleActionSlot(self):
"""
Slot to receive events from the console action in the
contextual menu.
"""
from .telnet_console import telnetConsole
for item in self.scene().selectedItems():
if isinstance(item, NodeItem) and hasattr(item.node(), "console") and item.node().initialized():
if hasattr(item.node(), "serialConsole") and item.node().serialConsole():
try:
from .serial_console import serialConsole
serialConsole(item.node().name())
except (OSError, ValueError) as e:
QtGui.QMessageBox.critical(self, "Console", "Cannot start serial console application: {}".format(e))
else:
node = item.node()
if node.status() != Node.started:
continue
name = node.name()
console_port = node.console()
console_host = node.server().host
try:
telnetConsole(name, console_host, console_port)
except (OSError, ValueError) as e:
QtGui.QMessageBox.critical(self, "Console", "Cannot start console application: {}".format(e))
break
if isinstance(item, NodeItem):
if self.consoleToNode(item.node()):
continue
def auxConsoleActionSlot(self):
"""
Slot to receive events from the auxiliary console action in the
contextual menu.
"""
for item in self.scene().selectedItems():
if isinstance(item, NodeItem):
if self.consoleToNode(item.node(), aux=True):
continue
def importConfigActionSlot(self):
"""
Slot to receive events from the import config action in the
contextual menu.
"""
items = []
for item in self.scene().selectedItems():
if isinstance(item, NodeItem) and hasattr(item.node(), "importConfig") and item.node().initialized():
items.append(item)
if not items:
return
if len(items) > 1:
path = QtGui.QFileDialog.getExistingDirectory(self, "Import directory", ".", QtGui.QFileDialog.ShowDirsOnly)
if path:
for item in items:
item.node().importConfigFromDirectory(path)
else:
item = items[0]
path, _ = QtGui.QFileDialog.getOpenFileNameAndFilter(self,
"Import config",
".",
"All files (*.*);;Config files (*.cfg)",
"Config files (*.cfg)")
if path:
item.node().importConfig(path)
if hasattr(item.node(), "importPrivateConfig"):
path, _ = QtGui.QFileDialog.getOpenFileNameAndFilter(self,
"Import private-config",
".",
"All files (*.*);;Config files (*.cfg)",
"Config files (*.cfg)")
if path:
item.node().importPrivateConfig(path)
def exportConfigActionSlot(self):
"""
Slot to receive events from the export config action in the
contextual menu.
"""
items = []
for item in self.scene().selectedItems():
if isinstance(item, NodeItem) and hasattr(item.node(), "exportConfig") and item.node().initialized():
items.append(item)
if not items:
return
if len(items) > 1:
path = QtGui.QFileDialog.getExistingDirectory(self, "Export directory", ".", QtGui.QFileDialog.ShowDirsOnly)
if path:
for item in items:
item.node().exportConfigToDirectory(path)
else:
item = items[0]
config_path = QtGui.QFileDialog.getSaveFileName(self, "Export config")
if hasattr(item.node(), "importPrivateConfig"):
private_config_path = QtGui.QFileDialog.getSaveFileName(self, "Export private-config")
item.node().exportConfig(config_path, private_config_path)
else:
item.node().exportConfig(config_path)
def captureActionSlot(self):
"""
@@ -1104,6 +1250,7 @@ class GraphicsView(QtGui.QGraphicsView):
"""
try:
log.debug('In createNode')
node_module = None
for module in MODULES:
instance = module.instance()
@@ -1119,9 +1266,18 @@ class GraphicsView(QtGui.QGraphicsView):
server = node_module.allocateServer(node_class)
elif node_data["server"] == "local":
server = Servers.instance().localServer()
elif node_data["server"] == "cloud":
server = Servers.instance().anyCloudServer()
else:
host, port = node_data["server"].rsplit(":", 1)
try:
host, port = node_data["server"].rsplit(":", 1)
except ValueError:
raise ModuleError("Wrong format for server: '{}', please recreate the node in preferences".format(node_data["server"]))
server = Servers.instance().getRemoteServer(host, port)
if server is None:
return
if not server.connected() and ConnectToServer(self, server) is False:
return

View File

@@ -224,7 +224,10 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
when a the node has been updated.
"""
self._node_label.setPlainText(self._node.name())
if self._node_label:
if self._node_label.toPlainText() != self._node.name():
self._node_label.setPlainText(self._node.name())
self._centerLabel()
self.setUnsavedState()
# update the link tooltips in case the
@@ -307,6 +310,19 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
self._node_label = label
def _centerLabel(self):
"""
Centers the node label.
"""
text_rect = self._node_label.boundingRect()
text_middle = text_rect.topRight() / 2
node_rect = self.boundingRect()
node_middle = node_rect.topRight() / 2
label_x_pos = node_middle.x() - text_middle.x()
label_y_pos = -25
self._node_label.setPos(label_x_pos, label_y_pos)
def _showLabel(self):
"""
Shows the node label on the scene.
@@ -316,13 +332,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
self._node_label = NoteItem(self)
self._node_label.setEditable(False)
self._node_label.setPlainText(self._node.name())
text_rect = self._node_label.boundingRect()
text_middle = text_rect.topRight() / 2
node_rect = self.boundingRect()
node_middle = node_rect.topRight() / 2
label_x_pos = node_middle.x() - text_middle.x()
label_y_pos = -25
self._node_label.setPos(label_x_pos, label_y_pos)
self._centerLabel()
def connectToPort(self, unavailable_ports=[]):
"""
@@ -340,29 +350,33 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
QtGui.QMessageBox.critical(self.scene().parent(), "Link", "No port available, please configure this device")
return None
# sort by port name
port_names = {}
# sort the ports
ports_dict = {}
for port in ports:
port_names[port.name()] = port
if port.slotNumber() is not None:
# multiply the slot number by 16 to make the port number unique.
ports_dict[(port.slotNumber() * 16) + port.portNumber()] = port
elif port.portNumber()is not None:
ports_dict[port.portNumber()] = port
else:
ports_dict[port.name()] = port
try:
# try a numeric sort first
ports = sorted(port_names.keys(), key=int)
ports = sorted(ports_dict.keys(), key=int)
except ValueError:
# fall back to a classic sort
ports = sorted(port_names.keys())
ports = sorted(ports_dict.keys())
# show a contextual menu for the user to choose a port
for port in ports:
port_object = port_names[port]
port_object = ports_dict[port]
if port in unavailable_ports:
# this port cannot be chosen by the user (grayed out)
action = menu.addAction(QtGui.QIcon(':/icons/led_green.svg'), port)
action = menu.addAction(QtGui.QIcon(':/icons/led_green.svg'), port_object.name())
action.setDisabled(True)
elif port_object.isFree():
menu.addAction(QtGui.QIcon(':/icons/led_red.svg'), port)
menu.addAction(QtGui.QIcon(':/icons/led_red.svg'), port_object.name())
else:
menu.addAction(QtGui.QIcon(':/icons/led_green.svg'), port)
menu.addAction(QtGui.QIcon(':/icons/led_green.svg'), port_object.name())
menu.triggered.connect(self.selectedPortSlot)
menu.exec_(QtGui.QCursor.pos())

View File

@@ -280,7 +280,7 @@ class ShapeItem:
color = QtGui.QColor(color)
else:
color = QtGui.QColor(255, 255, 255)
if transparency:
if transparency is not None:
color.setAlpha(transparency)
self.setBrush(QtGui.QBrush(color))
@@ -293,8 +293,8 @@ class ShapeItem:
border_color.setAlpha(border_transparency)
pen.setColor(border_color)
if border_width is not None:
pen.setWidth(border_width)
if border_style:
pen.setWidth(int(border_width))
if border_style is not None:
pen.setStyle(QtCore.Qt.PenStyle(border_style))
self.setPen(pen)

View File

@@ -82,11 +82,15 @@ def main():
"""
parser = argparse.ArgumentParser()
parser.add_argument('--version', help="show the version", action='version', version=__version__)
parser.add_argument('--debug', help="print out debug messages", action='store_true', default=False)
parser.add_argument("project", help="load a GNS3 project (.gns3)", metavar="path", nargs="?")
parser.add_argument("--version", help="show the version", action="version", version=__version__)
parser.add_argument("--debug", help="print out debug messages", action="store_true", default=False)
options = parser.parse_args()
exception_file_path = "exception.log"
if options.project and hasattr(sys, "frozen"):
os.chdir(os.path.dirname(os.path.abspath(sys.executable)))
def exceptionHook(exception, value, tb):
if exception == KeyboardInterrupt:
@@ -103,7 +107,7 @@ def main():
logfile.write("".join(lines))
logfile.close()
except OSError as e:
print("Could not save traceback to {}: {}".format(exception_file_path, e))
print("Could not save traceback to {}: {}".format(os.path.normpath(exception_file_path), e))
if not sys.stdout.isatty():
# if stdout is not a tty (redirected to the console view),
@@ -156,7 +160,7 @@ def main():
if sys.platform.startswith('win') or sys.platform.startswith('darwin'):
QtCore.QSettings.setDefaultFormat(QtCore.QSettings.IniFormat)
if sys.platform.startswith('win'):
if sys.platform.startswith('win') and hasattr(sys, "frozen"):
try:
import win32console
import win32con
@@ -165,56 +169,56 @@ def main():
raise RuntimeError("Python for Windows extensions must be installed.")
try:
win32console.AllocConsole()
# hide the console
console_window = win32console.GetConsoleWindow()
win32gui.ShowWindow(console_window, win32con.SW_HIDE)
except win32console.error as e:
print("warning: could not allocate console: {}".format(e))
exit_code = MainWindow.exit_code_reboot
while exit_code == MainWindow.exit_code_reboot:
app = QtGui.QApplication(sys.argv)
exit_code = 0
app = QtGui.QApplication(sys.argv)
# this info is necessary for QSettings
app.setOrganizationName("GNS3")
app.setOrganizationDomain("gns3.net")
app.setApplicationName("GNS3")
app.setApplicationVersion(__version__)
# this info is necessary for QSettings
app.setOrganizationName("GNS3")
app.setOrganizationDomain("gns3.net")
app.setApplicationName("GNS3")
app.setApplicationVersion(__version__)
# save client logging info to a file
logfile = os.path.join(os.path.dirname(QtCore.QSettings().fileName()), "GNS3_client.log")
# save client logging info to a file
logfile = os.path.join(os.path.dirname(QtCore.QSettings().fileName()), "GNS3_client.log") # FIXME: does it work?
try:
try:
try:
os.makedirs(os.path.dirname(QtCore.QSettings().fileName()))
except FileExistsError:
pass
handler = logging.FileHandler(logfile, "w")
if options.debug:
root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG)
os.makedirs(os.path.dirname(QtCore.QSettings().fileName()))
except FileExistsError:
pass
handler = logging.FileHandler(logfile, "w")
if options.debug:
root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG)
if len(root_logger.handlers) > 0:
root_handler = root_logger.handlers[0]
root_handler.setLevel(logging.DEBUG)
else:
handler.setLevel(logging.INFO)
log.info('Log level: {}'.format(logging.getLevelName(log.getEffectiveLevel())))
root_handler = logging.StreamHandler()
root_logger.addHandler(root_handler)
root_handler.setLevel(logging.DEBUG)
else:
handler.setLevel(logging.INFO)
log.info('Log level: {}'.format(logging.getLevelName(log.getEffectiveLevel())))
formatter = logging.Formatter("[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d] %(message)s",
datefmt="%y%m%d %H:%M:%S")
handler.setFormatter(formatter)
log.addHandler(handler)
except OSError as e:
log.warn("could not log to {}: {}".format(logfile, e))
formatter = logging.Formatter("[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d] %(message)s",
datefmt="%y%m%d %H:%M:%S")
handler.setFormatter(formatter)
log.addHandler(handler)
except OSError as e:
log.warn("could not log to {}: {}".format(logfile, e))
# update the exception file path to have it in the same directory as the settings file.
exception_file_path = os.path.join(os.path.dirname(QtCore.QSettings().fileName()), exception_file_path)
# update the exception file path to have it in the same directory as the settings file.
exception_file_path = os.path.join(os.path.dirname(QtCore.QSettings().fileName()), exception_file_path)
mainwindow = MainWindow.instance()
mainwindow.show()
exit_code = app.exec_()
delattr(MainWindow, "_instance")
app.deleteLater()
mainwindow = MainWindow(options.project)
mainwindow.show()
exit_code = app.exec_()
delattr(MainWindow, "_instance")
app.deleteLater()
sys.exit(exit_code)

File diff suppressed because it is too large Load Diff

View File

@@ -25,6 +25,7 @@ from gns3.servers import Servers
from ..module import Module
from ..module_error import ModuleError
from .cloud import Cloud
from .host import Host
import logging
@@ -241,7 +242,7 @@ class Builtin(Module):
:returns: list of classes
"""
return [Cloud]
return [Cloud, Host]
def nodes(self):
"""

View File

@@ -211,53 +211,55 @@ class Cloud(Node):
:param new_settings: settings dictionary
"""
nios = new_settings["nios"]
updated = False
# add ports
for nio in nios:
if nio in self._settings["nios"]:
# port already created for this NIO
continue
nio_object = None
if nio.lower().startswith("nio_udp"):
nio_object = self._createNIOUDP(nio)
if nio.lower().startswith("nio_gen_eth"):
nio_object = self._createNIOGenericEthernet(nio)
if nio.lower().startswith("nio_gen_linux"):
nio_object = self._createNIOLinuxEthernet(nio)
if nio.lower().startswith("nio_tap"):
nio_object = self._createNIOTAP(nio)
if nio.lower().startswith("nio_unix"):
nio_object = self._createNIOUNIX(nio)
if nio.lower().startswith("nio_vde"):
nio_object = self._createNIOVDE(nio)
if nio.lower().startswith("nio_null"):
nio_object = self._createNIONull(nio)
if nio_object == None:
log.error("Could not create NIO object from {}".format(nio))
continue
port = Port(nio, nio_object, stub=True)
port.setStatus(Port.started)
self._ports.append(port)
updated = True
log.debug("port {} has been added".format(nio))
if "nios" in new_settings:
nios = new_settings["nios"]
# delete ports
for nio in self._settings["nios"]:
if nio not in nios:
for port in self._ports.copy():
if port.name() == nio:
self._ports.remove(port)
updated = True
log.debug("port {} has been deleted".format(nio))
break
# add ports
for nio in nios:
if nio in self._settings["nios"]:
# port already created for this NIO
continue
nio_object = None
if nio.lower().startswith("nio_udp"):
nio_object = self._createNIOUDP(nio)
if nio.lower().startswith("nio_gen_eth"):
nio_object = self._createNIOGenericEthernet(nio)
if nio.lower().startswith("nio_gen_linux"):
nio_object = self._createNIOLinuxEthernet(nio)
if nio.lower().startswith("nio_tap"):
nio_object = self._createNIOTAP(nio)
if nio.lower().startswith("nio_unix"):
nio_object = self._createNIOUNIX(nio)
if nio.lower().startswith("nio_vde"):
nio_object = self._createNIOVDE(nio)
if nio.lower().startswith("nio_null"):
nio_object = self._createNIONull(nio)
if nio_object is None:
log.error("Could not create NIO object from {}".format(nio))
continue
port = Port(nio, nio_object, stub=True)
port.setStatus(Port.started)
self._ports.append(port)
updated = True
log.debug("port {} has been added".format(nio))
# delete ports
for nio in self._settings["nios"]:
if nio not in nios:
for port in self._ports.copy():
if port.name() == nio:
self._ports.remove(port)
updated = True
log.debug("port {} has been deleted".format(nio))
break
self._settings["nios"] = new_settings["nios"].copy()
if "name" in new_settings and new_settings["name"] != self.name():
self._settings["name"] = new_settings["name"]
updated = True
self._settings["nios"] = new_settings["nios"].copy()
if updated:
log.info("cloud {} has been updated".format(self.name()))
self.updated_signal.emit()

View File

@@ -0,0 +1,116 @@
# -*- 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 gns3.node import Node
from .cloud import Cloud
import logging
log = logging.getLogger(__name__)
class Host(Cloud):
"""
Pseudo host based on a Dynamips Cloud.
:param module: parent module for this node
:param server: GNS3 server instance
"""
_name_instance_count = 1
def __init__(self, module, server):
Cloud.__init__(self, module, server)
log.info("host is being created")
# create an unique id and name
self._name_id = Host._name_instance_count
Host._name_instance_count += 1
name = "Host{}".format(self._name_id)
self._settings["name"] = name
def setup(self, name=None, initial_settings={}):
"""
Setups this host.
:param name: optional name for this host
"""
if name:
self._settings["name"] = name
if initial_settings:
self._initial_settings = initial_settings
else:
self.created_signal.connect(self._autoConfigure)
self._server.send_message("builtin.interfaces", None, self._setupCallback)
def _autoConfigure(self, node_id):
"""
Auto adds all Ethernet and TAP interfaces.
:param node_id: ignored
"""
new_settings = {"nios": []}
for interface in self._settings["interfaces"]:
if interface["name"].startswith("tap"):
new_settings["nios"].append("nio_tap:{}".format(interface["name"]))
else:
new_settings["nios"].append("nio_gen_eth:{}".format(interface["name"]))
self.update(new_settings)
@staticmethod
def defaultSymbol():
"""
Returns the default symbol path for this host.
:returns: symbol path (or resource).
"""
return ":/symbols/computer.normal.svg"
@staticmethod
def hoverSymbol():
"""
Returns the symbol to use when the host is hovered.
:returns: symbol path (or resource).
"""
return ":/symbols/computer.selected.svg"
@staticmethod
def symbolName():
return "Host"
@staticmethod
def categories():
"""
Returns the node categories the node is part of (used by the device panel).
:returns: list of node category (integer)
"""
return [Node.end_devices]
def __str__(self):
return "Host"

View File

@@ -474,6 +474,9 @@ class CloudConfigurationPage(QtGui.QWidget, Ui_cloudConfigPageWidget):
self.uiGenericEthernetComboBox.clear()
index = 0
for interface in settings["interfaces"]:
if interface["name"].startswith("tap"):
# do not add TAP interfaces
continue
self.uiGenericEthernetComboBox.addItem(interface["name"])
self.uiGenericEthernetComboBox.setItemData(index, interface["id"], QtCore.Qt.ToolTipRole)
index += 1
@@ -483,7 +486,7 @@ class CloudConfigurationPage(QtGui.QWidget, Ui_cloudConfigPageWidget):
self.uiLinuxEthernetComboBox.clear()
index = 0
for interface in settings["interfaces"]:
if not interface["name"].startswith(r"\Device\NPF_"):
if not interface["name"].startswith(r"\Device\NPF_") and not interface["name"].startswith("tap"):
self.uiLinuxEthernetComboBox.addItem(interface["name"])
self.uiLinuxEthernetComboBox.setItemData(index, interface["id"], QtCore.Qt.ToolTipRole)
index += 1

View File

@@ -24,7 +24,6 @@ import glob
from gns3.qt import QtCore, QtGui
from gns3.servers import Servers
from gns3.node import Node
from ..module import Module
from ..module_error import ModuleError
@@ -41,7 +40,9 @@ from .nodes.ethernet_switch import EthernetSwitch
from .nodes.ethernet_hub import EthernetHub
from .nodes.frame_relay_switch import FrameRelaySwitch
from .nodes.atm_switch import ATMSwitch
from .settings import DYNAMIPS_SETTINGS, DYNAMIPS_SETTING_TYPES, PLATFORMS_DEFAULT_RAM
from .settings import DYNAMIPS_SETTINGS, DYNAMIPS_SETTING_TYPES
from .settings import IOS_ROUTER_SETTINGS, IOS_ROUTER_SETTING_TYPES
from .settings import PLATFORMS_DEFAULT_RAM
PLATFORM_TO_CLASS = {
"c1700": C1700,
@@ -111,54 +112,18 @@ class Dynamips(Module):
settings = QtCore.QSettings()
settings.beginGroup("IOSRouters")
#FIXME: this is ugly
# load the IOS images
size = settings.beginReadArray("ios_router")
for index in range(0, size):
settings.setArrayIndex(index)
name = settings.value("name", "")
path = settings.value("path", "")
image = settings.value("image", "")
startup_config = settings.value("startup_config", "")
private_config = settings.value("private_config", "")
default_symbol = settings.value("default_symbol", ":/symbols/router.normal.svg")
hover_symbol = settings.value("hover_symbol", ":/symbols/router.selected.svg")
category = settings.value("category", Node.routers, type=int)
platform = settings.value("platform", "")
chassis = settings.value("chassis", "")
idlepc = settings.value("idlepc", "")
mac_addr = settings.value("mac_addr", "")
ram = settings.value("ram", 128, type=int)
nvram = settings.value("nvram", 256, type=int)
disk0 = settings.value("disk0", 16, type=int)
disk1 = settings.value("disk1", 0, type=int)
confreg = settings.value("confreg", "0x2102")
system_id = settings.value("system_id", "FTX0945W0MY")
server = settings.value("server", "local")
name = settings.value("name")
server = settings.value("server")
key = "{server}:{name}".format(server=server, name=name)
if key in self._ios_routers:
if key in self._ios_routers or not name or not server:
continue
self._ios_routers[key] = {"name": name,
"path": path,
"image": image,
"default_symbol": default_symbol,
"hover_symbol": hover_symbol,
"category": category,
"startup_config": startup_config,
"private_config": private_config,
"platform": platform,
"chassis": chassis,
"idlepc": idlepc,
"ram": ram,
"nvram": nvram,
"mac_addr": mac_addr,
"disk0": disk0,
"disk1": disk1,
"confreg": confreg,
"system_id": system_id,
"server": server}
self._ios_routers[key] = {}
for setting_name, default_value in IOS_ROUTER_SETTINGS.items():
self._ios_routers[key][setting_name] = settings.value(setting_name, default_value, IOS_ROUTER_SETTING_TYPES[setting_name])
for slot_id in range(0, 7):
slot = "slot{}".format(slot_id)
@@ -170,10 +135,13 @@ class Dynamips(Module):
if settings.contains(wic):
self._ios_routers[key][wic] = settings.value(wic, "")
platform = self._ios_routers[key]["platform"]
chassis = self._ios_routers[key]["chassis"]
if platform == "c7200":
self._ios_routers[key]["midplane"] = settings.value("midplane", "vxr")
self._ios_routers[key]["npe"] = settings.value("npe", "npe-400")
self._ios_routers[key]["slot0"] = settings.value("slot0", "C7200-IO-2FE")
self._ios_routers[key]["slot0"] = settings.value("slot0", "C7200-IO-FE")
else:
self._ios_routers[key]["iomem"] = 5
@@ -364,6 +332,8 @@ class Dynamips(Module):
if server.isLocal():
params.update({"working_dir": self._working_dir})
else:
if "path" in params:
del params["path"] # do not send Dynamips path to remote servers
project_name = os.path.basename(self._working_dir)
if project_name.endswith("-files"):
project_name = project_name[:-6]
@@ -386,12 +356,42 @@ class Dynamips(Module):
if server.isLocal():
params.update({"working_dir": self._working_dir})
else:
if "path" in params:
del params["path"] # do not send Dynamips path to remote servers
project_name = os.path.basename(self._working_dir)
if project_name.endswith("-files"):
project_name = project_name[:-6]
params.update({"project_name": project_name})
server.send_notification("dynamips.settings", params)
def allocateServer(self, node_class, use_cloud=False):
"""
Allocates a server.
:param node_class: Node object
:returns: allocated server (WebSocketClient instance)
"""
# allocate a server for the node
servers = Servers.instance()
if use_cloud:
from ...topology import Topology
topology = Topology.instance()
top_instance = topology.anyInstance()
server = servers.getCloudServer(top_instance.host, top_instance.port, top_instance.ssl_ca_file)
else:
if self._settings["use_local_server"]:
# use the local server
server = servers.localServer()
else:
# pick up a remote server (round-robin method)
server = next(iter(servers))
if not server:
raise ModuleError("No remote server is configured")
return server
def createNode(self, node_class, server):
"""
Creates a new node.
@@ -435,19 +435,19 @@ class Dynamips(Module):
ios_router = self._ios_routers[ios_key]
break
# hack for EtherSwitch router
if isinstance(node, EtherSwitchRouter) and node.server() == Servers.instance().localServer():
for info in self._ios_routers.values():
if info["platform"] == "c3725" and info["server"] == "local":
ios_router = {
"platform": "c3725",
"path": info["path"],
"ram": info["ram"],
"startup_config": info["startup_config"],
}
break
if not ios_router:
raise ModuleError("Please create an c3725 IOS router in order to use an EtherSwitch router")
# # hack for EtherSwitch router
# if isinstance(node, EtherSwitchRouter) and node.server() == Servers.instance().localServer():
# for info in self._ios_routers.values():
# if info["platform"] == "c3725" and info["server"] == "local":
# ios_router = {
# "platform": "c3725",
# "path": info["path"],
# "ram": info["ram"],
# "startup_config": info["startup_config"],
# }
# break
# if not ios_router:
# raise ModuleError("Please create an c3725 IOS router in order to use an EtherSwitch router")
if not ios_router:
raise ModuleError("No IOS router for platform {}".format(node.settings()["platform"]))
@@ -487,7 +487,11 @@ class Dynamips(Module):
if wic in ios_router:
settings[wic] = ios_router[wic]
node.setup(ios_router["path"], ios_router["ram"], initial_settings=settings)
base_name = "R"
if "slot1" in settings and settings["slot1"] == "NM-16ESW":
# must be an EtherSwitch router
base_name = "ESW"
node.setup(ios_router["path"], ios_router["ram"], initial_settings=settings, base_name=base_name)
else:
node.setup()
@@ -546,8 +550,8 @@ class Dynamips(Module):
"""
for node in self._nodes:
if hasattr(node, "exportConfigs") and node.initialized():
node.exportConfigs(directory)
if isinstance(node, Router) and node.initialized():
node.exportConfigToDirectory(directory)
def importConfigs(self, directory):
"""
@@ -557,8 +561,8 @@ class Dynamips(Module):
"""
for node in self._nodes:
if hasattr(node, "importConfigs") and node.initialized():
node.importConfigs(directory)
if isinstance(node, Router) and node.initialized():
node.importConfigFromDirectory(directory)
def findAlternativeIOSImage(self, image, node):
"""
@@ -643,7 +647,7 @@ class Dynamips(Module):
server = "{}:{}".format(remote_server.host, remote_server.port)
nodes = []
for node_class in [EtherSwitchRouter, EthernetSwitch, EthernetHub, FrameRelaySwitch, ATMSwitch]:
for node_class in [EthernetSwitch, EthernetHub, FrameRelaySwitch, ATMSwitch]:
nodes.append(
{"class": node_class.__name__,
"name": node_class.symbolName(),

View File

@@ -19,14 +19,19 @@
Wizard for IOS routers.
"""
import sys
import os
import re
from gns3.qt import QtCore, QtGui
from gns3.servers import Servers
from gns3.node import Node
from gns3.utils.message_box import MessageBox
from gns3.dialogs.exec_command_dialog import ExecCommandDialog
from gns3.utils.run_in_terminal import RunInTerminal
from gns3.utils.get_resource import get_resource
from gns3.utils.get_default_base_config import get_default_base_config
from ....settings import ENABLE_CLOUD
from ..ui.ios_router_wizard_ui import Ui_IOSRouterWizard
from ..settings import PLATFORMS_DEFAULT_RAM, CHASSIS, ADAPTER_MATRIX, WIC_MATRIX
from .. import Dynamips
@@ -54,23 +59,44 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
Wizard to create an IOS router.
:param parent: parent widget
:param ios_routers: existing IOS routers
"""
def __init__(self, parent):
def __init__(self, ios_routers, parent):
QtGui.QWizard.__init__(self, parent)
self.setupUi(self)
self.setPixmap(QtGui.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/router.normal.svg"))
self.setWizardStyle(QtGui.QWizard.ModernStyle)
if sys.platform.startswith("darwin"):
# we want to see the cancel button on OSX
self.setOptions(QtGui.QWizard.NoDefaultButton)
self.uiRemoteRadioButton.toggled.connect(self._remoteServerToggledSlot)
self.uiLoadBalanceCheckBox.toggled.connect(self._loadBalanceToggledSlot)
self.uiIOSImageToolButton.clicked.connect(self._iosImageBrowserSlot)
self.uiTestIOSImagePushButton.clicked.connect(self._testIOSImageSlot)
self.uiIdlePCFinderPushButton.clicked.connect(self._idlePCFinderSlot)
self.uiEtherSwitchCheckBox.stateChanged.connect(self._etherSwitchSlot)
self.uiPlatformComboBox.currentIndexChanged[str].connect(self._platformChangedSlot)
self.uiPlatformComboBox.addItems(list(PLATFORMS_DEFAULT_RAM.keys()))
# Validate the Idle PC value
self._idle_valid = False
idle_pc_rgx = QtCore.QRegExp("^(0x[0-9a-fA-F]+)?$")
validator = QtGui.QRegExpValidator(idle_pc_rgx)
self.uiIdlepcLineEdit.setValidator(validator)
self.uiIdlepcLineEdit.textChanged.connect(self._idlePCValidateSlot)
self.uiIdlepcLineEdit.textChanged.emit(self.uiIdlepcLineEdit.text())
# location of the base config templates
self._base_startup_config_template = get_resource(os.path.join("configs", "ios_base_startup-config.txt"))
self._base_private_config_template = get_resource(os.path.join("configs", "ios_base_private-config.txt"))
self._base_etherswitch_startup_config_template = get_resource(os.path.join("configs", "ios_etherswitch_startup-config.txt"))
#FIXME: hide because of issue on Windows.
self.uiTestIOSImagePushButton.hide()
# Mandatory fields
self.uiNamePlatformWizardPage.registerField("name*", self.uiNameLineEdit)
self.uiIOSImageWizardPage.registerField("image*", self.uiIOSImageLineEdit)
@@ -87,12 +113,15 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
1: self.uiWic1comboBox,
2: self.uiWic2comboBox}
self.uiTestIOSImagePushButton.hide() # hide it because it doesn't work
self._ios_routers = ios_routers
if Dynamips.instance().settings()["use_local_server"]:
# skip the server page if we use the local server
self.setStartId(1)
if not ENABLE_CLOUD:
self.uiCloudRadioButton.hide()
def _remoteServerToggledSlot(self, checked):
"""
Slot for when the remote server radio button is toggled.
@@ -127,16 +156,50 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
self.uiChassisComboBox.clear()
if platform in CHASSIS:
self.uiChassisComboBox.addItems(CHASSIS[platform])
if platform not in ("c2600", "c3600", "c2691", "c3725", "c3745"):
self.uiEtherSwitchCheckBox.setChecked(False)
self.uiEtherSwitchCheckBox.hide()
else:
self.uiEtherSwitchCheckBox.show()
def _testIOSImageSlot(self):
"""
Slot to locally test the IOS image.
"""
platform = self.uiPlatformComboBox.currentText()
ram = self.uiRamSpinBox.value()
ios_image = self.uiIOSImageLineEdit.text()
params = ["-P", platform[1:], "-r", str(ram), ios_image]
dialog = ExecCommandDialog(self, "/usr/bin/dynamips", params)
dialog.show()
dialog.exec_()
dynamips = os.path.realpath(Dynamips.instance().settings()["path"])
if not os.path.exists(dynamips):
QtGui.QMessageBox.critical(self, "IOS image", "Could not find Dynamips executable: {}".format(dynamips))
return
command = '"{path}" -P {platform} -r {ram} "{ios_image}"'.format(path=dynamips,
platform=platform[1:],
ram=ram,
ios_image=ios_image)
try:
RunInTerminal(command)
except OSError as e:
QtGui.QMessageBox.critical(self, "IOS image", "Could not test the IOS image: {}".format(e))
def _idlePCValidateSlot(self):
"""
Slot to validate the entered Idle-PC Value
"""
sender = self.sender()
validator = sender.validator()
state = validator.validate(sender.text(), 0)[0]
if state == QtGui.QValidator.Acceptable:
color = '#A2C964' # green
self._idle_valid = True
elif state == QtGui.QValidator.Intermediate:
color = '#fff79a' # yellow
self._idle_valid = False
else:
color = '#f6989d' # red
self._idle_valid = False
sender.setStyleSheet('QLineEdit { background-color: %s }' % color)
def _idlePCFinderSlot(self):
"""
@@ -154,6 +217,20 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
self._router.created_signal.connect(self.createdSlot)
self.uiIdlePCFinderPushButton.setEnabled(False)
def _etherSwitchSlot(self, state):
"""
Slot if the EtherSwitch option is chosen or not.
:param state: boolean
"""
if state:
# forces the name to EtherSwitch
self.uiNameLineEdit.setText("EtherSwitch router")
#self.uiNameLineEdit.setEnabled(False)
else:
self.uiNameLineEdit.setText(self.uiPlatformComboBox.currentText())
#self.uiNameLineEdit.setEnabled(True)
def createdSlot(self, node_id):
"""
The node for the auto Idle-PC has been created.
@@ -203,7 +280,7 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
# try to guess the platform
image = os.path.basename(path)
match = re.match("^(c[0-9]+)\\-\w+", image)
match = re.match("^(c[0-9]+)p?\\-\w+", image.lower())
if not match:
QtGui.QMessageBox.warning(self, "IOS image", "Could not detect the platform, make sure this is a valid IOS image!")
return
@@ -221,6 +298,9 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
QtGui.QMessageBox.warning(self, "IOS image", "This IOS image is for the {} platform/chassis and is not supported by this application!".format(detected_platform))
return
if image.lower().startswith("c7200p"):
QtGui.QMessageBox.warning(self, "IOS image", "This IOS image is for c7200 PowerPC routers and is not recommended. Please use an IOS image that do not start with c7200p.")
index = self.uiPlatformComboBox.findText(detected_platform)
if index != -1:
self.uiPlatformComboBox.setCurrentIndex(index)
@@ -277,6 +357,7 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
self.uiNameLineEdit.setText(self.uiPlatformComboBox.currentText())
ios_image = self.uiIOSImageLineEdit.text()
self.setWindowTitle("New IOS router - {}".format(os.path.basename(ios_image)))
elif self.page(page_id) == self.uiMemoryWizardPage:
# set the correct amount of RAM based on the platform
from ..pages.ios_router_preferences_page import IOSRouterPreferencesPage
@@ -284,7 +365,7 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
path = self.uiIOSImageLineEdit.text()
if os.path.isfile(path):
minimum_required_ram = IOSRouterPreferencesPage.getMinimumRequiredRAM(path)
if minimum_required_ram > PLATFORMS_DEFAULT_RAM[platform]:
if minimum_required_ram >= PLATFORMS_DEFAULT_RAM[platform]:
self.uiRamSpinBox.setValue(minimum_required_ram)
else:
self.uiRamSpinBox.setValue(PLATFORMS_DEFAULT_RAM[platform])
@@ -298,45 +379,35 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
chassis = ""
self._populateAdapters(platform, chassis)
if platform == "c7200":
self.uiSlot0comboBox.setCurrentIndex(self.uiSlot0comboBox.findText("C7200-IO-2FE"))
self.uiSlot0comboBox.setCurrentIndex(self.uiSlot0comboBox.findText("C7200-IO-FE"))
if self.uiEtherSwitchCheckBox.isChecked():
self.uiSlot1comboBox.setCurrentIndex(self.uiSlot1comboBox.findText("NM-16ESW"))
def validateCurrentPage(self):
"""
Validates the IOS name.
Validates the IOS name and checks validation state for Idle-PC value
"""
if self.currentPage() == self.uiServerWizardPage:
#FIXME: prevent users to use "cloud"
if self.uiCloudRadioButton.isChecked():
QtGui.QMessageBox.critical(self, "Cloud", "Sorry not implemented yet!")
if self.currentPage() == self.uiNamePlatformWizardPage:
name = self.uiNameLineEdit.text()
for ios_router in self._ios_routers.values():
if ios_router["name"] == name:
QtGui.QMessageBox.critical(self, "Name", "{} is already used, please choose another name".format(name))
return False
#if self.uiEtherSwitchCheckBox.isChecked() and ios_router["etherswitch"]:
# QtGui.QMessageBox.critical(self, "EtherSwitch router", "A router has already been configured to be used as the EtherSwitch router".format(name))
# return False
if self.currentPage() == self.uiIdlePCWizardPage:
if not self._idle_valid:
idle_pc = self.uiIdlepcLineEdit.text()
QtGui.QMessageBox.critical(self, "Idle-PC", "{} is not a valid Idle-PC value ".format(idle_pc))
return False
if self.currentPage() == self.uiServerWizardPage and self.uiRemoteRadioButton.isChecked():
if not Servers.instance().remoteServers():
QtGui.QMessageBox.critical(self, "Remote server", "There is no remote server registered in Dynamips preferences")
return False
# if self.currentPage() == self.uiNameImageWizardPage:
# name = self.uiNameLineEdit.text()
# if not re.search(r"""^[\-\w]+$""", name):
# # IOS names must start with a letter, end with a letter or digit, and
# # have as interior characters only letters, digits, and hyphens.
# # They must be 63 characters or fewer.
# QtGui.QMessageBox.critical(self, "Name", "Invalid name detected: {}".format(name))
# return False
return True
# minimum_required_ram = self._getMinimumRequiredRAM(path)
# if minimum_required_ram > ram:
# QtGui.QMessageBox.warning(self, "IOS image", "There is not sufficient RAM allocated to this IOS image, recommended RAM is {} MB".format(minimum_required_ram))
#
# # basename doesn't work on Unix with Windows paths
# if not sys.platform.startswith('win') and len(path) > 2 and path[1] == ":":
# import ntpath
# image = ntpath.basename(path)
# else:
# image = os.path.basename(path)
#
# if image.startswith("c7200p"):
# QtGui.QMessageBox.warning(self, "IOS image", "This IOS image is for the c7200 platform with NPE-G2 and using it is not recommended.\nPlease use an IOS image that do not start with c7200p.")
def getSettings(self):
"""
Returns the settings set in this Wizard.
@@ -345,28 +416,40 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
"""
path = self.uiIOSImageLineEdit.text()
image = os.path.basename(path)
if Dynamips.instance().settings()["use_local_server"] or self.uiLocalRadioButton.isChecked():
server = "local"
elif self.uiLoadBalanceCheckBox.isChecked():
server = next(iter(Servers.instance()))
if not server:
QtGui.QMessageBox.critical(self, "IOS router", "No remote server available!")
return
server = "{}:{}".format(server.host, server.port)
else:
server = self.uiRemoteServersComboBox.currentText()
elif self.uiRemoteRadioButton.isChecked():
if self.uiLoadBalanceCheckBox.isChecked():
server = next(iter(Servers.instance()))
server = "{}:{}".format(server.host, server.port)
else:
server = self.uiRemoteServersComboBox.currentText()
else: # Cloud is selected
server = "cloud"
settings = {
"name": self.uiNameLineEdit.text(),
"path": path,
"startup_config": get_default_base_config(self._base_startup_config_template),
"private_config": get_default_base_config(self._base_private_config_template),
"ram": self.uiRamSpinBox.value(),
"idlepc": self.uiIdlepcLineEdit.text(),
"image": os.path.basename(path),
"image": image,
"platform": self.uiPlatformComboBox.currentText(),
"chassis": self.uiChassisComboBox.currentText(),
"server": server,
}
if self.uiEtherSwitchCheckBox.isChecked():
settings["startup_config"] = get_default_base_config(self._base_etherswitch_startup_config_template)
settings["default_symbol"] = ":/symbols/multilayer_switch.normal.svg"
settings["hover_symbol"] = ":/symbols/multilayer_switch.selected.svg"
settings["category"] = Node.switches
if image.lower().startswith("c7200p"):
settings["npe"] = "npe-g2"
for slot_id, widget in self._widget_slots.items():
if widget.isEnabled():
settings["slot{}".format(slot_id)] = widget.currentText()
@@ -388,4 +471,4 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
if platform not in WIC_MATRIX:
# skip the WIC modules page if the platform doesn't support any.
return self.uiNetworkAdaptersWizardPage.nextId() + 1
return QtGui.QWizard.nextId(self)
return QtGui.QWizard.nextId(self)

View File

@@ -141,33 +141,36 @@ class ATMSwitch(Node):
:param new_settings: settings dictionary
"""
ports_to_create = []
mapping = new_settings["mappings"]
updated = False
for source, destination in mapping.items():
source_port = source.split(":")[0]
destination_port = destination.split(":")[0]
if source_port not in ports_to_create:
ports_to_create.append(source_port)
if destination_port not in ports_to_create:
ports_to_create.append(destination_port)
if "mappings" in new_settings:
ports_to_create = []
mapping = new_settings["mappings"]
for port in self._ports.copy():
if port.isFree():
for source, destination in mapping.items():
source_port = source.split(":")[0]
destination_port = destination.split(":")[0]
if source_port not in ports_to_create:
ports_to_create.append(source_port)
if destination_port not in ports_to_create:
ports_to_create.append(destination_port)
for port in self._ports.copy():
if port.isFree():
updated = True
self._ports.remove(port)
else:
ports_to_create.remove(port.name())
for port_name in ports_to_create:
port = ATMPort(port_name)
port.setPortNumber(int(port_name))
port.setStatus(ATMPort.started)
port.setPacketCaptureSupported(True)
self._ports.append(port)
updated = True
self._ports.remove(port)
else:
ports_to_create.remove(port.name())
log.debug("port {} has been added".format(port_name))
for port_name in ports_to_create:
port = ATMPort(port_name)
port.setPortNumber(int(port_name))
port.setStatus(ATMPort.started)
port.setPacketCaptureSupported(True)
self._ports.append(port)
updated = True
log.debug("port {} has been added".format(port_name))
self._settings["mappings"] = new_settings["mappings"].copy()
params = {}
if "name" in new_settings and new_settings["name"] != self.name():
@@ -178,7 +181,6 @@ class ATMSwitch(Node):
"name": new_settings["name"]}
updated = True
self._settings["mappings"] = new_settings["mappings"].copy()
if updated:
if params:
log.debug("{} is being updated: {}".format(self.name(), params))
@@ -337,7 +339,7 @@ class ATMSwitch(Node):
try:
port.startPacketCapture(result["capture_file_path"])
except OSError as e:
self.error_signal.emit(self.id(), "could not start the packet capture reader: {}".format(e))
self.error_signal.emit(self.id(), "could not start the packet capture reader: {}: {}".format(e, e.filename))
self.updated_signal.emit()
break

View File

@@ -33,7 +33,7 @@ class C1700(Router):
def __init__(self, module, server, chassis="1720"):
Router.__init__(self, module, server, platform="c1700")
self._platform_settings = {"ram": 64,
self._platform_settings = {"ram": 128,
"nvram": 32,
"disk0": 0,
"disk1": 0,

View File

@@ -45,7 +45,7 @@ class C2600(Router):
def __init__(self, module, server, chassis="2610"):
Router.__init__(self, module, server, platform="c2600")
self._platform_settings = {"ram": 64,
self._platform_settings = {"ram": 128,
"nvram": 128,
"disk0": 0,
"disk1": 0,

View File

@@ -33,7 +33,7 @@ class C2691(Router):
def __init__(self, module, server):
Router.__init__(self, module, server, platform="c2691")
self._platform_settings = {"ram": 128,
self._platform_settings = {"ram": 192,
"nvram": 112,
"disk0": 16,
"disk1": 0,

View File

@@ -33,7 +33,7 @@ class C3600(Router):
def __init__(self, module, server, chassis="3640"):
Router.__init__(self, module, server, platform="c3600")
self._platform_settings = {"ram": 128,
self._platform_settings = {"ram": 192,
"nvram": 128,
"disk0": 0,
"disk1": 0,

View File

@@ -33,7 +33,7 @@ class C3745(Router):
def __init__(self, module, server):
Router.__init__(self, module, server, platform="c3745")
self._platform_settings = {"ram": 128,
self._platform_settings = {"ram": 256,
"nvram": 304,
"disk0": 16,
"disk1": 0,

View File

@@ -33,7 +33,7 @@ class C7200(Router):
def __init__(self, module, server, npe="npe-400"):
Router.__init__(self, module, server, platform="c7200")
self._platform_settings = {"ram": 256,
self._platform_settings = {"ram": 512,
"nvram": 128,
"disk0": 64,
"disk1": 0,
@@ -47,7 +47,7 @@ class C7200(Router):
if npe == "npe-g2":
self._platform_settings["slot0"] = "C7200-IO-GE-E"
else:
self._platform_settings["slot0"] = "C7200-IO-2FE"
self._platform_settings["slot0"] = "C7200-IO-FE"
# merge platform settings with the generic ones
self._settings.update(self._platform_settings)

View File

@@ -143,30 +143,33 @@ class EthernetHub(Node):
:param new_settings: settings dictionary
"""
ports_to_create = []
ports = new_settings["ports"]
updated = False
for port_number in ports:
if port_number not in ports_to_create:
ports_to_create.append(port_number)
if "ports" in new_settings:
ports_to_create = []
ports = new_settings["ports"]
for port_number in ports:
if port_number not in ports_to_create:
ports_to_create.append(port_number)
for port in self._ports.copy():
if port.isFree():
self._ports.remove(port)
for port in self._ports.copy():
if port.isFree():
self._ports.remove(port)
updated = True
log.debug("port {} has been removed".format(port.name()))
else:
ports_to_create.remove(port.name())
for port_name in ports_to_create:
port = EthernetPort(port_name)
port.setPortNumber(int(port_name))
port.setStatus(EthernetPort.started)
port.setPacketCaptureSupported(True)
self._ports.append(port)
updated = True
log.debug("port {} has been removed".format(port.name()))
else:
ports_to_create.remove(port.name())
log.debug("port {} has been added".format(port_name))
self._settings["ports"] = new_settings["ports"].copy()
for port_name in ports_to_create:
port = EthernetPort(port_name)
port.setPortNumber(int(port_name))
port.setStatus(EthernetPort.started)
port.setPacketCaptureSupported(True)
self._ports.append(port)
updated = True
log.debug("port {} has been added".format(port_name))
params = {}
if "name" in new_settings and new_settings["name"] != self.name():
@@ -177,7 +180,6 @@ class EthernetHub(Node):
"name": new_settings["name"]}
updated = True
self._settings["ports"] = new_settings["ports"].copy()
if updated:
if params:
log.debug("{} is being updated: {}".format(self.name(), params))
@@ -326,7 +328,7 @@ class EthernetHub(Node):
try:
port.startPacketCapture(result["capture_file_path"])
except OSError as e:
self.error_signal.emit(self.id(), "could not start the packet capture reader: {}".format(e))
self.error_signal.emit(self.id(), "could not start the packet capture reader: {}: {}".format(e, e.filename))
self.updated_signal.emit()
break

View File

@@ -146,31 +146,44 @@ class EthernetSwitch(Node):
:param new_settings: settings dictionary
"""
ports_to_update = {}
ports = new_settings["ports"]
updated = False
for port_number in ports.keys():
if port_number in self._settings["ports"]:
if self._settings["ports"][port_number] != ports[port_number]:
for port in self._ports:
if port.portNumber() == port_number and not port.isFree():
ports_to_update[port_number] = ports[port_number]
break
continue
port = EthernetPort(str(port_number))
port.setPortNumber(port_number)
port.setStatus(EthernetPort.started)
port.setPacketCaptureSupported(True)
self._ports.append(port)
updated = True
log.debug("port {} has been added".format(port_number))
params = {"id": self._ethsw_id}
if ports_to_update:
params["ports"] = {}
for port_number, info in ports_to_update.items():
params["ports"][port_number] = info
updated = True
if "ports" in new_settings:
ports_to_update = {}
ports = new_settings["ports"]
for port_number in ports.keys():
if port_number in self._settings["ports"]:
if self._settings["ports"][port_number] != ports[port_number]:
for port in self._ports:
if port.portNumber() == port_number and not port.isFree():
ports_to_update[port_number] = ports[port_number]
break
continue
port = EthernetPort(str(port_number))
port.setPortNumber(port_number)
port.setStatus(EthernetPort.started)
port.setPacketCaptureSupported(True)
self._ports.append(port)
updated = True
log.debug("port {} has been added".format(port_number))
if ports_to_update:
params["ports"] = {}
for port_number, info in ports_to_update.items():
params["ports"][port_number] = info
updated = True
# delete ports that are not configured
for port_number in self._settings["ports"].keys():
if port_number not in new_settings["ports"]:
for port in self._ports.copy():
if port.portNumber() == port_number:
self._ports.remove(port)
log.debug("port {} has been removed".format(port.name()))
break
self._settings["ports"] = new_settings["ports"].copy()
if "name" in new_settings and new_settings["name"] != self.name():
if self.hasAllocatedName(new_settings["name"]):
@@ -179,16 +192,6 @@ class EthernetSwitch(Node):
params["name"] = new_settings["name"]
updated = True
# delete ports that are not configured
for port_number in self._settings["ports"].keys():
if port_number not in new_settings["ports"]:
for port in self._ports.copy():
if port.portNumber() == port_number:
self._ports.remove(port)
log.debug("port {} has been removed".format(port.name()))
break
self._settings["ports"] = new_settings["ports"].copy()
if updated:
log.debug("{} is being updated: {}".format(self.name(), params))
self._server.send_message("dynamips.ethsw.update", params, self._updateCallback)
@@ -337,7 +340,7 @@ class EthernetSwitch(Node):
try:
port.startPacketCapture(result["capture_file_path"])
except OSError as e:
self.error_signal.emit(self.id(), "could not start the packet capture reader: {}".format(e))
self.error_signal.emit(self.id(), "could not start the packet capture reader: {}: {}".format(e, e.filename))
self.updated_signal.emit()
break

View File

@@ -17,6 +17,7 @@
"""
EtherSwitch router implementation (based on Dynamips c3745).
This is legacy code, kept only to support topologies made with GNS3 < 1.2.2
"""
import sys

View File

@@ -140,34 +140,37 @@ class FrameRelaySwitch(Node):
:param new_settings: settings dictionary
"""
ports_to_create = []
mapping = new_settings["mappings"]
updated = False
for source, destination in mapping.items():
source_port = source.split(":")[0]
destination_port = destination.split(":")[0]
if source_port not in ports_to_create:
ports_to_create.append(source_port)
if destination_port not in ports_to_create:
ports_to_create.append(destination_port)
if "mappings" in new_settings:
ports_to_create = []
mapping = new_settings["mappings"]
for port in self._ports.copy():
if port.isFree():
self._ports.remove(port)
for source, destination in mapping.items():
source_port = source.split(":")[0]
destination_port = destination.split(":")[0]
if source_port not in ports_to_create:
ports_to_create.append(source_port)
if destination_port not in ports_to_create:
ports_to_create.append(destination_port)
for port in self._ports.copy():
if port.isFree():
self._ports.remove(port)
updated = True
log.debug("port {} has been removed".format(port.name()))
else:
ports_to_create.remove(port.name())
for port_name in ports_to_create:
port = FrameRelayPort(port_name)
port.setPortNumber(int(port_name))
port.setStatus(FrameRelayPort.started)
port.setPacketCaptureSupported(True)
self._ports.append(port)
updated = True
log.debug("port {} has been removed".format(port.name()))
else:
ports_to_create.remove(port.name())
log.debug("port {} has been added".format(port_name))
for port_name in ports_to_create:
port = FrameRelayPort(port_name)
port.setPortNumber(int(port_name))
port.setStatus(FrameRelayPort.started)
port.setPacketCaptureSupported(True)
self._ports.append(port)
updated = True
log.debug("port {} has been added".format(port_name))
self._settings["mappings"] = new_settings["mappings"].copy()
params = {}
if "name" in new_settings and new_settings["name"] != self.name():
@@ -178,7 +181,6 @@ class FrameRelaySwitch(Node):
"name": new_settings["name"]}
updated = True
self._settings["mappings"] = new_settings["mappings"].copy()
if updated:
if params:
log.debug("{} is being updated: {}".format(self.name(), params))
@@ -336,7 +338,7 @@ class FrameRelaySwitch(Node):
try:
port.startPacketCapture(result["capture_file_path"])
except OSError as e:
self.error_signal.emit(self.id(), "could not start the packet capture reader: {}".format(e))
self.error_signal.emit(self.id(), "could not start the packet capture reader: {}: {}".format(e, e.filename))
self.updated_signal.emit()
break

View File

@@ -21,6 +21,7 @@ Asynchronously sends JSON messages to the GNS3 server and receives responses wit
"""
import os
import re
import base64
from gns3.node import Node
from gns3.ports.port import Port
@@ -103,9 +104,12 @@ class Router(Node):
if "chassis" in self._settings and self._settings["chassis"] in ("1720", "1721", "1750"):
# these chassis show their interface without a slot number
port_name = port.longNameType() + str(port_number)
short_name = port.shortNameType() + str(port_number)
else:
port_name = port.longNameType() + str(slot_number) + "/" + str(port_number)
short_name = port.shortNameType() + str(slot_number) + "/" + str(port_number)
new_port = port(port_name)
new_port.setShortName(short_name)
new_port.setPortNumber(port_number)
new_port.setSlotNumber(slot_number)
new_port.setPacketCaptureSupported(True)
@@ -138,7 +142,9 @@ class Router(Node):
port = WIC_MATRIX[wic]["port"]
# Dynamips WICs port number start on a multiple of 16.
port_name = port.longNameType() + str(base + port_number)
short_name = port.shortNameType() + str(base + port_number)
new_port = port(port_name)
new_port.setShortName(short_name)
new_port.setPortNumber(base + port_number)
# WICs are always in adapter slot 0.
new_port.setSlotNumber(0)
@@ -190,8 +196,10 @@ class Router(Node):
if "chassis" in self._settings and self._settings["chassis"] in ("1720", "1721", "1750"):
# these chassis show their interface without a slot number
port.setName(port.longNameType() + str(wic_port_number))
port.setShortName(port.shortNameType() + str(wic_port_number))
else:
port.setName(port.longNameType() + "0/" + str(wic_port_number))
port.setShortName(port.shortNameType() + "0/" + str(wic_port_number))
log.debug("port {} renamed to {}".format(old_name, port.name()))
def delete(self):
@@ -250,6 +258,10 @@ class Router(Node):
"ram": ram,
"image": image}
if self.server().isCloud():
initial_settings["cloud_path"] = "images/IOS"
params["image"] = os.path.basename(params["image"])
if router_id:
params["router_id"] = router_id
@@ -262,6 +274,8 @@ class Router(Node):
params["mac_addr"] = self._settings["mac_addr"] = initial_settings.pop("mac_addr")
if "chassis" in initial_settings:
params["chassis"] = self._settings["chassis"] = initial_settings.pop("chassis")
if "cloud_path" in initial_settings:
params["cloud_path"] = self._settings["cloud_path"] = initial_settings.pop("cloud_path")
# other initial settings will be applied when the router has been created
if initial_settings:
@@ -566,7 +580,7 @@ class Router(Node):
try:
port.startPacketCapture(result["capture_file_path"])
except OSError as e:
self.error_signal.emit(self.id(), "could not start the packet capture reader: {}".format(e))
self.error_signal.emit(self.id(), "could not start the packet capture reader: {}: {}".format(e, e.filename))
self.updated_signal.emit()
break
@@ -931,14 +945,14 @@ class Router(Node):
for port in self._ports:
ports.append(port.dump())
# make the IOU path relative
# make the IOS path relative
image_path = router["properties"]["image"]
if self.server().isLocal():
if os.path.commonprefix([image_path, self._module.imageFilesDir()]) == self._module.imageFilesDir():
# save only the image name if it is stored the images directory
router["properties"]["image"] = os.path.basename(image_path)
else:
router["properties"]["image"] = os.path.basename(image_path)
router["properties"]["image"] = image_path
return router
@@ -1000,7 +1014,50 @@ class Router(Node):
self._inital_settings = None
self._loading = False
def exportConfigs(self, directory):
def exportConfig(self, startup_config_export_path, private_config_export_path):
"""
Exports the startup-config.
:param startup_config_export_path: export path for the startup-config
:param private_config_export_path: export path for the private-config
"""
self._startup_config_export_path = startup_config_export_path
self._private_config_export_path = private_config_export_path
self._server.send_message("dynamips.vm.export_config", {"id": self._router_id}, self._exportConfigCallback)
def _exportConfigCallback(self, result, error=False):
"""
Callback for exportConfig.
:param result: server response
:param error: indicates an error (boolean)
"""
if error:
log.error("error while exporting {} configs: {}".format(self.name(), result["message"]))
self.server_error_signal.emit(self.id(), result["code"], result["message"])
else:
if "startup_config_base64" in result and self._startup_config_export_path:
config = base64.decodebytes(result["startup_config_base64"].encode("utf-8"))
try:
with open(self._startup_config_export_path, "wb") as f:
log.info("saving {} startup-config to {}".format(self.name(), self._startup_config_export_path))
f.write(config)
except OSError as e:
self.error_signal.emit(self.id(), "could not export startup-config to {}: {}".format(self._startup_config_export_path, e))
if "private_config_base64" in result and self._private_config_export_path:
config = base64.decodebytes(result["private_config_base64"].encode("utf-8"))
try:
with open(self._private_config_export_path, "wb") as f:
log.info("saving {} private-config to {}".format(self.name(), self._private_config_export_path))
f.write(config)
except OSError as e:
self.error_signal.emit(self.id(), "could not export private-config to {}: {}".format(self._private_config_export_path, e))
def exportConfigToDirectory(self, directory):
"""
Exports the startup-config and private-config to a directory.
@@ -1008,11 +1065,11 @@ class Router(Node):
"""
self._export_directory = directory
self._server.send_message("dynamips.vm.export_config", {"id": self._router_id}, self._exportConfigsCallback)
self._server.send_message("dynamips.vm.export_config", {"id": self._router_id}, self._exportConfigToDirectoryCallback)
def _exportConfigsCallback(self, result, error=False):
def _exportConfigToDirectoryCallback(self, result, error=False):
"""
Callback for exportConfigs.
Callback for exportConfigToDirectory.
:param result: server response
:param error: indicates an error (boolean)
@@ -1045,7 +1102,27 @@ class Router(Node):
self._export_directory = None
def importConfigs(self, directory):
def importConfig(self, path):
"""
Imports a startup-config.
:param path: path to the startup-config
"""
new_settings = {"startup_config": path}
self.update(new_settings)
def importPrivateConfig(self, path):
"""
Imports a private-config.
:param path: path to the private-config
"""
new_settings = {"private_config": path}
self.update(new_settings)
def importConfigFromDirectory(self, directory):
"""
Imports a startup-config and a private-config from a directory.
@@ -1122,6 +1199,23 @@ class Router(Node):
from ..pages.ios_router_configuration_page import IOSRouterConfigurationPage
return IOSRouterConfigurationPage
@staticmethod
def validateHostname(hostname):
"""
Checks if the hostname is valid.
:param hostname: hostname to check
:returns: boolean
"""
# IOS names must start with a letter, end with a letter or digit, and
# have as interior characters only letters, digits, and hyphens.
# They must be 63 characters or fewer.
if re.search(r"""^[\-\w]+$""", hostname) and len(hostname) <= 63:
return True
return False
@staticmethod
def defaultSymbol():
"""

View File

@@ -21,10 +21,8 @@ Configuration page for Dynamips IOS routers.
import os
import re
import sys
import pkg_resources
from gns3.qt import QtGui
from gns3.qt import QtCore, QtGui
from gns3.dialogs.node_configurator_dialog import ConfigurationError
from ..ui.ios_router_configuration_page_ui import Ui_iosRouterConfigPageWidget
from ..settings import CHASSIS, ADAPTER_MATRIX, WIC_MATRIX
@@ -56,6 +54,31 @@ class IOSRouterConfigurationPage(QtGui.QWidget, Ui_iosRouterConfigPageWidget):
self.uiPrivateConfigToolButton.clicked.connect(self._privateConfigBrowserSlot)
self.uiIOSImageToolButton.clicked.connect(self._iosImageBrowserSlot)
self._idle_valid = False
idle_pc_rgx = QtCore.QRegExp("^(0x[0-9a-fA-F]+)?$")
validator = QtGui.QRegExpValidator(idle_pc_rgx)
self.uiIdlepcLineEdit.setValidator(validator)
self.uiIdlepcLineEdit.textChanged.connect(self._idlePCValidateSlot)
self.uiIdlepcLineEdit.textChanged.emit(self.uiIdlepcLineEdit.text())
def _idlePCValidateSlot(self):
"""
Slot to validate the entered Idle-PC Value
"""
sender = self.sender()
validator = sender.validator()
state = validator.validate(sender.text(), 0)[0]
if state == QtGui.QValidator.Acceptable:
color = '#A2C964' # green
self._idle_valid = True
elif state == QtGui.QValidator.Intermediate:
color = '#fff79a' # yellow
self._idle_valid = False
else:
color = '#f6989d' # red
self._idle_valid = False
sender.setStyleSheet('QLineEdit { background-color: %s }' % color)
def _iosImageBrowserSlot(self):
"""
Slot to open a file browser and select an IOU image.
@@ -98,10 +121,7 @@ class IOSRouterConfigurationPage(QtGui.QWidget, Ui_iosRouterConfigPageWidget):
Slot to open a file browser and select a startup-config file.
"""
if hasattr(sys, "frozen"):
config_dir = "configs"
else:
config_dir = pkg_resources.resource_filename("gns3", "configs")
config_dir = os.path.join(os.path.dirname(QtCore.QSettings().fileName()), "base_configs")
path = QtGui.QFileDialog.getOpenFileName(self, "Select a startup configuration", config_dir)
if not path:
return
@@ -118,10 +138,7 @@ class IOSRouterConfigurationPage(QtGui.QWidget, Ui_iosRouterConfigPageWidget):
Slot to open a file browser and select a private-config file.
"""
if hasattr(sys, "frozen"):
config_dir = "configs"
else:
config_dir = pkg_resources.resource_filename("gns3", "configs")
config_dir = os.path.join(os.path.dirname(QtCore.QSettings().fileName()), "base_configs")
path = QtGui.QFileDialog.getOpenFileName(self, "Select a private configuration", config_dir)
if not path:
return
@@ -154,13 +171,14 @@ class IOSRouterConfigurationPage(QtGui.QWidget, Ui_iosRouterConfigPageWidget):
if type(slot_adapters) == str:
# only one default adapter for this slot.
self._widget_slots[slot_number].addItem(slot_adapters)
# elif platform == "c7200" and slot_number == 0 and settings["slot0"] != None:
# # special case
# self._widget_slots[slot_number].addItem(settings["slot0"])
else:
# list of adapters
module_list = list(slot_adapters)
self._widget_slots[slot_number].addItems([""] + module_list)
if platform == "c7200" and slot_number == 0:
# special case
self._widget_slots[slot_number].addItems(module_list)
else:
self._widget_slots[slot_number].addItems([""] + module_list)
# set the combox box to the correct slot adapter if configured.
if settings["slot" + str(slot_number)]:
@@ -210,12 +228,6 @@ class IOSRouterConfigurationPage(QtGui.QWidget, Ui_iosRouterConfigPageWidget):
self.uiAuxPortLabel.hide()
self.uiAuxPortSpinBox.hide()
# load the startup-config
self.uiStartupConfigLineEdit.setText(settings["startup_config"])
# load the private-config
self.uiPrivateConfigLineEdit.setText(settings["private_config"])
# load the MAC address setting
self.uiBaseMACLineEdit.setInputMask("HHHH.HHHH.HHHH;_")
@@ -235,12 +247,6 @@ class IOSRouterConfigurationPage(QtGui.QWidget, Ui_iosRouterConfigPageWidget):
self.uiIOSImageLabel.hide()
self.uiIOSImageLineEdit.hide()
self.uiIOSImageToolButton.hide()
self.uiStartupConfigLabel.hide()
self.uiStartupConfigLineEdit.hide()
self.uiStartupConfigToolButton.hide()
self.uiPrivateConfigLabel.hide()
self.uiPrivateConfigLineEdit.hide()
self.uiPrivateConfigToolButton.hide()
self.uiConsolePortLabel.hide()
self.uiConsolePortSpinBox.hide()
self.uiAuxPortLabel.hide()
@@ -248,6 +254,19 @@ class IOSRouterConfigurationPage(QtGui.QWidget, Ui_iosRouterConfigPageWidget):
self.uiBaseMacLabel.hide()
self.uiBaseMACLineEdit.hide()
if not node:
# load the startup-config
self.uiStartupConfigLineEdit.setText(settings["startup_config"])
# load the private-config
self.uiPrivateConfigLineEdit.setText(settings["private_config"])
else:
self.uiStartupConfigLabel.hide()
self.uiStartupConfigLineEdit.hide()
self.uiStartupConfigToolButton.hide()
self.uiPrivateConfigLabel.hide()
self.uiPrivateConfigLineEdit.hide()
self.uiPrivateConfigToolButton.hide()
# show the platform and chassis if applicable
platform = settings["platform"]
self.uiPlatformTextLabel.setText(platform)
@@ -420,14 +439,17 @@ class IOSRouterConfigurationPage(QtGui.QWidget, Ui_iosRouterConfigPageWidget):
if not group:
# Check if the Idle-PC value has been validated okay
if not self._idle_valid:
idle_pc = self.uiIdlepcLineEdit.text()
QtGui.QMessageBox.critical(self, "Idle-PC", "{} is not a valid Idle-PC value ".format(idle_pc))
raise ConfigurationError()
# set the device name
name = self.uiNameLineEdit.text()
if not name:
QtGui.QMessageBox.critical(self, "Name", "IOS router name cannot be empty!")
elif node and not re.search(r"""^[\-\w]+$""", name):
# IOS names must start with a letter, end with a letter or digit, and
# have as interior characters only letters, digits, and hyphens.
# They must be 63 characters or fewer.
elif node and not node.validateHostname(name):
QtGui.QMessageBox.critical(self, "Name", "Invalid name detected for IOS router: {}".format(name))
else:
settings["name"] = name
@@ -435,20 +457,6 @@ class IOSRouterConfigurationPage(QtGui.QWidget, Ui_iosRouterConfigPageWidget):
settings["console"] = self.uiConsolePortSpinBox.value()
settings["aux"] = self.uiAuxPortSpinBox.value()
startup_config = self.uiStartupConfigLineEdit.text()
if startup_config != settings["startup_config"]:
if os.access(startup_config, os.R_OK):
settings["startup_config"] = startup_config
else:
QtGui.QMessageBox.critical(self, "Startup-config", "Cannot read the startup-config file")
private_config = self.uiPrivateConfigLineEdit.text()
if private_config != settings["private_config"]:
if os.access(private_config, os.R_OK):
settings["private_config"] = private_config
else:
QtGui.QMessageBox.critical(self, "Private-config", "Cannot read the private-config file")
# check and save the base MAC address
#mac = self.uiBaseMACLineEdit.text()
#if mac and not re.search(r"""^([0-9a-fA-F]{4}\.){2}[0-9a-fA-F]{4}$""", mac):
@@ -470,6 +478,21 @@ class IOSRouterConfigurationPage(QtGui.QWidget, Ui_iosRouterConfigPageWidget):
del settings["private_config"]
del settings["image"]
if not node:
startup_config = self.uiStartupConfigLineEdit.text()
if startup_config != settings["startup_config"]:
if os.access(startup_config, os.R_OK):
settings["startup_config"] = startup_config
else:
QtGui.QMessageBox.critical(self, "Startup-config", "Cannot read the startup-config file")
private_config = self.uiPrivateConfigLineEdit.text()
if private_config != settings["private_config"]:
if os.access(private_config, os.R_OK):
settings["private_config"] = private_config
else:
QtGui.QMessageBox.critical(self, "Private-config", "Cannot read the private-config file")
# get the platform and chassis if applicable
platform = settings["platform"]
if "chassis" in settings:

View File

@@ -22,26 +22,31 @@ Configuration page for IOS router preferences.
import os
import copy
import sys
import pkg_resources
import shutil
import math
import zipfile
import logging
from gns3.qt import QtCore, QtGui
from gns3.node import Node
from gns3.main_window import MainWindow
from gns3.utils.progress_dialog import ProgressDialog
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
from gns3.dialogs.configuration_dialog import ConfigurationDialog
from gns3.cloud.utils import UploadFilesThread
from gns3.utils.progress_dialog import ProgressDialog
from gns3.utils.file_copy_thread import FileCopyThread
from .. import Dynamips
from ..settings import IOS_ROUTER_SETTINGS
from ..utils.decompress_ios import isIOSCompressed
from ..utils.decompress_ios_thread import DecompressIOSThread
from .. import Dynamips
from ..ui.ios_router_preferences_page_ui import Ui_IOSRouterPreferencesPageWidget
from ..pages.ios_router_configuration_page import IOSRouterConfigurationPage
from ..dialogs.ios_router_wizard import IOSRouterWizard
log = logging.getLogger(__name__)
class IOSRouterPreferencesPage(QtGui.QWidget, Ui_IOSRouterPreferencesPageWidget):
"""
QWidget preference page for IOS routers.
@@ -86,48 +91,43 @@ class IOSRouterPreferencesPage(QtGui.QWidget, Ui_IOSRouterPreferencesPageWidget)
Creates a new IOS router.
"""
wizard = IOSRouterWizard(parent=self)
wizard = IOSRouterWizard(self._ios_routers, parent=self)
wizard.show()
if wizard.exec_():
ios_settings = wizard.getSettings()
key = "{server}:{name}".format(server=ios_settings["server"], name=ios_settings["name"])
# set the default base startup-config
resource_name = "configs/ios_base_startup-config.txt"
if hasattr(sys, "frozen") and os.path.isfile(resource_name):
startup_config = os.path.normpath(resource_name)
elif pkg_resources.resource_exists("gns3", resource_name):
ios_base_config_path = pkg_resources.resource_filename("gns3", resource_name)
startup_config = os.path.normpath(ios_base_config_path)
self._ios_routers[key] = IOS_ROUTER_SETTINGS.copy()
self._ios_routers[key].update(ios_settings)
# set the default base private-config
resource_name = "configs/ios_base_private-config.txt"
if hasattr(sys, "frozen") and os.path.isfile(resource_name):
private_config = os.path.normpath(resource_name)
elif pkg_resources.resource_exists("gns3", resource_name):
ios_base_config_path = pkg_resources.resource_filename("gns3", resource_name)
private_config = os.path.normpath(ios_base_config_path)
if ios_settings["server"] == 'cloud':
import logging
log = logging.getLogger(__name__)
self._ios_routers[key] = {"name": ios_settings["name"],
"path": ios_settings["path"],
"image": ios_settings["image"],
"default_symbol": ":/symbols/router.normal.svg",
"hover_symbol": ":/symbols/router.selected.svg",
"category": Node.routers,
"startup_config": startup_config,
"private_config": private_config,
"platform": ios_settings["platform"],
"chassis": ios_settings["chassis"],
"idlepc": ios_settings["idlepc"],
"ram": ios_settings["ram"],
"nvram": 256,
"mac_addr": "",
"disk0": 1,
"disk1": 0,
"confreg": "0x2102",
"system_id": "FTX0945W0MY",
"server": ios_settings["server"]}
log.debug(ios_settings["image"])
# Start uploading the image to cloud files
self._upload_image_progress_dialog = QtGui.QProgressDialog("Uploading image file {}".format(ios_settings['image']), "Cancel", 0, 0, parent=self)
self._upload_image_progress_dialog.setWindowModality(QtCore.Qt.WindowModal)
self._upload_image_progress_dialog.setWindowTitle("IOS image upload")
self._upload_image_progress_dialog.show()
try:
upload_thread = UploadFilesThread(
self,
cloud_settings=MainWindow.instance().cloudSettings(),
files_to_upload=[(
self._ios_routers[key]["path"],
'images/' + os.path.relpath(self._ios_routers[key]["path"],
self._main_window.settings()["images_path"])
)]
)
upload_thread.completed.connect(self._imageUploadComplete)
upload_thread.start()
except Exception as e:
self._upload_image_progress_dialog.reject()
log.error(e)
QtGui.QMessageBox.critical(self, "IOS image upload", "Error uploading IOS image: {}".format(e))
if ios_settings["platform"] == "c7200":
self._ios_routers[key]["midplane"] = "vxr"
@@ -153,6 +153,11 @@ class IOSRouterPreferencesPage(QtGui.QWidget, Ui_IOSRouterPreferencesPageWidget)
self._items.append(item)
self.uiIOSRoutersTreeWidget.setCurrentItem(item)
def _imageUploadComplete(self):
if self._upload_image_progress_dialog.wasCanceled():
return
self._upload_image_progress_dialog.accept()
def _iosRouterEditSlot(self):
"""
Edits an IOS router.
@@ -166,11 +171,18 @@ class IOSRouterPreferencesPage(QtGui.QWidget, Ui_IOSRouterPreferencesPageWidget)
dialog.show()
if dialog.exec_():
if ios_router["name"] != item.text(0):
if "{}:{}".format(ios_router["server"], ios_router["name"]) in self._ios_routers:
# FIXME: bug when changing name
QtGui.QMessageBox.critical(self, "New IOS router", "IOS router name {} already exists".format(ios_router["name"]))
# rename the IOS router
new_key = "{server}:{name}".format(server=ios_router["server"], name=ios_router["name"])
if new_key in self._ios_routers:
QtGui.QMessageBox.critical(self, "IOS router", "IOS router name {} already exists for server {}".format(ios_router["name"],
ios_router["server"]))
ios_router["name"] = item.text(0)
return
self._ios_routers[new_key] = self._ios_routers[key]
del self._ios_routers[key]
item.setText(0, ios_router["name"])
item.setData(0, QtCore.Qt.UserRole, new_key)
self._refreshInfo(ios_router)
def _iosRouterDeleteSlot(self):
@@ -181,8 +193,14 @@ class IOSRouterPreferencesPage(QtGui.QWidget, Ui_IOSRouterPreferencesPageWidget)
item = self.uiIOSRoutersTreeWidget.currentItem()
if item:
key = item.data(0, QtCore.Qt.UserRole)
ios_router = self._ios_routers[key]
del self._ios_routers[key]
self.uiIOSRoutersTreeWidget.takeTopLevelItem(self.uiIOSRoutersTreeWidget.indexOfTopLevelItem(item))
if self._ios_routers == {}:
self.uiEditIOSRouterPushButton.setEnabled(False)
self.uiDeleteIOSRouterPushButton.setEnabled(False)
self.uiDecompressIOSPushButton.setEnabled(False)
@staticmethod
def getIOSImage(parent):
@@ -231,7 +249,7 @@ class IOSRouterPreferencesPage(QtGui.QWidget, Ui_IOSRouterPreferencesPageWidget)
except FileExistsError:
pass
except OSError as e:
QtGui.QMessageBox.critical(parent, "IOS images directory", "Could not create the IOS images directory {}: {}".format(destination_directory, str(e)))
QtGui.QMessageBox.critical(parent, "IOS images directory", "Could not create the IOS images directory {}: {}".format(destination_directory, e))
return
if isIOSCompressed(path):
@@ -249,21 +267,25 @@ class IOSRouterPreferencesPage(QtGui.QWidget, Ui_IOSRouterPreferencesPageWidget)
path = decompressed_image_path
thread.wait()
if os.path.dirname(path) != destination_directory:
if os.path.normpath(os.path.dirname(path)) != destination_directory:
# the IOS image is not in the default images directory
new_destination_path = os.path.join(destination_directory, os.path.basename(path))
try:
# try to create a symbolic link to it
symlink_path = new_destination_path
os.symlink(path, symlink_path)
path = symlink_path
except (OSError, NotImplementedError):
# if unsuccessful, then copy the IOS image itself
try:
shutil.copyfile(path, new_destination_path)
path = new_destination_path
except OSError:
pass
reply = QtGui.QMessageBox.question(parent,
"IOS image",
"Would you like to copy {} to the default images directory".format(os.path.basename(path)),
QtGui.QMessageBox.Yes,
QtGui.QMessageBox.No)
if reply == QtGui.QMessageBox.Yes:
destination_path = os.path.join(destination_directory, os.path.basename(path))
thread = FileCopyThread(path, destination_path)
progress_dialog = ProgressDialog(thread, "IOS image", "Copying {}".format(os.path.basename(path)), "Cancel", busy=True, parent=parent)
thread.deleteLater()
progress_dialog.show()
progress_dialog.exec_()
errors = progress_dialog.errors()
if errors:
QtGui.QMessageBox.critical(parent, "IOS image", "{}".format("".join(errors)))
else:
path = destination_path
return path
@@ -293,46 +315,6 @@ class IOSRouterPreferencesPage(QtGui.QWidget, Ui_IOSRouterPreferencesPageWidget)
# round up to the closest multiple of 32 (step of the RAM SpinBox)
return math.ceil(decompressed_size / 32) * 32
def _startupConfigBrowserSlot(self):
"""
Slot to open a file browser and select a startup-config file.
"""
if hasattr(sys, "frozen"):
config_dir = "configs"
else:
config_dir = pkg_resources.resource_filename("gns3", "configs")
path = QtGui.QFileDialog.getOpenFileName(self, "Select a startup configuration", config_dir)
if not path:
return
if not os.access(path, os.R_OK):
QtGui.QMessageBox.critical(self, "Startup configuration", "Cannot read {}".format(path))
return
self.uiStartupConfigLineEdit.clear()
self.uiStartupConfigLineEdit.setText(path)
def _privateConfigBrowserSlot(self):
"""
Slot to open a file browser and select a private-config file.
"""
if hasattr(sys, "frozen"):
config_dir = "configs"
else:
config_dir = pkg_resources.resource_filename("gns3", "configs")
path = QtGui.QFileDialog.getOpenFileName(self, "Select a private configuration", config_dir)
if not path:
return
if not os.access(path, os.R_OK):
QtGui.QMessageBox.critical(self, "Private configuration", "Cannot read {}".format(path))
return
self.uiPrivateConfigLineEdit.clear()
self.uiPrivateConfigLineEdit.setText(path)
def _decompressIOSSlot(self):
"""
Slot to decompress an IOS image.
@@ -457,16 +439,16 @@ class IOSRouterPreferencesPage(QtGui.QWidget, Ui_IOSRouterPreferencesPageWidget)
Change a symbol for an IOS router.
"""
dialog = SymbolSelectionDialog(self)
dialog.show()
if dialog.exec_():
normal_symbol, selected_symbol = dialog.getSymbols()
category = dialog.getCategory()
item = self.uiIOSRoutersTreeWidget.currentItem()
if item:
item = self.uiIOSRoutersTreeWidget.currentItem()
if item:
key = item.data(0, QtCore.Qt.UserRole)
ios_router = self._ios_routers[key]
dialog = SymbolSelectionDialog(self, symbol=ios_router["default_symbol"], category=ios_router["category"])
dialog.show()
if dialog.exec_():
normal_symbol, selected_symbol = dialog.getSymbols()
category = dialog.getCategory()
item.setIcon(0, QtGui.QIcon(normal_symbol))
key = item.data(0, QtCore.Qt.UserRole)
ios_router = self._ios_routers[key]
ios_router["default_symbol"] = normal_symbol
ios_router["hover_symbol"] = selected_symbol
ios_router["category"] = category

View File

@@ -19,9 +19,10 @@
Default Dynamips settings.
"""
from gns3.node import Node
import sys
import os
from collections import OrderedDict
# default path to Dynamips executable
@@ -81,13 +82,67 @@ DYNAMIPS_SETTING_TYPES = {
"mmap_support": bool,
}
IOS_ROUTER_SETTINGS = {
"name": "",
"path": "",
"image": "",
"default_symbol": ":/symbols/router.normal.svg",
"hover_symbol": ":/symbols/router.selected.svg",
"category": Node.routers,
"startup_config": "",
"private_config": "",
"platform": "",
"chassis": "",
"idlepc": "",
"idlemax": 500,
"idlesleep": 30,
"exec_area": 64,
"mmap": True,
"sparsemem": True,
"ram": 128,
"nvram": 256,
"mac_addr": "",
"disk0": 1,
"disk1": 0,
"confreg": "0x2102",
"system_id": "FTX0945W0MY",
"server": "local"
}
IOS_ROUTER_SETTING_TYPES = {
"name": str,
"path": str,
"image": str,
"default_symbol": str,
"hover_symbol": str,
"category": int,
"startup_config": str,
"private_config": str,
"platform": str,
"chassis": str,
"idlepc": str,
"idlemax": int,
"idlesleep": int,
"exec_area": int,
"mmap": bool,
"sparsemem": bool,
"ram": int,
"nvram": int,
"mac_addr": str,
"disk0": int,
"disk1": int,
"confreg": str,
"system_id": str,
"server": str
}
# supported platforms with the default RAM value
PLATFORMS_DEFAULT_RAM = {"c1700": 64,
"c2600": 64,
"c2691": 128,
"c3600": 128,
PLATFORMS_DEFAULT_RAM = {"c1700": 128,
"c2600": 128,
"c2691": 192,
"c3600": 192,
"c3725": 128,
"c3745": 128,
"c3745": 256,
"c7200": 512}
# platforms with supported chassis

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>419</width>
<height>522</height>
<width>435</width>
<height>510</height>
</rect>
</property>
<property name="windowTitle">
@@ -65,7 +65,7 @@
<item row="3" column="0">
<widget class="QLabel" name="uiIOSImageLabel">
<property name="text">
<string>IOS image:</string>
<string>IOS image path:</string>
</property>
</widget>
</item>
@@ -89,7 +89,7 @@
<item row="4" column="0">
<widget class="QLabel" name="uiStartupConfigLabel">
<property name="text">
<string>Startup-config:</string>
<string>Initial startup-config:</string>
</property>
</widget>
</item>
@@ -113,7 +113,7 @@
<item row="5" column="0">
<widget class="QLabel" name="uiPrivateConfigLabel">
<property name="text">
<string>Private-config:</string>
<string>Initial private-config:</string>
</property>
</widget>
</item>

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/dynamips/ui/ios_router_configuration_page.ui'
#
# Created: Fri Oct 10 10:43:48 2014
# Created: Wed Dec 24 17:35:25 2014
# by: PyQt4 UI code generator 4.10.4
#
# WARNING! All changes made in this file will be lost!
@@ -26,7 +26,7 @@ except AttributeError:
class Ui_iosRouterConfigPageWidget(object):
def setupUi(self, iosRouterConfigPageWidget):
iosRouterConfigPageWidget.setObjectName(_fromUtf8("iosRouterConfigPageWidget"))
iosRouterConfigPageWidget.resize(419, 522)
iosRouterConfigPageWidget.resize(435, 510)
self.vboxlayout = QtGui.QVBoxLayout(iosRouterConfigPageWidget)
self.vboxlayout.setObjectName(_fromUtf8("vboxlayout"))
self.uiTabWidget = QtGui.QTabWidget(iosRouterConfigPageWidget)
@@ -566,11 +566,11 @@ class Ui_iosRouterConfigPageWidget(object):
self.uiNameLabel.setText(_translate("iosRouterConfigPageWidget", "Name:", None))
self.uiPlatformLabel.setText(_translate("iosRouterConfigPageWidget", "Platform:", None))
self.uiChassisLabel.setText(_translate("iosRouterConfigPageWidget", "Chassis:", None))
self.uiIOSImageLabel.setText(_translate("iosRouterConfigPageWidget", "IOS image:", None))
self.uiIOSImageLabel.setText(_translate("iosRouterConfigPageWidget", "IOS image path:", None))
self.uiIOSImageToolButton.setText(_translate("iosRouterConfigPageWidget", "&Browse...", None))
self.uiStartupConfigLabel.setText(_translate("iosRouterConfigPageWidget", "Startup-config:", None))
self.uiStartupConfigLabel.setText(_translate("iosRouterConfigPageWidget", "Initial startup-config:", None))
self.uiStartupConfigToolButton.setText(_translate("iosRouterConfigPageWidget", "&Browse...", None))
self.uiPrivateConfigLabel.setText(_translate("iosRouterConfigPageWidget", "Private-config:", None))
self.uiPrivateConfigLabel.setText(_translate("iosRouterConfigPageWidget", "Initial private-config:", None))
self.uiPrivateConfigToolButton.setText(_translate("iosRouterConfigPageWidget", "&Browse...", None))
self.uiConsolePortLabel.setText(_translate("iosRouterConfigPageWidget", "Console port:", None))
self.uiAuxPortLabel.setText(_translate("iosRouterConfigPageWidget", "Aux port:", None))

View File

@@ -30,7 +30,7 @@
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
<pointsize>11</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>

View File

@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file '/Users/jseutter/projects/gns3-gui/gns3/modules/dynamips/ui/ios_router_preferences_page.ui'
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/dynamips/ui/ios_router_preferences_page.ui'
#
# Created: Tue Oct 14 11:35:46 2014
# Created: Wed Nov 19 18:57:20 2014
# by: PyQt4 UI code generator 4.10.4
#
# WARNING! All changes made in this file will be lost!
@@ -37,7 +37,7 @@ class Ui_IOSRouterPreferencesPageWidget(object):
self.uiIOSRoutersTreeWidget.setSizePolicy(sizePolicy)
self.uiIOSRoutersTreeWidget.setMaximumSize(QtCore.QSize(160, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
font.setPointSize(11)
font.setBold(True)
font.setWeight(75)
self.uiIOSRoutersTreeWidget.setFont(font)

View File

@@ -183,6 +183,13 @@
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QCheckBox" name="uiEtherSwitchCheckBox">
<property name="text">
<string>This is an EtherSwitch router</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWizardPage" name="uiMemoryWizardPage">

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/dynamips/ui/ios_router_wizard.ui'
#
# Created: Wed Oct 22 16:46:37 2014
# Created: Wed Dec 31 11:23:11 2014
# by: PyQt4 UI code generator 4.10.4
#
# WARNING! All changes made in this file will be lost!
@@ -111,6 +111,9 @@ class Ui_IOSRouterWizard(object):
self.uiChassisLabel = QtGui.QLabel(self.uiNamePlatformWizardPage)
self.uiChassisLabel.setObjectName(_fromUtf8("uiChassisLabel"))
self.gridLayout.addWidget(self.uiChassisLabel, 2, 0, 1, 1)
self.uiEtherSwitchCheckBox = QtGui.QCheckBox(self.uiNamePlatformWizardPage)
self.uiEtherSwitchCheckBox.setObjectName(_fromUtf8("uiEtherSwitchCheckBox"))
self.gridLayout.addWidget(self.uiEtherSwitchCheckBox, 3, 0, 1, 2)
IOSRouterWizard.addPage(self.uiNamePlatformWizardPage)
self.uiMemoryWizardPage = QtGui.QWizardPage()
self.uiMemoryWizardPage.setObjectName(_fromUtf8("uiMemoryWizardPage"))
@@ -299,6 +302,7 @@ class Ui_IOSRouterWizard(object):
self.uiTypeLabel.setText(_translate("IOSRouterWizard", "Platform:", None))
self.uiNameLabel.setText(_translate("IOSRouterWizard", "Name:", None))
self.uiChassisLabel.setText(_translate("IOSRouterWizard", "Chassis:", None))
self.uiEtherSwitchCheckBox.setText(_translate("IOSRouterWizard", "This is an EtherSwitch router", None))
self.uiMemoryWizardPage.setTitle(_translate("IOSRouterWizard", "Memory", None))
self.uiMemoryWizardPage.setSubTitle(_translate("IOSRouterWizard", "Please check the amount of memory (RAM) that you allocate to IOS. Not enough RAM could prevent IOS to start.", None))
self.uiRamLabel.setText(_translate("IOSRouterWizard", "Default RAM:", None))
@@ -320,7 +324,7 @@ class Ui_IOSRouterWizard(object):
self.uiWic1Label.setText(_translate("IOSRouterWizard", "wic 1:", None))
self.uiWic2Label.setText(_translate("IOSRouterWizard", "wic 2:", None))
self.uiIdlePCWizardPage.setTitle(_translate("IOSRouterWizard", "Idle-PC", None))
self.uiIdlePCWizardPage.setSubTitle(_translate("IOSRouterWizard", "An Idle-PC value is necessary to prevent IOS to use 100% of your processor or one of its core.", None))
self.uiIdlePCWizardPage.setSubTitle(_translate("IOSRouterWizard", "An idle-pc value is necessary to prevent IOS to use 100% of your processor or one of its core.", None))
self.uiIdlepcLabel.setText(_translate("IOSRouterWizard", "Idle-PC:", None))
self.uiIdlePCFinderPushButton.setText(_translate("IOSRouterWizard", "Idle-PC finder", None))

View File

@@ -29,6 +29,7 @@ from ..module import Module
from ..module_error import ModuleError
from .iou_device import IOUDevice
from .settings import IOU_SETTINGS, IOU_SETTING_TYPES
from .settings import IOU_DEVICE_SETTINGS, IOU_DEVICE_SETTING_TYPES
import logging
log = logging.getLogger(__name__)
@@ -91,33 +92,14 @@ class IOU(Module):
size = settings.beginReadArray("iou_device")
for index in range(0, size):
settings.setArrayIndex(index)
name = settings.value("name", "IOU{}".format(index + 1))
default_symbol = settings.value("default_symbol", ":/symbols/multilayer_switch.normal.svg")
hover_symbol = settings.value("hover_symbol", ":/symbols/multilayer_switch.selected.svg")
category = settings.value("category", Node.routers, type=int)
path = settings.value("path", "")
image = settings.value("image", "")
initial_config = settings.value("initial_config", "")
use_default_iou_values = settings.value("use_default_iou_values", True, type=bool)
ram = settings.value("ram", 256, type=int)
nvram = settings.value("nvram", 128, type=int)
ethernet_adapters = settings.value("ethernet_adapters", 2, type=int)
serial_adapters = settings.value("serial_adapters", 2, type=int)
server = settings.value("server", "local")
name = settings.value("name")
server = settings.value("server")
key = "{server}:{name}".format(server=server, name=name)
self._iou_devices[key] = {"name": name,
"path": path,
"default_symbol": default_symbol,
"hover_symbol": hover_symbol,
"category": category,
"image": image,
"initial_config": initial_config,
"use_default_iou_values": use_default_iou_values,
"ram": ram,
"nvram": nvram,
"ethernet_adapters": ethernet_adapters,
"serial_adapters": serial_adapters,
"server": server}
if key in self._iou_devices or not name or not server:
continue
self._iou_devices[key] = {}
for setting_name, default_value in IOU_DEVICE_SETTINGS.items():
self._iou_devices[key][setting_name] = settings.value(setting_name, default_value, IOU_DEVICE_SETTING_TYPES[setting_name])
settings.endArray()
settings.endGroup()
@@ -286,8 +268,11 @@ class IOU(Module):
if params:
if "iourc" in params:
# encode the iourc file in base64
params["iourc"] = self._base64iourc(params["iourc"])
if os.path.isfile(params["iourc"]):
# encode the iourc file in base64
params["iourc"] = self._base64iourc(params["iourc"])
else:
del params["iourc"]
for server in self._servers:
# send the local working directory only if this is a local server
if server.isLocal():
@@ -314,8 +299,11 @@ class IOU(Module):
log.info("sending IOU settings to server {}:{}".format(server.host, server.port))
params = self._settings.copy()
# encode the iourc file in base64
params["iourc"] = self._base64iourc(params["iourc"])
if os.path.isfile(params["iourc"]):
# encode the iourc file in base64
params["iourc"] = self._base64iourc(params["iourc"])
else:
del params["iourc"]
# send the local working directory only if this is a local server
if server.isLocal():
@@ -339,7 +327,7 @@ class IOU(Module):
log.info("creating node {}".format(node_class))
if not self._settings["iourc"] or not os.path.isfile(self._settings["iourc"]):
if server.isLocal() and (not self._settings["iourc"] or not os.path.isfile(self._settings["iourc"])):
raise ModuleError("The path to IOURC must be configured")
if not server.connected():
@@ -407,7 +395,12 @@ class IOU(Module):
settings["nvram"] = self._iou_devices[iouimage]["nvram"]
settings["ethernet_adapters"] = self._iou_devices[iouimage]["ethernet_adapters"]
settings["serial_adapters"] = self._iou_devices[iouimage]["serial_adapters"]
node.setup(iou_path, initial_settings=settings)
if node.server().isCloud():
settings["cloud_path"] = "images/IOU"
node.setup(self._iou_devices[iouimage]["image"], initial_settings=settings)
else:
node.setup(iou_path, initial_settings=settings)
def reset(self):
"""
@@ -444,8 +437,8 @@ class IOU(Module):
"""
for node in self._nodes:
if hasattr(node, "exportConfig") and node.initialized():
node.exportConfig(directory)
if node.initialized():
node.exportConfigToDirectory(directory)
def importConfigs(self, directory):
"""
@@ -455,8 +448,8 @@ class IOU(Module):
"""
for node in self._nodes:
if hasattr(node, "importConfig") and node.initialized():
node.importConfig(directory)
if node.initialized():
node.importConfigFromDirectory(directory)
def findAlternativeIOUImage(self, image):
"""

View File

@@ -21,12 +21,14 @@ Wizard for IOU devices.
import os
import sys
import pkg_resources
from gns3.qt import QtGui
from gns3.node import Node
from gns3.servers import Servers
from gns3.utils.get_resource import get_resource
from gns3.utils.get_default_base_config import get_default_base_config
from ....settings import ENABLE_CLOUD
from ..ui.iou_device_wizard_ui import Ui_IOUDeviceWizard
from .. import IOU
@@ -38,12 +40,15 @@ class IOUDeviceWizard(QtGui.QWizard, Ui_IOUDeviceWizard):
:param parent: parent widget
"""
def __init__(self, parent):
def __init__(self, iou_devices, parent):
QtGui.QWizard.__init__(self, parent)
self.setupUi(self)
self.setPixmap(QtGui.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/multilayer_switch.normal.svg"))
self.setWizardStyle(QtGui.QWizard.ModernStyle)
if sys.platform.startswith("darwin"):
# we want to see the cancel button on OSX
self.setOptions(QtGui.QWizard.NoDefaultButton)
self.uiRemoteRadioButton.toggled.connect(self._remoteServerToggledSlot)
self.uiLoadBalanceCheckBox.toggled.connect(self._loadBalanceToggledSlot)
@@ -61,12 +66,21 @@ class IOUDeviceWizard(QtGui.QWizard, Ui_IOUDeviceWizard):
self.uiNameImageWizardPage.registerField("name*", self.uiNameLineEdit)
self.uiNameImageWizardPage.registerField("image*", self.uiIOUImageLineEdit)
self._iou_devices = iou_devices
if IOU.instance().settings()["use_local_server"]:
# skip the server page if we use the local server
self.setStartId(1)
else:
self.uiIOUImageToolButton.setEnabled(False)
if not ENABLE_CLOUD:
self.uiCloudRadioButton.hide()
# location of the base config templates
self._base_iou_l2_config_template = get_resource(os.path.join("configs", "iou_l2_base_initial-config.txt"))
self._base_iou_l3_config_template = get_resource(os.path.join("configs", "iou_l3_base_initial-config.txt"))
def _remoteServerToggledSlot(self, checked):
"""
Slot for when the remote server radio button is toggled.
@@ -138,11 +152,15 @@ class IOUDeviceWizard(QtGui.QWizard, Ui_IOUDeviceWizard):
Validates the server.
"""
if self.currentPage() == self.uiServerWizardPage:
#FIXME: prevent users to use "cloud"
if self.uiCloudRadioButton.isChecked():
QtGui.QMessageBox.critical(self, "Cloud", "Sorry not implemented yet!")
if self.currentPage() == self.uiNameImageWizardPage:
name = self.uiNameLineEdit.text()
for iou_device in self._iou_devices.values():
if iou_device["name"] == name:
QtGui.QMessageBox.critical(self, "Name", "{} is already used, please choose another name".format(name))
return False
if self.currentPage() == self.uiServerWizardPage and self.uiRemoteRadioButton.isChecked():
if not Servers.instance().remoteServers():
QtGui.QMessageBox.critical(self, "Remote server", "There is no remote server registered in IOS on UNIX preferences")
return False
return True
@@ -155,45 +173,46 @@ class IOUDeviceWizard(QtGui.QWizard, Ui_IOUDeviceWizard):
path = self.uiIOUImageLineEdit.text()
initial_config = ""
if self.uiTypeComboBox.currentText() == "L2 image":
# set the default L2 base initial-config
resource_name = "configs/iou_l2_base_initial-config.txt"
if hasattr(sys, "frozen") and os.path.isfile(resource_name):
initial_config = os.path.normpath(resource_name)
elif pkg_resources.resource_exists("gns3", resource_name):
iou_base_config_path = pkg_resources.resource_filename("gns3", resource_name)
initial_config = os.path.normpath(iou_base_config_path)
default_base_config = get_default_base_config(self._base_iou_l2_config_template)
if default_base_config:
initial_config = default_base_config
default_symbol = ":/symbols/multilayer_switch.normal.svg"
hover_symbol = ":/symbols/multilayer_switch.selected.svg"
category = Node.switches
ethernet_adapters = 4
serial_adapters = 0
else:
# set the default L3 base initial-config
resource_name = "configs/iou_l3_base_initial-config.txt"
if hasattr(sys, "frozen") and os.path.isfile(resource_name):
initial_config = os.path.normpath(resource_name)
elif pkg_resources.resource_exists("gns3", resource_name):
iou_base_config_path = pkg_resources.resource_filename("gns3", resource_name)
initial_config = os.path.normpath(iou_base_config_path)
default_base_config = get_default_base_config(self._base_iou_l3_config_template)
if default_base_config:
initial_config = default_base_config
default_symbol = ":/symbols/router.normal.svg"
hover_symbol = ":/symbols/router.selected.svg"
category = Node.routers
ethernet_adapters = 2
serial_adapters = 2
if IOU.instance().settings()["use_local_server"] or self.uiLocalRadioButton.isChecked():
server = "local"
elif self.uiLoadBalanceCheckBox.isChecked():
server = next(iter(Servers.instance()))
if not server:
QtGui.QMessageBox.critical(self, "IOU device", "No remote server available!")
return
server = "{}:{}".format(server.host, server.port)
else:
server = self.uiRemoteServersComboBox.currentText()
elif self.uiRemoteRadioButton.isChecked():
if self.uiLoadBalanceCheckBox.isChecked():
server = next(iter(Servers.instance()))
server = "{}:{}".format(server.host, server.port)
else:
server = self.uiRemoteServersComboBox.currentText()
else: # Cloud is selected
server = "cloud"
settings = {
"name": self.uiNameLineEdit.text(),
"path": path,
"image": os.path.basename(path),
"initial_config": initial_config,
"ethernet_adapters": ethernet_adapters,
"serial_adapters": serial_adapters,
"default_symbol": default_symbol,
"category": category,
"hover_symbol": hover_symbol,

View File

@@ -20,12 +20,14 @@ IOU device implementation.
"""
import os
import re
import base64
from gns3.node import Node
from gns3.ports.port import Port
from gns3.ports.ethernet_port import EthernetPort
from gns3.ports.serial_port import SerialPort
from gns3.utils.normalize_filename import normalize_filename
from .settings import IOU_DEVICE_SETTINGS
import logging
log = logging.getLogger(__name__)
@@ -54,11 +56,11 @@ class IOUDevice(Node):
"path": "",
"initial_config": "",
"l1_keepalives": False,
"use_default_iou_values": True,
"ram": 256,
"nvram": 128,
"ethernet_adapters": 2,
"serial_adapters": 2,
"use_default_iou_values": IOU_DEVICE_SETTINGS["use_default_iou_values"],
"ram": IOU_DEVICE_SETTINGS["ram"],
"nvram": IOU_DEVICE_SETTINGS["nvram"],
"ethernet_adapters": IOU_DEVICE_SETTINGS["ethernet_adapters"],
"serial_adapters": IOU_DEVICE_SETTINGS["serial_adapters"],
"console": None}
#self._occupied_slots = []
@@ -85,7 +87,9 @@ class IOUDevice(Node):
else:
port = SerialPort
port_name = port.longNameType() + str(slot_number) + "/" + str(port_number)
short_name = port.shortNameType() + str(slot_number) + "/" + str(port_number)
new_port = port(port_name)
new_port.setShortName(short_name)
new_port.setPortNumber(port_number)
new_port.setSlotNumber(slot_number)
new_port.setPacketCaptureSupported(True)
@@ -133,6 +137,9 @@ class IOUDevice(Node):
if console:
params["console"] = self._settings["console"] = console
if "cloud_path" in initial_settings:
params["cloud_path"] = self._settings["cloud_path"] = initial_settings.pop("cloud_path")
# other initial settings will be applied when the router has been created
if initial_settings:
self._inital_settings = initial_settings
@@ -497,7 +504,7 @@ class IOUDevice(Node):
try:
port.startPacketCapture(result["capture_file_path"])
except OSError as e:
self.error_signal.emit(self.id(), "could not start the packet capture reader: {}".format(e))
self.error_signal.emit(self.id(), "could not start the packet capture reader: {}: {}".format(e, e.filename))
self.updated_signal.emit()
break
@@ -614,7 +621,7 @@ class IOUDevice(Node):
# save only the image name if it is stored the images directory
iou["properties"]["path"] = os.path.basename(image_path)
else:
iou["properties"]["path"] = os.path.basename(image_path)
iou["properties"]["path"] = image_path
return iou
@@ -671,7 +678,39 @@ class IOUDevice(Node):
self._inital_settings = None
self._loading = False
def exportConfig(self, directory):
def exportConfig(self, config_export_path):
"""
Exports the initial-config
:param config_export_path: export path for the initial-config
"""
self._config_export_path = config_export_path
self._server.send_message("iou.export_config", {"id": self._iou_id}, self._exportConfigCallback)
def _exportConfigCallback(self, result, error=False):
"""
Callback for exportConfig.
:param result: server response
:param error: indicates an error (boolean)
"""
if error:
log.error("error while exporting {} initial-config: {}".format(self.name(), result["message"]))
self.server_error_signal.emit(self.id(), result["code"], result["message"])
else:
if "initial_config_base64" in result and self._config_export_path:
config = base64.decodebytes(result["initial_config_base64"].encode("utf-8"))
try:
with open(self._config_export_path, "wb") as f:
log.info("saving {} initial-config to {}".format(self.name(), self._config_export_path))
f.write(config)
except OSError as e:
self.error_signal.emit(self.id(), "could not export initial-config to {}: {}".format(self._config_export_path, e))
def exportConfigToDirectory(self, directory):
"""
Exports the initial-config to a directory.
@@ -679,11 +718,11 @@ class IOUDevice(Node):
"""
self._export_directory = directory
self._server.send_message("iou.export_config", {"id": self._iou_id}, self._exportConfigCallback)
self._server.send_message("iou.export_config", {"id": self._iou_id}, self._exportConfigToDirectoryCallback)
def _exportConfigCallback(self, result, error=False):
def _exportConfigToDirectoryCallback(self, result, error=False):
"""
Callback for exportConfigs.
Callback for exportConfigToDirectory.
:param result: server response
:param error: indicates an error (boolean)
@@ -706,7 +745,17 @@ class IOUDevice(Node):
self._export_directory = None
def importConfig(self, directory):
def importConfig(self, path):
"""
Imports an initial-config.
:param path: path to the initial config
"""
new_settings = {"initial_config": path}
self.update(new_settings)
def importConfigFromDirectory(self, directory):
"""
Imports an initial-config from a directory.
@@ -769,6 +818,23 @@ class IOUDevice(Node):
from .pages.iou_device_configuration_page import iouDeviceConfigurationPage
return iouDeviceConfigurationPage
@staticmethod
def validateHostname(hostname):
"""
Checks if the hostname is valid.
:param hostname: hostname to check
:returns: boolean
"""
# IOS names must start with a letter, end with a letter or digit, and
# have as interior characters only letters, digits, and hyphens.
# They must be 63 characters or fewer.
if re.search(r"""^[\-\w]+$""", hostname) and len(hostname) <= 63:
return True
return False
@staticmethod
def defaultSymbol():
"""

View File

@@ -20,12 +20,11 @@ Configuration page for IOU devices.
"""
import os
import re
import sys
import pkg_resources
from gns3.qt import QtGui
from gns3.qt import QtCore, QtGui
from gns3.dialogs.node_configurator_dialog import ConfigurationError
from gns3.utils.get_resource import get_resource
from gns3.utils.get_default_base_config import get_default_base_config
from ..ui.iou_device_configuration_page_ui import Ui_iouDeviceConfigPageWidget
@@ -43,6 +42,10 @@ class iouDeviceConfigurationPage(QtGui.QWidget, Ui_iouDeviceConfigPageWidget):
self.uiDefaultValuesCheckBox.stateChanged.connect(self._useDefaultValuesSlot)
self._current_iou_image = ""
# location of the base config templates
self._base_iou_l2_config_template = get_resource(os.path.join("configs", "iou_l2_base_initial-config.txt"))
self._base_iou_l3_config_template = get_resource(os.path.join("configs", "iou_l3_base_initial-config.txt"))
def _useDefaultValuesSlot(self, state):
"""
Slot to enable or not the RAM and NVRAM spin boxes.
@@ -67,15 +70,23 @@ class iouDeviceConfigurationPage(QtGui.QWidget, Ui_iouDeviceConfigPageWidget):
self.uiIOUImageLineEdit.clear()
self.uiIOUImageLineEdit.setText(path)
if "l2" in path:
# set the default L2 base initial-config
default_base_config = get_default_base_config(self._base_iou_l2_config_template)
if default_base_config:
self.uiInitialConfigLineEdit.setText(default_base_config)
else:
# set the default L3 base initial-config
default_base_config = get_default_base_config(self._base_iou_l3_config_template)
if default_base_config:
self.uiInitialConfigLineEdit.setText(default_base_config)
def _initialConfigBrowserSlot(self):
"""
Slot to open a file browser and select a initial-config file.
"""
if hasattr(sys, "frozen"):
config_dir = "configs"
else:
config_dir = pkg_resources.resource_filename("gns3", "configs")
config_dir = os.path.join(os.path.dirname(QtCore.QSettings().fileName()), "base_configs")
path = QtGui.QFileDialog.getOpenFileName(self, "Select an initial configuration", config_dir)
if not path:
return
@@ -105,15 +116,20 @@ class iouDeviceConfigurationPage(QtGui.QWidget, Ui_iouDeviceConfigPageWidget):
self.uiConsolePortLabel.hide()
self.uiConsolePortSpinBox.hide()
# load the initial-config
self.uiInitialConfigLineEdit.setText(settings["initial_config"])
# load the IOU image path
self.uiIOUImageLineEdit.setText(settings["path"])
else:
self.uiGeneralgroupBox.hide()
if not node:
# load the initial-config
self.uiInitialConfigLineEdit.setText(settings["initial_config"])
else:
self.uiInitialConfigLabel.hide()
self.uiInitialConfigLineEdit.hide()
self.uiInitialConfigToolButton.hide()
# load advanced settings
if "l1_keepalives" in settings:
self.uiL1KeepalivesCheckBox.setChecked(settings["l1_keepalives"])
@@ -144,23 +160,13 @@ class iouDeviceConfigurationPage(QtGui.QWidget, Ui_iouDeviceConfigPageWidget):
name = self.uiNameLineEdit.text()
if not name:
QtGui.QMessageBox.critical(self, "Name", "IOU device name cannot be empty!")
elif node and not re.search(r"""^[\-\w]+$""", name):
# IOS names must start with a letter, end with a letter or digit, and
# have as interior characters only letters, digits, and hyphens.
# They must be 63 characters or fewer.
elif node and not node.validateHostname(name):
QtGui.QMessageBox.critical(self, "Name", "Invalid name detected for IOU device: {}".format(name))
else:
settings["name"] = name
settings["console"] = self.uiConsolePortSpinBox.value()
initial_config = self.uiInitialConfigLineEdit.text()
if initial_config != settings["initial_config"]:
if os.access(initial_config, os.R_OK):
settings["initial_config"] = initial_config
else:
QtGui.QMessageBox.critical(self, "Initial-config", "Cannot read the initial-config file")
# save the IOU image path
ios_path = self.uiIOUImageLineEdit.text().strip()
if ios_path:
@@ -169,6 +175,14 @@ class iouDeviceConfigurationPage(QtGui.QWidget, Ui_iouDeviceConfigPageWidget):
del settings["name"]
del settings["console"]
if not node:
initial_config = self.uiInitialConfigLineEdit.text()
if initial_config != settings["initial_config"]:
if os.access(initial_config, os.R_OK):
settings["initial_config"] = initial_config
else:
QtGui.QMessageBox.critical(self, "Initial-config", "Cannot read the initial-config file")
# save advanced settings
settings["l1_keepalives"] = self.uiL1KeepalivesCheckBox.isChecked()
settings["use_default_iou_values"] = self.uiDefaultValuesCheckBox.isChecked()

View File

@@ -21,16 +21,18 @@ Configuration page for IOU device preferences.
import copy
import os
import sys
import pkg_resources
import shutil
import stat
from gns3.qt import QtCore, QtGui
from gns3.main_window import MainWindow
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
from gns3.dialogs.configuration_dialog import ConfigurationDialog
from gns3.cloud.utils import UploadFilesThread
from gns3.utils.progress_dialog import ProgressDialog
from gns3.utils.file_copy_thread import FileCopyThread
from .. import IOU
from ..settings import IOU_DEVICE_SETTINGS
from ..ui.iou_device_preferences_page_ui import Ui_IOUDevicePreferencesPageWidget
from ..pages.iou_device_configuration_page import iouDeviceConfigurationPage
from ..dialogs.iou_device_wizard import IOUDeviceWizard
@@ -55,9 +57,6 @@ class IOUDevicePreferencesPage(QtGui.QWidget, Ui_IOUDevicePreferencesPageWidget)
self.uiIOUDevicesTreeWidget.currentItemChanged.connect(self._iouDeviceChangedSlot)
self.uiIOUDevicesTreeWidget.itemPressed.connect(self._iouDevicePressedSlot)
# self.uiIOUPathToolButton.clicked.connect(self._iouImageBrowserSlot)
# self.uiInitialConfigToolButton.clicked.connect(self._initialConfigBrowserSlot)
def _iouDeviceChangedSlot(self, current, previous):
"""
Loads a selected an IOU device from the tree widget.
@@ -81,28 +80,15 @@ class IOUDevicePreferencesPage(QtGui.QWidget, Ui_IOUDevicePreferencesPageWidget)
Creates a new IOU device.
"""
wizard = IOUDeviceWizard(parent=self)
wizard = IOUDeviceWizard(self._iou_devices, parent=self)
wizard.show()
if wizard.exec_():
new_device_settings = wizard.getSettings()
key = "{server}:{name}".format(server=new_device_settings["server"], name=new_device_settings["name"])
self._iou_devices[key] = {"name": new_device_settings["name"],
"path": new_device_settings["path"],
"default_symbol": new_device_settings["default_symbol"],
"hover_symbol": new_device_settings["hover_symbol"],
"category": new_device_settings["category"],
"image": os.path.basename(new_device_settings["path"]),
"initial_config": new_device_settings["initial_config"],
"use_default_iou_values": True,
"ram": 256,
"nvram": 128,
"ethernet_adapters": 2,
"serial_adapters": 2,
"server": new_device_settings["server"]}
self._iou_devices[key] = IOU_DEVICE_SETTINGS.copy()
self._iou_devices[key].update(new_device_settings)
item = QtGui.QTreeWidgetItem(self.uiIOUDevicesTreeWidget)
item.setText(0, self._iou_devices[key]["name"])
item.setIcon(0, QtGui.QIcon(self._iou_devices[key]["default_symbol"]))
@@ -110,6 +96,33 @@ class IOUDevicePreferencesPage(QtGui.QWidget, Ui_IOUDevicePreferencesPageWidget)
self._items.append(item)
self.uiIOUDevicesTreeWidget.setCurrentItem(item)
if new_device_settings["server"] == 'cloud':
import logging
log = logging.getLogger(__name__)
# Start uploading the image to cloud files
self._upload_image_progress_dialog = QtGui.QProgressDialog(
"Uploading image file {}".format(new_device_settings['image']), "Cancel", 0, 0, parent=self)
self._upload_image_progress_dialog.setWindowModality(QtCore.Qt.WindowModal)
self._upload_image_progress_dialog.setWindowTitle("IOU image upload")
self._upload_image_progress_dialog.show()
try:
src = self._iou_devices[key]['path']
# Eg: images/IOU/i86.bin
dst = 'images/IOU/{}'.format(self._iou_devices[key]['image'])
upload_thread = UploadFilesThread(self, MainWindow.instance().cloudSettings(), [(src, dst)])
upload_thread.completed.connect(self._imageUploadComplete)
upload_thread.start()
except Exception as e:
self._upload_image_progress_dialog.reject()
log.error(e)
QtGui.QMessageBox.critical(self, "IOU image upload", "Error uploading IOU image: {}".format(e))
def _imageUploadComplete(self):
if self._upload_image_progress_dialog.wasCanceled():
return
self._upload_image_progress_dialog.accept()
def _iouDeviceEditSlot(self):
"""
Edits an IOU device.
@@ -123,11 +136,16 @@ class IOUDevicePreferencesPage(QtGui.QWidget, Ui_IOUDevicePreferencesPageWidget)
dialog.show()
if dialog.exec_():
if iou_device["name"] != item.text(0):
if "{}:{}".format(iou_device["server"], iou_device["name"]) in self._iou_devices:
# FIXME: bug when changing name
QtGui.QMessageBox.critical(self, "New IOU device", "IOU device name {} already exists".format(iou_device["name"]))
new_key = "{server}:{name}".format(server=iou_device["server"], name=iou_device["name"])
if new_key in self._iou_devices:
QtGui.QMessageBox.critical(self, "IOU device", "IOU device name {} already exists for server {}".format(iou_device["name"],
iou_device["server"]))
iou_device["name"] = item.text(0)
return
self._iou_devices[new_key] = self._iou_devices[key]
del self._iou_devices[key]
item.setText(0, iou_device["name"])
item.setData(0, QtCore.Qt.UserRole, new_key)
self._refreshInfo(iou_device)
def _iouDeviceDeleteSlot(self):
@@ -216,83 +234,41 @@ class IOUDevicePreferencesPage(QtGui.QWidget, Ui_IOUDevicePreferencesPageWidget)
QtGui.QMessageBox.critical(parent, "IOU image", "Sorry, this is not a valid IOU image!")
return
if not os.access(path, os.X_OK):
QtGui.QMessageBox.critical(parent, "IOU image", "{} is not executable".format(path))
return
try:
os.makedirs(destination_directory)
except FileExistsError:
pass
except OSError as e:
QtGui.QMessageBox.critical(parent, "IOU images directory", "Could not create the IOU images directory {}: {}".format(destination_directory, str(e)))
QtGui.QMessageBox.critical(parent, "IOU images directory", "Could not create the IOU images directory {}: {}".format(destination_directory, e))
return
if os.path.dirname(path) != destination_directory:
if os.path.normpath(os.path.dirname(path)) != destination_directory:
# the IOU image is not in the default images directory
new_destination_path = os.path.join(destination_directory, os.path.basename(path))
try:
# try to create a symbolic link to it
symlink_path = new_destination_path
os.symlink(path, symlink_path)
path = symlink_path
except (OSError, NotImplementedError):
# if unsuccessful, then copy the IOU image itself
try:
shutil.copyfile(path, new_destination_path)
path = new_destination_path
except OSError:
pass
reply = QtGui.QMessageBox.question(parent,
"IOU image",
"Would you like to copy {} to the default images directory".format(os.path.basename(path)),
QtGui.QMessageBox.Yes,
QtGui.QMessageBox.No)
if reply == QtGui.QMessageBox.Yes:
destination_path = os.path.join(destination_directory, os.path.basename(path))
thread = FileCopyThread(path, destination_path)
progress_dialog = ProgressDialog(thread, "Project", "Copying {}".format(os.path.basename(path)), "Cancel", busy=True, parent=parent)
thread.deleteLater()
progress_dialog.show()
progress_dialog.exec_()
errors = progress_dialog.errors()
if errors:
QtGui.QMessageBox.critical(parent, "IOS image", "{}".format("".join(errors)))
else:
path = destination_path
mode = os.stat(path).st_mode
os.chmod(path, mode | stat.S_IXUSR)
if not os.access(path, os.X_OK):
QtGui.QMessageBox.warning(parent, "IOU image", "{} is not executable".format(path))
return path
def _iouImageBrowserSlot(self):
"""
Slot to open a file browser and select an IOU image.
"""
path = self.getIOUImage(self)
if not path:
return
self.uiIOUPathLineEdit.clear()
self.uiIOUPathLineEdit.setText(path)
if "l2" in path:
# set the default L2 base initial-config
resource_name = "configs/iou_l2_base_initial-config.txt"
if hasattr(sys, "frozen") and os.path.isfile(resource_name):
self.uiInitialConfigLineEdit.setText(os.path.normpath(resource_name))
elif pkg_resources.resource_exists("gns3", resource_name):
iou_base_config_path = pkg_resources.resource_filename("gns3", resource_name)
self.uiInitialConfigLineEdit.setText(os.path.normpath(iou_base_config_path))
else:
# set the default L3 base initial-config
resource_name = "configs/iou_l3_base_initial-config.txt"
if hasattr(sys, "frozen") and os.path.isfile(resource_name):
self.uiInitialConfigLineEdit.setText(os.path.normpath(resource_name))
elif pkg_resources.resource_exists("gns3", resource_name):
iou_base_config_path = pkg_resources.resource_filename("gns3", resource_name)
self.uiInitialConfigLineEdit.setText(os.path.normpath(iou_base_config_path))
def _initialConfigBrowserSlot(self):
"""
Slot to open a file browser and select a initial-config file.
"""
if hasattr(sys, "frozen"):
config_dir = "configs"
else:
config_dir = pkg_resources.resource_filename("gns3", "configs")
path = QtGui.QFileDialog.getOpenFileName(self, "Select an initial configuration", config_dir)
if not path:
return
if not os.access(path, os.R_OK):
QtGui.QMessageBox.critical(self, "Initial configuration", "Cannot read {}".format(path))
return
self.uiInitialConfigLineEdit.clear()
self.uiInitialConfigLineEdit.setText(path)
def _iouDevicePressedSlot(self, item, column):
"""
Slot for item pressed.
@@ -321,16 +297,16 @@ class IOUDevicePreferencesPage(QtGui.QWidget, Ui_IOUDevicePreferencesPageWidget)
Change a symbol for an IOU device.
"""
dialog = SymbolSelectionDialog(self)
dialog.show()
if dialog.exec_():
normal_symbol, selected_symbol = dialog.getSymbols()
category = dialog.getCategory()
item = self.uiIOUDevicesTreeWidget.currentItem()
if item:
item = self.uiIOUDevicesTreeWidget.currentItem()
if item:
key = item.data(0, QtCore.Qt.UserRole)
iou_device = self._iou_devices[key]
dialog = SymbolSelectionDialog(self, symbol=iou_device["default_symbol"], category=iou_device["category"])
dialog.show()
if dialog.exec_():
normal_symbol, selected_symbol = dialog.getSymbols()
category = dialog.getCategory()
item.setIcon(0, QtGui.QIcon(normal_symbol))
key = item.data(0, QtCore.Qt.UserRole)
iou_device = self._iou_devices[key]
iou_device["default_symbol"] = normal_symbol
iou_device["hover_symbol"] = selected_symbol
iou_device["category"] = category

View File

@@ -15,13 +15,16 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
import os
"""
Default IOU settings.
"""
from gns3.node import Node
import sys
import os
if sys.platform.startswith("linux"):
paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep)
# look for iouyap in the current working directory and $PATH
@@ -59,3 +62,35 @@ IOU_SETTING_TYPES = {
"udp_end_port_range": int,
"use_local_server": bool,
}
IOU_DEVICE_SETTINGS = {
"name": "",
"path": "",
"default_symbol": ":/symbols/multilayer_switch.normal.svg",
"hover_symbol": ":/symbols/multilayer_switch.selected.svg",
"category": Node.routers,
"image": "",
"initial_config": "",
"use_default_iou_values": True,
"ram": 256,
"nvram": 128,
"ethernet_adapters": 2,
"serial_adapters": 2,
"server": "local"
}
IOU_DEVICE_SETTING_TYPES = {
"name": str,
"path": str,
"default_symbol": str,
"hover_symbol": str,
"category": int,
"image": str,
"initial_config": str,
"use_default_iou_values": bool,
"ram": int,
"nvram": int,
"ethernet_adapters": int,
"serial_adapters": int,
"server": str
}

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>420</width>
<height>489</height>
<width>392</width>
<height>473</height>
</rect>
</property>
<property name="windowTitle">
@@ -46,7 +46,7 @@
<item row="1" column="0">
<widget class="QLabel" name="uiIOUImageLabel">
<property name="text">
<string>IOU image:</string>
<string>IOU image path:</string>
</property>
</widget>
</item>
@@ -70,7 +70,7 @@
<item row="2" column="0">
<widget class="QLabel" name="uiInitialConfigLabel">
<property name="text">
<string>Initial config:</string>
<string>Initial startup-config:</string>
</property>
</widget>
</item>

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/iou/ui/iou_device_configuration_page.ui'
#
# Created: Fri Oct 10 10:43:47 2014
# Created: Wed Dec 24 17:35:25 2014
# by: PyQt4 UI code generator 4.10.4
#
# WARNING! All changes made in this file will be lost!
@@ -26,7 +26,7 @@ except AttributeError:
class Ui_iouDeviceConfigPageWidget(object):
def setupUi(self, iouDeviceConfigPageWidget):
iouDeviceConfigPageWidget.setObjectName(_fromUtf8("iouDeviceConfigPageWidget"))
iouDeviceConfigPageWidget.resize(420, 489)
iouDeviceConfigPageWidget.resize(392, 473)
self.verticalLayout = QtGui.QVBoxLayout(iouDeviceConfigPageWidget)
self.verticalLayout.setObjectName(_fromUtf8("verticalLayout"))
self.uiTabWidget = QtGui.QTabWidget(iouDeviceConfigPageWidget)
@@ -177,9 +177,9 @@ class Ui_iouDeviceConfigPageWidget(object):
iouDeviceConfigPageWidget.setWindowTitle(_translate("iouDeviceConfigPageWidget", "IOU device configuration", None))
self.uiGeneralgroupBox.setTitle(_translate("iouDeviceConfigPageWidget", "General", None))
self.uiNameLabel.setText(_translate("iouDeviceConfigPageWidget", "Name:", None))
self.uiIOUImageLabel.setText(_translate("iouDeviceConfigPageWidget", "IOU image:", None))
self.uiIOUImageLabel.setText(_translate("iouDeviceConfigPageWidget", "IOU image path:", None))
self.uiIOUImageToolButton.setText(_translate("iouDeviceConfigPageWidget", "&Browse...", None))
self.uiInitialConfigLabel.setText(_translate("iouDeviceConfigPageWidget", "Initial config:", None))
self.uiInitialConfigLabel.setText(_translate("iouDeviceConfigPageWidget", "Initial startup-config:", None))
self.uiInitialConfigToolButton.setText(_translate("iouDeviceConfigPageWidget", "&Browse...", None))
self.uiConsolePortLabel.setText(_translate("iouDeviceConfigPageWidget", "Console port:", None))
self.uiOtherSettingsGroupBox.setTitle(_translate("iouDeviceConfigPageWidget", "Other settings", None))

View File

@@ -30,7 +30,7 @@
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
<pointsize>11</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/iou/ui/iou_device_preferences_page.ui'
#
# Created: Tue Oct 7 15:39:24 2014
# Created: Wed Nov 19 18:57:19 2014
# by: PyQt4 UI code generator 4.10.4
#
# WARNING! All changes made in this file will be lost!
@@ -37,7 +37,7 @@ class Ui_IOUDevicePreferencesPageWidget(object):
self.uiIOUDevicesTreeWidget.setSizePolicy(sizePolicy)
self.uiIOUDevicesTreeWidget.setMaximumSize(QtCore.QSize(160, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
font.setPointSize(11)
font.setBold(True)
font.setWeight(75)
self.uiIOUDevicesTreeWidget.setFont(font)

View File

@@ -28,6 +28,7 @@ from ..module import Module
from ..module_error import ModuleError
from .qemu_vm import QemuVM
from .settings import QEMU_SETTINGS, QEMU_SETTING_TYPES
from .settings import QEMU_VM_SETTINGS, QEMU_VM_SETTING_TYPES
import logging
log = logging.getLogger(__name__)
@@ -88,40 +89,14 @@ class Qemu(Module):
size = settings.beginReadArray("VM")
for index in range(0, size):
settings.setArrayIndex(index)
name = settings.value("name", "")
default_symbol = settings.value("default_symbol", ":/symbols/qemu_guest.normal.svg")
hover_symbol = settings.value("hover_symbol", ":/symbols/qemu_guest.selected.svg")
category = settings.value("category", Node.end_devices, type=int)
qemu_path = settings.value("qemu_path", "")
hda_disk_image = settings.value("hda_disk_image", "")
hdb_disk_image = settings.value("hdb_disk_image", "")
ram = settings.value("ram", 256, type=int)
adapters = settings.value("adapters", 1, type=int)
adapter_type = settings.value("adapter_type", "e1000")
initrd = settings.value("initrd", "")
kernel_image = settings.value("kernel_image", "")
kernel_command_line = settings.value("kernel_command_line", "")
options = settings.value("options", "")
server = settings.value("server", "local")
name = settings.value("name")
server = settings.value("server")
key = "{server}:{name}".format(server=server, name=name)
self._qemu_vms[key] = {"name": name,
"default_symbol": default_symbol,
"hover_symbol": hover_symbol,
"category": category,
"qemu_path": qemu_path,
"hda_disk_image": hda_disk_image,
"hdb_disk_image": hdb_disk_image,
"ram": ram,
"adapters": adapters,
"adapter_type": adapter_type,
"options": options,
"initrd": initrd,
"kernel_image": kernel_image,
"kernel_command_line": kernel_command_line,
"server": server}
if key in self._qemu_vms or not name or not server:
continue
self._qemu_vms[key] = {}
for setting_name, default_value in QEMU_VM_SETTINGS.items():
self._qemu_vms[key][setting_name] = settings.value(setting_name, default_value, QEMU_VM_SETTING_TYPES[setting_name])
settings.endArray()
settings.endGroup()
@@ -360,6 +335,7 @@ class Qemu(Module):
"adapters": self._qemu_vms[vm]["adapters"],
"adapter_type": self._qemu_vms[vm]["adapter_type"]}
#FIXME: this is ugly...
if self._qemu_vms[vm]["hda_disk_image"]:
settings["hda_disk_image"] = self._qemu_vms[vm]["hda_disk_image"]
@@ -375,11 +351,23 @@ class Qemu(Module):
if self._qemu_vms[vm]["kernel_command_line"]:
settings["kernel_command_line"] = self._qemu_vms[vm]["kernel_command_line"]
if self._qemu_vms[vm]["legacy_networking"]:
settings["legacy_networking"] = self._qemu_vms[vm]["legacy_networking"]
settings["cpu_throttling"] = self._qemu_vms[vm]["cpu_throttling"]
if self._qemu_vms[vm]["process_priority"]:
settings["process_priority"] = self._qemu_vms[vm]["process_priority"]
if self._qemu_vms[vm]["options"]:
settings["options"] = self._qemu_vms[vm]["options"]
qemu_path = self._qemu_vms[vm]["qemu_path"]
name = self._qemu_vms[vm]["name"]
if node.server().isCloud():
settings["cloud_path"] = "images/qemu"
node.setup(qemu_path, initial_settings=settings, base_name=name)
def reset(self):

View File

@@ -20,18 +20,18 @@ Wizard for QEMU VMs.
"""
import sys
import os
import shutil
from gns3.qt import QtCore, QtGui
from gns3.servers import Servers
from gns3.node import Node
from gns3.main_window import MainWindow
from gns3.modules.module_error import ModuleError
from gns3.utils.connect_to_server import ConnectToServer
from gns3.settings import ENABLE_CLOUD
from ..ui.qemu_vm_wizard_ui import Ui_QemuVMWizard
from .. import Qemu
from ..ui.qemu_vm_wizard_ui import Ui_QemuVMWizard
from ..pages.qemu_vm_configuration_page import QemuVMConfigurationPage
from ..settings import QEMU_BINARIES_FOR_CLOUD
class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
@@ -41,12 +41,15 @@ class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
:param parent: parent widget
"""
def __init__(self, parent):
def __init__(self, qemu_vms, parent):
QtGui.QWizard.__init__(self, parent)
self.setupUi(self)
self.setPixmap(QtGui.QWizard.LogoPixmap, QtGui.QPixmap(":/icons/qemu.svg"))
self.setWizardStyle(QtGui.QWizard.ModernStyle)
if sys.platform.startswith("darwin"):
# we want to see the cancel button on OSX
self.setOptions(QtGui.QWizard.NoDefaultButton)
self.uiRemoteRadioButton.toggled.connect(self._remoteServerToggledSlot)
self.uiHdaDiskImageToolButton.clicked.connect(self._hdaDiskImageBrowserSlot)
@@ -56,7 +59,7 @@ class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
self.uiTypeComboBox.currentIndexChanged[str].connect(self._typeChangedSlot)
# Available types
self.uiTypeComboBox.addItems(["Default", "ASA 8.4(2)", "IDS"])
self.uiTypeComboBox.addItems(["Default", "IOSv", "IOSv-L2", "ASA 8.4(2)", "IDS"])
# Mandatory fields
self.uiNameTypeWizardPage.registerField("vm_name*", self.uiNameLineEdit)
@@ -65,6 +68,8 @@ class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
self.uiASAWizardPage.registerField("initrd*", self.uiInitrdLineEdit)
self.uiASAWizardPage.registerField("kernel_image*", self.uiKernelImageLineEdit)
self._qemu_vms = qemu_vms
if Qemu.instance().settings()["use_local_server"]:
# skip the server page if we use the local server
self.setStartId(1)
@@ -72,6 +77,9 @@ class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
# By default we use the local server
self._server = Servers.instance().localServer()
if not ENABLE_CLOUD:
self.uiCloudRadioButton.hide()
def _remoteServerToggledSlot(self, checked):
"""
Slot for when the remote server radio button is toggled.
@@ -91,59 +99,32 @@ class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
:param vm_type: type of VM
"""
if vm_type == "ASA 8.4(2)":
if vm_type == "IOSv":
self.setPixmap(QtGui.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/iosv_virl.normal.svg"))
self.uiNameLineEdit.setText("vIOS")
self.uiHdaDiskImageLabel.setText("IOSv VDMK file:")
elif vm_type == "IOSv-L2":
self.setPixmap(QtGui.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/iosv_l2_virl.normal.svg"))
self.uiNameLineEdit.setText("vIOS-L2")
self.uiHdaDiskImageLabel.setText("IOSv-L2 VDMK file:")
elif vm_type == "ASA 8.4(2)":
self.setPixmap(QtGui.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/asa.normal.svg"))
self.uiNameLineEdit.setText("ASA")
elif vm_type == "IDS":
self.setPixmap(QtGui.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/ids.normal.svg"))
self.uiNameLineEdit.setText("IDS")
self.uiHdaDiskImageLabel.setText("Disk image (hda):")
else:
self.setPixmap(QtGui.QWizard.LogoPixmap, QtGui.QPixmap(":/icons/qemu.svg"))
def _getDiskImage(self):
destination_directory = os.path.join(MainWindow.instance().settings()["images_path"], "QEMU")
path, _ = QtGui.QFileDialog.getOpenFileNameAndFilter(self,
"Select a QEMU disk image",
destination_directory)
if not path:
return
if not os.access(path, os.R_OK):
QtGui.QMessageBox.critical(self, "QEMU disk image", "Cannot read {}".format(path))
return
try:
os.makedirs(destination_directory)
except FileExistsError:
pass
except OSError as e:
QtGui.QMessageBox.critical(self, "QEMU disk images directory", "Could not create the QEMU disk images directory {}: {}".format(destination_directory,
str(e)))
return
if os.path.dirname(path) != destination_directory:
# the QEMU disk image is not in the default images directory
new_destination_path = os.path.join(destination_directory, os.path.basename(path))
try:
# try to create a symbolic link to it
symlink_path = new_destination_path
os.symlink(path, symlink_path)
path = symlink_path
except (OSError, NotImplementedError):
# if unsuccessful, then copy the QEMU disk image itself
try:
shutil.copyfile(path, new_destination_path)
path = new_destination_path
except OSError:
pass
return path
self.uiHdaDiskImageLabel.setText("Disk image (hda):")
self.uiNameLineEdit.setText("")
def _hdaDiskImageBrowserSlot(self):
"""
Slot to open a file browser and select a QEMU hda disk image.
"""
path = self._getDiskImage()
path = QemuVMConfigurationPage.getDiskImage(self)
if path:
self.uiHdaDiskImageLineEdit.clear()
self.uiHdaDiskImageLineEdit.setText(path)
@@ -153,7 +134,7 @@ class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
Slot to open a file browser and select a QEMU hdb disk image.
"""
path = self._getDiskImage()
path = QemuVMConfigurationPage.getDiskImage(self)
if path:
self.uiHdbDiskImageLineEdit.clear()
self.uiHdbDiskImageLineEdit.setText(path)
@@ -163,7 +144,7 @@ class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
Slot to open a file browser and select a QEMU initrd.
"""
path = self._getDiskImage()
path = QemuVMConfigurationPage.getDiskImage(self)
if path:
self.uiInitrdLineEdit.clear()
self.uiInitrdLineEdit.setText(path)
@@ -173,7 +154,8 @@ class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
Slot to open a file browser and select a QEMU kernel image.
"""
path = self._getDiskImage()
from ..pages.qemu_vm_configuration_page import QemuVMConfigurationPage
path = QemuVMConfigurationPage.getDiskImage(self)
if path:
self.uiKernelImageLineEdit.clear()
self.uiKernelImageLineEdit.setText(path)
@@ -184,19 +166,24 @@ class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
"""
if self.currentPage() == self.uiServerWizardPage:
if not self.uiCloudRadioButton.isChecked():
if Qemu.instance().settings()["use_local_server"] or self.uiLocalRadioButton.isChecked():
server = Servers.instance().localServer()
elif self.uiRemoteRadioButton.isChecked():
if not Servers.instance().remoteServers():
QtGui.QMessageBox.critical(self, "Remote server", "There is no remote server registered in QEMU preferences")
return False
server = self.uiRemoteServersComboBox.itemData(self.uiRemoteServersComboBox.currentIndex())
if not server.connected() and ConnectToServer(self, server) is False:
return False
self._server = server
#FIXME: prevent users to use "cloud"
if self.uiCloudRadioButton.isChecked():
QtGui.QMessageBox.critical(self, "Cloud", "Sorry not implemented yet!")
return False
if Qemu.instance().settings()["use_local_server"] or self.uiLocalRadioButton.isChecked():
server = Servers.instance().localServer()
else:
server = self.uiRemoteServersComboBox.itemData(self.uiRemoteServersComboBox.currentIndex())
if not server.connected() and ConnectToServer(self, server) is False:
return False
self._server = server
if self.currentPage() == self.uiNameTypeWizardPage:
name = self.uiNameLineEdit.text()
for qemu_vm in self._qemu_vms.values():
if qemu_vm["name"] == name:
QtGui.QMessageBox.critical(self, "Name", "{} is already used, please choose another name".format(name))
return False
if self.currentPage() == self.uiBinaryMemoryWizardPage:
if not self.uiQemuListComboBox.count():
@@ -212,15 +199,23 @@ class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
for server in Servers.instance().remoteServers().values():
self.uiRemoteServersComboBox.addItem("{}:{}".format(server.host, server.port), server)
if self.page(page_id) == self.uiBinaryMemoryWizardPage:
self._qemu_binaries_progress_dialog = QtGui.QProgressDialog("Loading QEMU binaries", "Cancel", 0, 0, parent=self)
self._qemu_binaries_progress_dialog.setWindowModality(QtCore.Qt.WindowModal)
self._qemu_binaries_progress_dialog.setWindowTitle("QEMU binaries")
self._qemu_binaries_progress_dialog.show()
try:
Qemu.instance().getQemuBinariesFromServer(self._server, self._getQemuBinariesFromServerCallback)
except ModuleError as e:
self._qemu_binaries_progress_dialog.reject()
QtGui.QMessageBox.critical(self, "Qemu binaries", "Error while getting the QEMU binaries: {}".format(e))
if self.uiCloudRadioButton.isChecked():
for binary in QEMU_BINARIES_FOR_CLOUD:
self.uiQemuListComboBox.addItem("{path}".format(path=binary), binary)
# Default to x86_64 for the user
index = self.uiQemuListComboBox.findData("x86_64", flags=QtCore.Qt.MatchEndsWith)
if index != -1:
self.uiQemuListComboBox.setCurrentIndex(index)
else:
self._qemu_binaries_progress_dialog = QtGui.QProgressDialog("Loading QEMU binaries", "Cancel", 0, 0, parent=self)
self._qemu_binaries_progress_dialog.setWindowModality(QtCore.Qt.WindowModal)
self._qemu_binaries_progress_dialog.setWindowTitle("QEMU binaries")
self._qemu_binaries_progress_dialog.show()
try:
Qemu.instance().getQemuBinariesFromServer(self._server, self._getQemuBinariesFromServerCallback)
except ModuleError as e:
self._qemu_binaries_progress_dialog.reject()
QtGui.QMessageBox.critical(self, "Qemu binaries", "Error while getting the QEMU binaries: {}".format(e))
def _getQemuBinariesFromServerCallback(self, result, error=False):
"""
@@ -235,7 +230,7 @@ class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
self._qemu_binaries_progress_dialog.accept()
if error:
QtGui.QMessageBox.critical(self, "Qemu binaries", "Error: ".format(result["message"]))
QtGui.QMessageBox.critical(self, "Qemu binaries", "{}".format(result["message"]))
else:
self.uiQemuListComboBox.clear()
for qemu in result["qemus"]:
@@ -244,13 +239,26 @@ class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
else:
self.uiQemuListComboBox.addItem("{path}".format(path=qemu["path"]), qemu["path"])
#FIXME: use findData instead
is_64bit = sys.maxsize > 2**32
if sys.platform.startswith("win"):
# default is qemu-system-x86_64w on Windows
index = self.uiQemuListComboBox.findText("x86_64w", QtCore.Qt.MatchContains)
if self.uiTypeComboBox.currentText() != "Default" and (Qemu.instance().settings()["use_local_server"] or self.uiLocalRadioButton.isChecked()):
search_string = "qemu.exe"
elif is_64bit:
# default is qemu-system-x86_64w.exe on Windows 64-bit with a remote server
search_string = "x86_64w.exe"
else:
# default is qemu-system-i386w.exe on Windows 32-bit with a remote server
search_string = "i386w.exe"
elif sys.platform.startswith("darwin") and hasattr(sys, "frozen") and (Qemu.instance().settings()["use_local_server"] or self.uiLocalRadioButton.isChecked()):
search_string = "GNS3.app/Contents/Resources/qemu/bin/qemu-system-x86_64"
elif is_64bit:
# default is qemu-system-x86_64 on other 64-bit platforms
search_string = "x86_64"
else:
# default is qemu-system-x86_64 on other platforms
index = self.uiQemuListComboBox.findText("x86_64 ", QtCore.Qt.MatchContains) # the space after x86_64 must be present!
# default is qemu-system-i386 on other platforms
search_string = "i386"
index = self.uiQemuListComboBox.findData(search_string, flags=QtCore.Qt.MatchEndsWith)
if index != -1:
self.uiQemuListComboBox.setCurrentIndex(index)
@@ -263,8 +271,11 @@ class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
if Qemu.instance().settings()["use_local_server"] or self.uiLocalRadioButton.isChecked():
server = "local"
else:
elif self.uiRemoteRadioButton.isChecked():
server = self.uiRemoteServersComboBox.currentText()
else: # Cloud is selected
server = "cloud"
qemu_path = self.uiQemuListComboBox.itemData(self.uiQemuListComboBox.currentIndex())
settings = {
@@ -274,12 +285,27 @@ class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
"server": server,
}
if self.uiTypeComboBox.currentText() == "ASA 8.4(2)":
settings["adapters"] = 6
if self.uiTypeComboBox.currentText() == "IOSv":
settings["adapters"] = 8
settings["hda_disk_image"] = self.uiHdaDiskImageLineEdit.text()
settings["default_symbol"] = ":/symbols/iosv_virl.normal.svg"
settings["hover_symbol"] = ":/symbols/iosv_virl.selected.svg"
settings["category"] = Node.routers
elif self.uiTypeComboBox.currentText() == "IOSv-L2":
settings["adapters"] = 8
settings["hda_disk_image"] = self.uiHdaDiskImageLineEdit.text()
settings["default_symbol"] = ":/symbols/iosv_l2_virl.normal.svg"
settings["hover_symbol"] = ":/symbols/iosv_l2_virl.selected.svg"
settings["category"] = Node.switches
elif self.uiTypeComboBox.currentText() == "ASA 8.4(2)":
settings["adapters"] = 4
settings["initrd"] = self.uiInitrdLineEdit.text()
settings["kernel_image"] = self.uiKernelImageLineEdit.text()
settings["kernel_command_line"] = "ide_generic.probe_mask=0x01 ide_core.chs=0.0:980,16,32 auto nousb console=ttyS0,9600 bigphysarea=65536 ide1=noprobe no-hlt"
settings["options"] = "-nographic -cpu coreduo -icount auto -hdachs 980,16,32"
settings["options"] = "-icount auto -hdachs 980,16,32"
if not sys.platform.startswith("darwin"):
settings["cpu_throttling"] = 80 # limit to 80% CPU usage
settings["process_priority"] = "low"
settings["default_symbol"] = ":/symbols/asa.normal.svg"
settings["hover_symbol"] = ":/symbols/asa.selected.svg"
settings["category"] = Node.security_devices
@@ -295,6 +321,16 @@ class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
settings["hda_disk_image"] = self.uiHdaDiskImageLineEdit.text()
settings["category"] = Node.end_devices
if self.uiTypeComboBox.currentText() != "Default":
if not "options" in settings:
settings["options"] = ""
if server == "local" and (sys.platform.startswith("win") and qemu_path.endswith("qemu.exe")) or (sys.platform.startswith("darwin") and "GNS3.app" in qemu_path):
settings["options"] += " -vga none -vnc none"
settings["legacy_networking"] = True
else:
settings["options"] += " -nographic"
settings["options"] = settings["options"].strip()
return settings
def nextId(self):
@@ -305,7 +341,9 @@ class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
current_id = self.currentId()
if self.page(current_id) == self.uiNameTypeWizardPage:
if self.uiTypeComboBox.currentText() != "Default":
if self.uiTypeComboBox.currentText().startswith("IOSv"):
self.uiRamSpinBox.setValue(384)
elif self.uiTypeComboBox.currentText() != "Default":
self.uiRamSpinBox.setValue(1024)
elif self.page(current_id) == self.uiBinaryMemoryWizardPage:

View File

@@ -68,6 +68,8 @@ class QemuPreferencesPage(QtGui.QWidget, Ui_QemuPreferencesPageWidget):
self.uiUseLocalServercheckBox.setChecked(settings["use_local_server"])
self.uiConsoleStartPortSpinBox.setValue(settings["console_start_port_range"])
self.uiConsoleEndPortSpinBox.setValue(settings["console_end_port_range"])
self.uiMonitorStartPortSpinBox.setValue(settings["monitor_start_port_range"])
self.uiMonitorEndPortSpinBox.setValue(settings["monitor_end_port_range"])
self.uiUDPStartPortSpinBox.setValue(settings["udp_start_port_range"])
self.uiUDPEndPortSpinBox.setValue(settings["udp_end_port_range"])
@@ -108,6 +110,8 @@ class QemuPreferencesPage(QtGui.QWidget, Ui_QemuPreferencesPageWidget):
new_settings["use_local_server"] = self.uiUseLocalServercheckBox.isChecked()
new_settings["console_start_port_range"] = self.uiConsoleStartPortSpinBox.value()
new_settings["console_end_port_range"] = self.uiConsoleEndPortSpinBox.value()
new_settings["monitor_start_port_range"] = self.uiMonitorStartPortSpinBox.value()
new_settings["monitor_end_port_range"] = self.uiMonitorEndPortSpinBox.value()
new_settings["udp_start_port_range"] = self.uiUDPStartPortSpinBox.value()
new_settings["udp_end_port_range"] = self.uiUDPEndPortSpinBox.value()
Qemu.instance().setSettings(new_settings)

View File

@@ -20,17 +20,20 @@ Configuration page for QEMU VMs.
"""
import os
import shutil
from functools import partial
from collections import OrderedDict
from gns3.qt import QtCore, QtGui
from gns3.servers import Servers
from gns3.modules.module_error import ModuleError
from gns3.main_window import MainWindow
from gns3.dialogs.node_configurator_dialog import ConfigurationError
from gns3.utils.progress_dialog import ProgressDialog
from gns3.utils.file_copy_thread import FileCopyThread
from ..ui.qemu_vm_configuration_page_ui import Ui_QemuVMConfigPageWidget
from .. import Qemu
from ..settings import QEMU_BINARIES_FOR_CLOUD
class QemuVMConfigurationPage(QtGui.QWidget, Ui_QemuVMConfigPageWidget):
@@ -47,28 +50,45 @@ class QemuVMConfigurationPage(QtGui.QWidget, Ui_QemuVMConfigPageWidget):
self.uiHdbDiskImageToolButton.clicked.connect(self._hdbDiskImageBrowserSlot)
self.uiInitrdToolButton.clicked.connect(self._initrdBrowserSlot)
self.uiKernelImageToolButton.clicked.connect(self._kernelImageBrowserSlot)
self.uiActivateCPUThrottlingCheckBox.stateChanged.connect(self._cpuThrottlingChangedSlot)
qemu_network_devices = OrderedDict([
("e1000", "Intel Gigabit Ethernet"),
("i82550", "Intel i82550 Ethernet"),
("i82551", "Intel i82551 Ethernet"),
("i82557a", "Intel i82557A Ethernet"),
("i82557b", "Intel i82557B Ethernet"),
("i82557c", "Intel i82557C Ethernet"),
("i82558a", "Intel i82558A Ethernet"),
("i82558b", "Intel i82558B Ethernet"),
("i82559a", "Intel i82559A Ethernet"),
("i82559b", "Intel i82559B Ethernet"),
("i82559c", "Intel i82559C Ethernet"),
("i82559er", "Intel i82559ER Ethernet"),
("i82562", "Intel i82562 Ethernet"),
("i82801", "Intel i82801 Ethernet"),
("ne2k_pci", "NE2000 Ethernet"),
("pcnet", "AMD PCNet Ethernet"),
("rtl8139", "Realtek 8139 Ethernet"),
("virtio-net-pci", "Paravirtualized Network I/O"),
("vmxnet3", "VMWare Paravirtualized Ethernet v3")])
self.uiAdapterTypesComboBox.clear()
self.uiAdapterTypesComboBox.addItems(["ne2k_pci",
"i82551",
"i82557b",
"i82559er",
"rtl8139",
"e1000",
"pcnet",
"virtio"])
for device_name, device_description in qemu_network_devices.items():
self.uiAdapterTypesComboBox.addItem("{} ({})".format(device_description, device_name), device_name)
def _getDiskImage(self):
@staticmethod
def getDiskImage(parent):
destination_directory = os.path.join(MainWindow.instance().settings()["images_path"], "QEMU")
path, _ = QtGui.QFileDialog.getOpenFileNameAndFilter(self,
path, _ = QtGui.QFileDialog.getOpenFileNameAndFilter(parent,
"Select a QEMU disk image",
destination_directory)
if not path:
return
if not os.access(path, os.R_OK):
QtGui.QMessageBox.critical(self, "QEMU disk image", "Cannot read {}".format(path))
QtGui.QMessageBox.critical(parent, "QEMU disk image", "Cannot read {}".format(path))
return
try:
@@ -76,25 +96,28 @@ class QemuVMConfigurationPage(QtGui.QWidget, Ui_QemuVMConfigPageWidget):
except FileExistsError:
pass
except OSError as e:
QtGui.QMessageBox.critical(self, "QEMU disk images directory", "Could not create the QEMU disk images directory {}: {}".format(destination_directory,
str(e)))
QtGui.QMessageBox.critical(parent, "QEMU disk images directory", "Could not create the QEMU disk images directory {}: {}".format(destination_directory, e))
return
if os.path.dirname(path) != destination_directory:
if os.path.normpath(os.path.dirname(path)) != destination_directory:
# the QEMU disk image is not in the default images directory
new_destination_path = os.path.join(destination_directory, os.path.basename(path))
try:
# try to create a symbolic link to it
symlink_path = new_destination_path
os.symlink(path, symlink_path)
path = symlink_path
except (OSError, NotImplementedError):
# if unsuccessful, then copy the QEMU disk image itself
try:
shutil.copyfile(path, new_destination_path)
path = new_destination_path
except OSError:
pass
reply = QtGui.QMessageBox.question(parent,
"QEMU disk image",
"Would you like to copy {} to the default images directory".format(os.path.basename(path)),
QtGui.QMessageBox.Yes,
QtGui.QMessageBox.No)
if reply == QtGui.QMessageBox.Yes:
destination_path = os.path.join(destination_directory, os.path.basename(path))
thread = FileCopyThread(path, destination_path)
progress_dialog = ProgressDialog(thread, "QEMU disk image", "Copying {}".format(os.path.basename(path)), "Cancel", busy=True, parent=parent)
thread.deleteLater()
progress_dialog.show()
progress_dialog.exec_()
errors = progress_dialog.errors()
if errors:
QtGui.QMessageBox.critical(parent, "QEMU disk image", "{}".format("".join(errors)))
else:
path = destination_path
return path
@@ -103,7 +126,7 @@ class QemuVMConfigurationPage(QtGui.QWidget, Ui_QemuVMConfigPageWidget):
Slot to open a file browser and select a QEMU hda disk image.
"""
path = self._getDiskImage()
path = self.getDiskImage(self)
if path:
self.uiHdaDiskImageLineEdit.clear()
self.uiHdaDiskImageLineEdit.setText(path)
@@ -113,7 +136,7 @@ class QemuVMConfigurationPage(QtGui.QWidget, Ui_QemuVMConfigPageWidget):
Slot to open a file browser and select a QEMU hdb disk image.
"""
path = self._getDiskImage()
path = self.getDiskImage(self)
if path:
self.uiHdbDiskImageLineEdit.clear()
self.uiHdbDiskImageLineEdit.setText(path)
@@ -167,6 +190,16 @@ class QemuVMConfigurationPage(QtGui.QWidget, Ui_QemuVMConfigPageWidget):
QtGui.QMessageBox.critical(self, "Qemu", "Could not find {} in the Qemu binaries list".format(qemu_path))
self.uiQemuListComboBox.clear()
def _cpuThrottlingChangedSlot(self, state):
"""
Slot to enable or not CPU throttling.
"""
if state:
self.uiCPUThrottlingSpinBox.setEnabled(True)
else:
self.uiCPUThrottlingSpinBox.setEnabled(False)
def loadSettings(self, settings, node=None, group=False):
"""
Loads the QEMU VM settings.
@@ -182,19 +215,26 @@ class QemuVMConfigurationPage(QtGui.QWidget, Ui_QemuVMConfigPageWidget):
server = settings["server"]
if server == "local":
server = Servers.instance().localServer()
elif ":" in server:
host, port = server.rsplit(":")
server = Servers.instance().getRemoteServer(host, port)
self._qemu_binaries_progress_dialog = QtGui.QProgressDialog("Loading QEMU binaries", "Cancel", 0, 0, parent=self)
self._qemu_binaries_progress_dialog.setWindowModality(QtCore.Qt.WindowModal)
self._qemu_binaries_progress_dialog.setWindowTitle("QEMU binaries")
self._qemu_binaries_progress_dialog.show()
if server == "cloud":
for binary in QEMU_BINARIES_FOR_CLOUD:
self.uiQemuListComboBox.addItem("{path}".format(path=binary), binary)
else:
self._qemu_binaries_progress_dialog = QtGui.QProgressDialog("Loading QEMU binaries", "Cancel", 0, 0, parent=self)
self._qemu_binaries_progress_dialog.setWindowModality(QtCore.Qt.WindowModal)
self._qemu_binaries_progress_dialog.setWindowTitle("QEMU binaries")
self._qemu_binaries_progress_dialog.show()
callback = partial(self._getQemuBinariesFromServerCallback, qemu_path=settings["qemu_path"])
try:
Qemu.instance().getQemuBinariesFromServer(server, callback)
except ModuleError as e:
self._qemu_binaries_progress_dialog.reject()
QtGui.QMessageBox.critical(self, "Qemu binaries", "Error while getting the QEMU binaries: {}".format(e))
self.uiQemuListComboBox.clear()
callback = partial(self._getQemuBinariesFromServerCallback, qemu_path=settings["qemu_path"])
try:
Qemu.instance().getQemuBinariesFromServer(server, callback)
except ModuleError as e:
self._qemu_binaries_progress_dialog.reject()
QtGui.QMessageBox.critical(self, "Qemu binaries", "Error while getting the QEMU binaries: {}".format(e))
self.uiQemuListComboBox.clear()
if not group:
# set the device name
@@ -204,6 +244,11 @@ class QemuVMConfigurationPage(QtGui.QWidget, Ui_QemuVMConfigPageWidget):
else:
self.uiConsolePortLabel.hide()
self.uiConsolePortSpinBox.hide()
if "monitor" in settings:
self.uiMonitorPortSpinBox.setValue(settings["monitor"])
else:
self.uiMonitorPortLabel.hide()
self.uiMonitorPortSpinBox.hide()
self.uiHdaDiskImageLineEdit.setText(settings["hda_disk_image"])
self.uiHdbDiskImageLineEdit.setText(settings["hdb_disk_image"])
self.uiInitrdLineEdit.setText(settings["initrd"])
@@ -213,6 +258,8 @@ class QemuVMConfigurationPage(QtGui.QWidget, Ui_QemuVMConfigPageWidget):
self.uiNameLineEdit.hide()
self.uiConsolePortLabel.hide()
self.uiConsolePortSpinBox.hide()
self.uiMonitorPortLabel.hide()
self.uiMonitorPortSpinBox.hide()
self.uiHdaDiskImageLabel.hide()
self.uiHdaDiskImageLineEdit.hide()
self.uiHdaDiskImageToolButton.hide()
@@ -228,11 +275,21 @@ class QemuVMConfigurationPage(QtGui.QWidget, Ui_QemuVMConfigPageWidget):
self.uiKernelCommandLineEdit.setText(settings["kernel_command_line"])
self.uiAdaptersSpinBox.setValue(settings["adapters"])
index = self.uiAdapterTypesComboBox.findText(settings["adapter_type"])
index = self.uiAdapterTypesComboBox.findData(settings["adapter_type"])
if index != -1:
self.uiAdapterTypesComboBox.setCurrentIndex(index)
self.uiLegacyNetworkingCheckBox.setChecked(settings["legacy_networking"])
self.uiRamSpinBox.setValue(settings["ram"])
if settings["cpu_throttling"]:
self.uiActivateCPUThrottlingCheckBox.setChecked(True)
self.uiCPUThrottlingSpinBox.setValue(settings["cpu_throttling"])
else:
self.uiActivateCPUThrottlingCheckBox.setChecked(False)
index = self.uiProcessPriorityComboBox.findText(settings["process_priority"], QtCore.Qt.MatchFixedString)
if index != -1:
self.uiProcessPriorityComboBox.setCurrentIndex(index)
self.uiQemuOptionsLineEdit.setText(settings["options"])
def saveSettings(self, settings, node=None, group=False):
@@ -256,6 +313,8 @@ class QemuVMConfigurationPage(QtGui.QWidget, Ui_QemuVMConfigPageWidget):
if "console" in settings:
settings["console"] = self.uiConsolePortSpinBox.value()
if "monitor" in settings:
settings["monitor"] = self.uiMonitorPortSpinBox.value()
settings["hda_disk_image"] = self.uiHdaDiskImageLineEdit.text()
settings["hdb_disk_image"] = self.uiHdbDiskImageLineEdit.text()
settings["initrd"] = self.uiInitrdLineEdit.text()
@@ -265,6 +324,8 @@ class QemuVMConfigurationPage(QtGui.QWidget, Ui_QemuVMConfigPageWidget):
del settings["name"]
if "console" in settings:
del settings["console"]
if "monitor" in settings:
del settings["monitor"]
del settings["hda_disk_image"]
del settings["hdb_disk_image"]
del settings["initrd"]
@@ -274,7 +335,7 @@ class QemuVMConfigurationPage(QtGui.QWidget, Ui_QemuVMConfigPageWidget):
qemu_path = self.uiQemuListComboBox.itemData(self.uiQemuListComboBox.currentIndex())
settings["qemu_path"] = qemu_path
settings["adapter_type"] = self.uiAdapterTypesComboBox.currentText()
settings["adapter_type"] = self.uiAdapterTypesComboBox.itemData(self.uiAdapterTypesComboBox.currentIndex())
settings["kernel_command_line"] = self.uiKernelCommandLineEdit.text()
adapters = self.uiAdaptersSpinBox.value()
@@ -287,5 +348,11 @@ class QemuVMConfigurationPage(QtGui.QWidget, Ui_QemuVMConfigPageWidget):
raise ConfigurationError()
settings["adapters"] = adapters
settings["legacy_networking"] = self.uiLegacyNetworkingCheckBox.isChecked()
settings["ram"] = self.uiRamSpinBox.value()
if self.uiActivateCPUThrottlingCheckBox.isChecked():
settings["cpu_throttling"] = self.uiCPUThrottlingSpinBox.value()
else:
settings["cpu_throttling"] = 0
settings["process_priority"] = self.uiProcessPriorityComboBox.currentText().lower()
settings["options"] = self.uiQemuOptionsLineEdit.text()

View File

@@ -19,6 +19,7 @@
Configuration page for QEMU VM preferences.
"""
import ntpath
import os
import copy
@@ -27,13 +28,16 @@ from gns3.node import Node
from gns3.main_window import MainWindow
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
from gns3.dialogs.configuration_dialog import ConfigurationDialog
from gns3.cloud.utils import UploadFilesThread
from .. import Qemu
from ..settings import QEMU_VM_SETTINGS
from ..ui.qemu_vm_preferences_page_ui import Ui_QemuVMPreferencesPageWidget
from ..pages.qemu_vm_configuration_page import QemuVMConfigurationPage
from ..dialogs.qemu_vm_wizard import QemuVMWizard
class QemuVMPreferencesPage(QtGui.QWidget, Ui_QemuVMPreferencesPageWidget):
"""
QWidget preference page for QEMU VM preferences.
@@ -97,6 +101,14 @@ class QemuVMPreferencesPage(QtGui.QWidget, Ui_QemuVMPreferencesPageWidget):
if qemu_vm["kernel_command_line"]:
QtGui.QTreeWidgetItem(section_item, ["Kernel command line:", qemu_vm["kernel_command_line"]])
# performance section
section_item = self._createSectionItem("Optimizations")
if qemu_vm["cpu_throttling"]:
QtGui.QTreeWidgetItem(section_item, ["CPU throttling:", "{}%".format(qemu_vm["cpu_throttling"])])
else:
QtGui.QTreeWidgetItem(section_item, ["CPU throttling:", "disabled"])
QtGui.QTreeWidgetItem(section_item, ["Process priority:", qemu_vm["process_priority"]])
# fill out the Additional options section
if qemu_vm["options"]:
section_item = self._createSectionItem("Additional options")
@@ -129,34 +141,18 @@ class QemuVMPreferencesPage(QtGui.QWidget, Ui_QemuVMPreferencesPageWidget):
Creates a new VM.
"""
wizard = QemuVMWizard(parent=self)
wizard = QemuVMWizard(self._qemu_vms, parent=self)
wizard.show()
if wizard.exec_():
new_vm_settings = wizard.getSettings()
key = "{server}:{name}".format(server=new_vm_settings["server"], name=new_vm_settings["name"])
if key in self._qemu_vms:
QtGui.QMessageBox.critical(self, "New QEMU VM", "VM name {} already exists".format(new_vm_settings["name"]))
return
self._qemu_vms[key] = {"name": "",
"default_symbol": ":/symbols/qemu_guest.normal.svg",
"hover_symbol": ":/symbols/qemu_guest.selected.svg",
"category": Node.end_devices,
"qemu_path": "default",
"hda_disk_image": "",
"hdb_disk_image": "",
"ram": 256,
"adapters": 1,
"adapter_type": "e1000",
"options": "",
"initrd": "",
"kernel_image": "",
"kernel_command_line": "",
"server": "local"}
self._qemu_vms[key] = QEMU_VM_SETTINGS.copy()
self._qemu_vms[key].update(new_vm_settings)
item = QtGui.QTreeWidgetItem(self.uiQemuVMsTreeWidget)
item.setText(0, self._qemu_vms[key]["name"])
item.setIcon(0, QtGui.QIcon(self._qemu_vms[key]["default_symbol"]))
@@ -164,6 +160,62 @@ class QemuVMPreferencesPage(QtGui.QWidget, Ui_QemuVMPreferencesPageWidget):
self._items.append(item)
self.uiQemuVMsTreeWidget.setCurrentItem(item)
if self._qemu_vms[key]["server"] == 'cloud':
self._qemu_vms[key]["options"] = "-nographic"
self._uploadImages(new_vm_settings)
def _imageUploadComplete(self):
if self._upload_image_progress_dialog.wasCanceled():
return
self._upload_image_progress_dialog.accept()
def _uploadImages(self, qemu_vm):
"""
Upload hard drive images to Cloud Files.
"""
# Start uploading the image to cloud files
self._upload_image_progress_dialog = QtGui.QProgressDialog(
"Uploading image file(s)", "Cancel", 0, 0, parent=self)
self._upload_image_progress_dialog.setWindowModality(QtCore.Qt.WindowModal)
self._upload_image_progress_dialog.setWindowTitle("Qemu image upload")
self._upload_image_progress_dialog.show()
try:
uploads = []
src = qemu_vm.get("hda_disk_image", None)
if src:
_, filename = ntpath.split(src)
dst = "images/qemu/{}".format(filename)
uploads.append((src, dst))
src = qemu_vm.get("hdb_disk_image", None)
if src:
_, filename = ntpath.split(src)
dst = "images/qemu/{}".format(filename)
uploads.append((src, dst))
src = qemu_vm.get("initrd", None)
if src:
_, filename = ntpath.split(src)
dst = "images/qemu/{}".format(filename)
uploads.append((src, dst))
src = qemu_vm.get("kernel_image", None)
if src:
_, filename = ntpath.split(src)
dst = "images/qemu/{}".format(filename)
uploads.append((src, dst))
upload_thread = UploadFilesThread(self, MainWindow.instance().cloudSettings(), uploads)
upload_thread.completed.connect(self._imageUploadComplete)
upload_thread.start()
except Exception as e:
self._upload_image_progress_dialog.reject()
import logging
log = logging.getLogger(__name__)
log.error(e)
QtGui.QMessageBox.critical(self, "Qemu image upload", "Error uploading Qemu image: {}".format(e))
def _qemuVMEditSlot(self):
"""
Edits a QEMU VM.
@@ -177,11 +229,20 @@ class QemuVMPreferencesPage(QtGui.QWidget, Ui_QemuVMPreferencesPageWidget):
dialog.show()
if dialog.exec_():
if qemu_vm["name"] != item.text(0):
if "{}:{}".format(qemu_vm["server"], qemu_vm["name"]) in self._qemu_vms:
# FIXME: bug when changing name
QtGui.QMessageBox.critical(self, "New QEMU VM", "VM name {} already exists".format(qemu_vm["name"]))
new_key = "{server}:{name}".format(server=qemu_vm["server"], name=qemu_vm["name"])
if new_key in self._qemu_vms:
QtGui.QMessageBox.critical(self, "QEMU VM", "QEMU VM name {} already exists for server {}".format(qemu_vm["name"],
qemu_vm["server"]))
qemu_vm["name"] = item.text(0)
return
self._qemu_vms[new_key] = self._qemu_vms[key]
del self._qemu_vms[key]
item.setText(0, qemu_vm["name"])
item.setData(0, QtCore.Qt.UserRole, new_key)
if qemu_vm["server"] == 'cloud':
self._uploadImages(qemu_vm)
self._refreshInfo(qemu_vm)
def _qemuVMDeleteSlot(self):
@@ -220,16 +281,16 @@ class QemuVMPreferencesPage(QtGui.QWidget, Ui_QemuVMPreferencesPageWidget):
Change a symbol for a QEMU VM.
"""
dialog = SymbolSelectionDialog(self)
dialog.show()
if dialog.exec_():
normal_symbol, selected_symbol = dialog.getSymbols()
category = dialog.getCategory()
item = self.uiQemuVMsTreeWidget.currentItem()
if item:
item = self.uiQemuVMsTreeWidget.currentItem()
if item:
key = item.data(0, QtCore.Qt.UserRole)
qemu_vm = self._qemu_vms[key]
dialog = SymbolSelectionDialog(self, symbol=qemu_vm["default_symbol"], category=qemu_vm["category"])
dialog.show()
if dialog.exec_():
normal_symbol, selected_symbol = dialog.getSymbols()
category = dialog.getCategory()
item.setIcon(0, QtGui.QIcon(normal_symbol))
key = item.data(0, QtCore.Qt.UserRole)
qemu_vm = self._qemu_vms[key]
qemu_vm["default_symbol"] = normal_symbol
qemu_vm["hover_symbol"] = selected_symbol
qemu_vm["category"] = category

View File

@@ -22,6 +22,7 @@ QEMU VM implementation.
from gns3.node import Node
from gns3.ports.port import Port
from gns3.ports.ethernet_port import EthernetPort
from .settings import QEMU_VM_SETTINGS
import logging
log = logging.getLogger(__name__)
@@ -46,19 +47,23 @@ class QemuVM(Node):
self._loading = False
self._module = module
self._ports = []
self._settings = {"name": "",
"qemu_path": "",
"hda_disk_image": "",
"hdb_disk_image": "",
"options": "",
"ram": 256,
"ram": QEMU_VM_SETTINGS["ram"],
"console": None,
"adapters": 1,
"adapter_type": "e1000",
"monitor": None,
"adapters": QEMU_VM_SETTINGS["adapters"],
"adapter_type": QEMU_VM_SETTINGS["adapter_type"],
"legacy_networking": QEMU_VM_SETTINGS["legacy_networking"],
"cpu_throttling": QEMU_VM_SETTINGS["cpu_throttling"],
"process_priority": QEMU_VM_SETTINGS["process_priority"],
"initrd": "",
"kernel_image": "",
"kernel_command_line": "",
}
"kernel_command_line": ""}
self._addAdapters(1)
@@ -74,12 +79,14 @@ class QemuVM(Node):
for port_number in range(0, adapters):
port_name = EthernetPort.longNameType() + str(port_number)
short_name = EthernetPort.shortNameType() + str(port_number)
new_port = EthernetPort(port_name)
new_port.setShortName(short_name)
new_port.setPortNumber(port_number)
self._ports.append(new_port)
log.debug("port {} has been added".format(port_name))
def setup(self, qemu_path, name=None, console=None, qemu_id=None, initial_settings={}, base_name=None):
def setup(self, qemu_path, name=None, console=None, monitor=None, qemu_id=None, initial_settings={}, base_name=None):
"""
Setups this QEMU VM.
@@ -88,7 +95,7 @@ class QemuVM(Node):
# let's create a unique name if none has been chosen
if not name:
name = self.allocateName(base_name)
name = self.allocateName(base_name + "-")
if not name:
self.error_signal.emit(self.id(), "could not allocate a name for this QEMU VM")
@@ -100,6 +107,9 @@ class QemuVM(Node):
if console:
params["console"] = self._settings["console"] = console
if monitor:
params["monitor"] = self._settings["monitor"] = monitor
if qemu_id:
params["qemu_id"] = qemu_id
@@ -189,6 +199,9 @@ class QemuVM(Node):
if name in self._settings and self._settings[name] != value:
params[name] = value
if "cloud_path" in new_settings:
params["cloud_path"] = self._settings["cloud_path"] = new_settings.pop("cloud_path")
log.debug("{} is updating settings: {}".format(self.name(), params))
self._server.send_message("qemu.update", params, self._updateCallback)
@@ -296,36 +309,36 @@ class QemuVM(Node):
port.setStatus(Port.stopped)
self.stopped_signal.emit()
# def suspend(self):
# """
# Suspends this QEMU VM instance.
# """
#
# if self.status() == Node.suspended:
# log.debug("{} is already suspended".format(self.name()))
# return
#
# log.debug("{} is being suspended".format(self.name()))
# self._server.send_message("qemu.suspend", {"id": self._qemu_id}, self._suspendCallback)
#
# def _suspendCallback(self, result, error=False):
# """
# Callback for suspend.
#
# :param result: server response
# :param error: indicates an error (boolean)
# """
#
# if error:
# log.error("error while suspending {}: {}".format(self.name(), result["message"]))
# self.server_error_signal.emit(self.id(), result["code"], result["message"])
# else:
# log.info("{} has suspended".format(self.name()))
# self.setStatus(Node.suspended)
# for port in self._ports:
# # set ports as suspended
# port.setStatus(Port.suspended)
# self.suspended_signal.emit()
def suspend(self):
"""
Suspends this QEMU VM instance.
"""
if self.status() == Node.suspended:
log.debug("{} is already suspended".format(self.name()))
return
log.debug("{} is being suspended".format(self.name()))
self._server.send_message("qemu.suspend", {"id": self._qemu_id}, self._suspendCallback)
def _suspendCallback(self, result, error=False):
"""
Callback for suspend.
:param result: server response
:param error: indicates an error (boolean)
"""
if error:
log.error("error while suspending {}: {}".format(self.name(), result["message"]))
self.server_error_signal.emit(self.id(), result["code"], result["message"])
else:
log.info("{} has suspended".format(self.name()))
self.setStatus(Node.suspended)
for port in self._ports:
# set ports as suspended
port.setStatus(Port.suspended)
self.suspended_signal.emit()
def reload(self):
"""
@@ -450,11 +463,13 @@ class QemuVM(Node):
info = """QEMU VM {name} is {state}
Node ID is {id}, server's QEMU VM ID is {qemu_id}
console is on port {console}
monitor is on port {monitor}
""".format(name=self.name(),
id=self.id(),
qemu_id=self._qemu_id,
state=state,
console=self._settings["console"])
console=self._settings["console"],
monitor=self._settings["monitor"])
port_info = ""
for port in self._ports:
@@ -507,13 +522,14 @@ class QemuVM(Node):
settings = node_info["properties"]
name = settings.pop("name")
qemu_path = settings.pop("qemu_path")
console = settings.pop("console")
console = settings.pop("console", self._defaults["console"])
monitor = settings.pop("monitor", self._defaults["monitor"])
self.updated_signal.connect(self._updatePortSettings)
# block the created signal, it will be triggered when loading is completely done
self._loading = True
log.info("QEMU VM {} is loading".format(name))
self.setName(name)
self.setup(qemu_path, name, console, qemu_id, settings)
self.setup(qemu_path, name, console, monitor, qemu_id, settings)
def _updatePortSettings(self):
"""
@@ -574,6 +590,15 @@ class QemuVM(Node):
return self._settings["console"]
def monitor(self):
"""
Returns the monitor port for this QEMU VM instance.
:returns: port (integer)
"""
return self._settings["monitor"]
def configPage(self):
"""
Returns the configuration page widget to be used by the node configurator.

View File

@@ -19,9 +19,13 @@
Default QEMU settings.
"""
from gns3.node import Node
QEMU_SETTINGS = {
"console_start_port_range": 5001,
"console_end_port_range": 5500,
"monitor_start_port_range": 5501,
"monitor_end_port_range": 6000,
"udp_start_port_range": 40001,
"udp_end_port_range": 45500,
"use_local_server": True,
@@ -30,7 +34,83 @@ QEMU_SETTINGS = {
QEMU_SETTING_TYPES = {
"console_start_port_range": int,
"console_end_port_range": int,
"monitor_start_port_range": int,
"monitor_end_port_range": int,
"udp_start_port_range": int,
"udp_end_port_range": int,
"use_local_server": bool,
}
QEMU_VM_SETTINGS = {
"name": "",
"default_symbol": ":/symbols/qemu_guest.normal.svg",
"hover_symbol": ":/symbols/qemu_guest.selected.svg",
"category": Node.end_devices,
"qemu_path": "",
"hda_disk_image": "",
"hdb_disk_image": "",
"ram": 256,
"adapters": 1,
"adapter_type": "e1000",
"legacy_networking": False,
"cpu_throttling": 0,
"process_priority": "normal",
"options": "",
"initrd": "",
"kernel_image": "",
"kernel_command_line": "",
"server": "local"
}
QEMU_VM_SETTING_TYPES = {
"name": str,
"default_symbol": str,
"hover_symbol": str,
"category": int,
"qemu_path": str,
"hda_disk_image": str,
"hdb_disk_image": str,
"ram": int,
"adapters": int,
"adapter_type": str,
"legacy_networking": bool,
"cpu_throttling": int,
"process_priority": str,
"options": str,
"initrd": str,
"kernel_image": str,
"kernel_command_line": str,
"server": str
}
# Use a hardcoded list of binaries rather than a dynamic one so the user
# doesn't require a running cloud instance to upload qemu images.
QEMU_BINARIES_FOR_CLOUD = [
"qemu-system-arm",
"qemu-system-microblaze",
"qemu-system-mipsel",
"qemu-system-ppcemb",
"qemu-system-sparc64",
"qemu-system-cris",
"qemu-system-microblazeel",
"qemu-system-moxie",
"qemu-system-s390x",
"qemu-system-unicore32",
"qemu-system-i386",
"qemu-system-mips",
"qemu-system-or32",
"qemu-system-sh4",
"qemu-system-x86_64",
"qemu-system-lm32",
"qemu-system-mips64",
"qemu-system-ppc",
"qemu-system-sh4eb",
"qemu-system-xtensa",
"qemu-system-alpha",
"qemu-system-m68k",
"qemu-system-mips64el",
"qemu-system-ppc64",
"qemu-system-sparc",
"qemu-system-xtensaeb",
]
QEMU_BINARIES_FOR_CLOUD.sort()

View File

@@ -135,6 +135,67 @@
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QGroupBox" name="uiMonitorPortRangeGroupBox">
<property name="title">
<string>Monitor port range</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QSpinBox" name="uiMonitorStartPortSpinBox">
<property name="suffix">
<string notr="true"> TCP</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>65535</number>
</property>
<property name="value">
<number>1</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="uiMonitorPortRangeLabel">
<property name="text">
<string>to</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="uiMonitorEndPortSpinBox">
<property name="suffix">
<string notr="true"> TCP</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>65535</number>
</property>
<property name="value">
<number>1</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_1">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QGroupBox" name="uiUDPPortRangeGroupBox">
<property name="title">
<string>UDP tunneling port range</string>
@@ -195,7 +256,7 @@
</layout>
</widget>
</item>
<item row="3" column="0">
<item row="4" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>

View File

@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/qemu/ui/qemu_preferences_page.ui'
# Form implementation generated from reading ui file 'qemu_preferences_page.ui'
#
# Created: Sun Oct 19 11:35:54 2014
# Created: Tue Dec 23 15:05:44 2014
# by: PyQt4 UI code generator 4.10.4
#
# WARNING! All changes made in this file will be lost!
@@ -79,6 +79,30 @@ class Ui_QemuPreferencesPageWidget(object):
spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
self.horizontalLayout.addItem(spacerItem)
self.gridLayout_2.addWidget(self.uiConsolePortRangeGroupBox, 0, 0, 1, 2)
self.uiMonitorPortRangeGroupBox = QtGui.QGroupBox(self.uiAdvancedSettingsTabWidget)
self.uiMonitorPortRangeGroupBox.setObjectName(_fromUtf8("uiMonitorPortRangeGroupBox"))
self.horizontalLayout1 = QtGui.QHBoxLayout(self.uiMonitorPortRangeGroupBox)
self.horizontalLayout1.setObjectName(_fromUtf8("horizontalLayout1"))
self.uiMonitorStartPortSpinBox = QtGui.QSpinBox(self.uiMonitorPortRangeGroupBox)
self.uiMonitorStartPortSpinBox.setSuffix(_fromUtf8(" TCP"))
self.uiMonitorStartPortSpinBox.setMinimum(1)
self.uiMonitorStartPortSpinBox.setMaximum(65535)
self.uiMonitorStartPortSpinBox.setProperty("value", 1)
self.uiMonitorStartPortSpinBox.setObjectName(_fromUtf8("uiMonitorStartPortSpinBox"))
self.horizontalLayout1.addWidget(self.uiMonitorStartPortSpinBox)
self.uiMonitorPortRangeLabel = QtGui.QLabel(self.uiMonitorPortRangeGroupBox)
self.uiMonitorPortRangeLabel.setObjectName(_fromUtf8("uiMonitorPortRangeLabel"))
self.horizontalLayout1.addWidget(self.uiMonitorPortRangeLabel)
self.uiMonitorEndPortSpinBox = QtGui.QSpinBox(self.uiMonitorPortRangeGroupBox)
self.uiMonitorEndPortSpinBox.setSuffix(_fromUtf8(" TCP"))
self.uiMonitorEndPortSpinBox.setMinimum(1)
self.uiMonitorEndPortSpinBox.setMaximum(65535)
self.uiMonitorEndPortSpinBox.setProperty("value", 1)
self.uiMonitorEndPortSpinBox.setObjectName(_fromUtf8("uiMonitorEndPortSpinBox"))
self.horizontalLayout1.addWidget(self.uiMonitorEndPortSpinBox)
spacerItem1 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
self.horizontalLayout1.addItem(spacerItem1)
self.gridLayout_2.addWidget(self.uiMonitorPortRangeGroupBox, 1, 0, 1, 2)
self.uiUDPPortRangeGroupBox = QtGui.QGroupBox(self.uiAdvancedSettingsTabWidget)
self.uiUDPPortRangeGroupBox.setObjectName(_fromUtf8("uiUDPPortRangeGroupBox"))
self.horizontalLayout_4 = QtGui.QHBoxLayout(self.uiUDPPortRangeGroupBox)
@@ -100,17 +124,17 @@ class Ui_QemuPreferencesPageWidget(object):
self.uiUDPEndPortSpinBox.setProperty("value", 1)
self.uiUDPEndPortSpinBox.setObjectName(_fromUtf8("uiUDPEndPortSpinBox"))
self.horizontalLayout_4.addWidget(self.uiUDPEndPortSpinBox)
spacerItem1 = QtGui.QSpacerItem(147, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
self.horizontalLayout_4.addItem(spacerItem1)
self.gridLayout_2.addWidget(self.uiUDPPortRangeGroupBox, 1, 0, 1, 2)
spacerItem2 = QtGui.QSpacerItem(20, 304, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
self.gridLayout_2.addItem(spacerItem2, 3, 0, 1, 1)
spacerItem2 = QtGui.QSpacerItem(147, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
self.horizontalLayout_4.addItem(spacerItem2)
self.gridLayout_2.addWidget(self.uiUDPPortRangeGroupBox, 2, 0, 1, 2)
spacerItem3 = QtGui.QSpacerItem(20, 304, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
self.gridLayout_2.addItem(spacerItem3, 4, 0, 1, 1)
self.uiTabWidget.addTab(self.uiAdvancedSettingsTabWidget, _fromUtf8(""))
self.verticalLayout.addWidget(self.uiTabWidget)
self.horizontalLayout_2 = QtGui.QHBoxLayout()
self.horizontalLayout_2.setObjectName(_fromUtf8("horizontalLayout_2"))
spacerItem3 = QtGui.QSpacerItem(254, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
self.horizontalLayout_2.addItem(spacerItem3)
spacerItem4 = QtGui.QSpacerItem(254, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
self.horizontalLayout_2.addItem(spacerItem4)
self.uiRestoreDefaultsPushButton = QtGui.QPushButton(QemuPreferencesPageWidget)
self.uiRestoreDefaultsPushButton.setObjectName(_fromUtf8("uiRestoreDefaultsPushButton"))
self.horizontalLayout_2.addWidget(self.uiRestoreDefaultsPushButton)
@@ -129,6 +153,8 @@ class Ui_QemuPreferencesPageWidget(object):
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.uiServerSettingsTabWidget), _translate("QemuPreferencesPageWidget", "Server settings", None))
self.uiConsolePortRangeGroupBox.setTitle(_translate("QemuPreferencesPageWidget", "Console port range", None))
self.uiConsolePortRangeLabel.setText(_translate("QemuPreferencesPageWidget", "to", None))
self.uiMonitorPortRangeGroupBox.setTitle(_translate("QemuPreferencesPageWidget", "Monitor port range", None))
self.uiMonitorPortRangeLabel.setText(_translate("QemuPreferencesPageWidget", "to", None))
self.uiUDPPortRangeGroupBox.setTitle(_translate("QemuPreferencesPageWidget", "UDP tunneling port range", None))
self.uiUDPPortRangeLabel.setText(_translate("QemuPreferencesPageWidget", "to", None))
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.uiAdvancedSettingsTabWidget), _translate("QemuPreferencesPageWidget", "Advanced settings", None))

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>492</width>
<height>399</height>
<width>486</width>
<height>407</height>
</rect>
</property>
<property name="windowTitle">
@@ -34,7 +34,7 @@
<item row="0" column="2">
<widget class="QLineEdit" name="uiNameLineEdit"/>
</item>
<item row="4" column="1" colspan="2">
<item row="5" column="1" colspan="2">
<spacer name="spacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
@@ -71,6 +71,20 @@
</property>
</widget>
</item>
<item row="4" column="2">
<widget class="QSpinBox" name="uiMonitorPortSpinBox">
<property name="maximum">
<number>65535</number>
</property>
</widget>
</item>
<item row="4" column="0" colspan="2">
<widget class="QLabel" name="uiMonitorPortLabel">
<property name="text">
<string>Monitor port:</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="uiNameLabel">
<property name="text">
@@ -175,7 +189,7 @@
<attribute name="title">
<string>Network</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_8">
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="0">
<widget class="QLabel" name="uiAdaptersLabel">
<property name="text">
@@ -192,10 +206,10 @@
</sizepolicy>
</property>
<property name="minimum">
<number>1</number>
<number>0</number>
</property>
<property name="maximum">
<number>8</number>
<number>32</number>
</property>
</widget>
</item>
@@ -206,7 +220,7 @@
</property>
</widget>
</item>
<item row="1" column="1" colspan="2">
<item row="1" column="1">
<widget class="QComboBox" name="uiAdapterTypesComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
@@ -216,7 +230,14 @@
</property>
</widget>
</item>
<item row="2" column="1">
<item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="uiLegacyNetworkingCheckBox">
<property name="text">
<string>Use the legacy networking mode</string>
</property>
</widget>
</item>
<item row="3" column="1">
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
@@ -235,8 +256,8 @@
<attribute name="title">
<string>Advanced settings</string>
</attribute>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="uiLinuxBootGroupBox">
<property name="title">
<string>Linux boot specific settings</string>
@@ -295,7 +316,99 @@
</layout>
</widget>
</item>
<item row="1" column="0">
<item>
<widget class="QGroupBox" name="uiOptimizationGroupBox">
<property name="title">
<string>Optimizations</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="uiActivateCPUThrottlingCheckBox">
<property name="text">
<string>Activate CPU throttling</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="uiCPUThrottlingLabel">
<property name="text">
<string>Percentage of CPU allowed:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="uiCPUThrottlingSpinBox">
<property name="suffix">
<string> %</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>800</number>
</property>
<property name="value">
<number>100</number>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="uiProcessPriorityLabel">
<property name="text">
<string>Process priority:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="uiProcessPriorityComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="currentIndex">
<number>3</number>
</property>
<item>
<property name="text">
<string>Realtime</string>
</property>
</item>
<item>
<property name="text">
<string>Very high</string>
</property>
</item>
<item>
<property name="text">
<string>High</string>
</property>
</item>
<item>
<property name="text">
<string>Normal</string>
</property>
</item>
<item>
<property name="text">
<string>Low</string>
</property>
</item>
<item>
<property name="text">
<string>Very low</string>
</property>
</item>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Aditional settings</string>
@@ -316,7 +429,7 @@
<zorder>uiQemuOptionsLabel</zorder>
</widget>
</item>
<item row="2" column="0">
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/qemu/ui/qemu_vm_configuration_page.ui'
#
# Created: Fri Oct 10 10:43:48 2014
# Created: Sat Jan 3 14:50:40 2015
# by: PyQt4 UI code generator 4.10.4
#
# WARNING! All changes made in this file will be lost!
@@ -26,7 +26,7 @@ except AttributeError:
class Ui_QemuVMConfigPageWidget(object):
def setupUi(self, QemuVMConfigPageWidget):
QemuVMConfigPageWidget.setObjectName(_fromUtf8("QemuVMConfigPageWidget"))
QemuVMConfigPageWidget.resize(492, 399)
QemuVMConfigPageWidget.resize(486, 407)
self.verticalLayout = QtGui.QVBoxLayout(QemuVMConfigPageWidget)
self.verticalLayout.setObjectName(_fromUtf8("verticalLayout"))
self.uiQemutabWidget = QtGui.QTabWidget(QemuVMConfigPageWidget)
@@ -42,7 +42,7 @@ class Ui_QemuVMConfigPageWidget(object):
self.uiNameLineEdit.setObjectName(_fromUtf8("uiNameLineEdit"))
self.gridLayout_4.addWidget(self.uiNameLineEdit, 0, 2, 1, 1)
spacerItem = QtGui.QSpacerItem(263, 94, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
self.gridLayout_4.addItem(spacerItem, 4, 1, 1, 2)
self.gridLayout_4.addItem(spacerItem, 5, 1, 1, 2)
self.uiQemuListComboBox = QtGui.QComboBox(self.tab)
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
@@ -58,6 +58,13 @@ class Ui_QemuVMConfigPageWidget(object):
self.uiConsolePortLabel = QtGui.QLabel(self.tab)
self.uiConsolePortLabel.setObjectName(_fromUtf8("uiConsolePortLabel"))
self.gridLayout_4.addWidget(self.uiConsolePortLabel, 3, 0, 1, 2)
self.uiMonitorPortSpinBox = QtGui.QSpinBox(self.tab)
self.uiMonitorPortSpinBox.setMaximum(65535)
self.uiMonitorPortSpinBox.setObjectName(_fromUtf8("uiMonitorPortSpinBox"))
self.gridLayout_4.addWidget(self.uiMonitorPortSpinBox, 4, 2, 1, 1)
self.uiMonitorPortLabel = QtGui.QLabel(self.tab)
self.uiMonitorPortLabel.setObjectName(_fromUtf8("uiMonitorPortLabel"))
self.gridLayout_4.addWidget(self.uiMonitorPortLabel, 4, 0, 1, 2)
self.uiNameLabel = QtGui.QLabel(self.tab)
self.uiNameLabel.setObjectName(_fromUtf8("uiNameLabel"))
self.gridLayout_4.addWidget(self.uiNameLabel, 0, 0, 1, 1)
@@ -106,24 +113,24 @@ class Ui_QemuVMConfigPageWidget(object):
self.uiQemutabWidget.addTab(self.tab_3, _fromUtf8(""))
self.tab_7 = QtGui.QWidget()
self.tab_7.setObjectName(_fromUtf8("tab_7"))
self.gridLayout_8 = QtGui.QGridLayout(self.tab_7)
self.gridLayout_8.setObjectName(_fromUtf8("gridLayout_8"))
self.gridLayout_5 = QtGui.QGridLayout(self.tab_7)
self.gridLayout_5.setObjectName(_fromUtf8("gridLayout_5"))
self.uiAdaptersLabel = QtGui.QLabel(self.tab_7)
self.uiAdaptersLabel.setObjectName(_fromUtf8("uiAdaptersLabel"))
self.gridLayout_8.addWidget(self.uiAdaptersLabel, 0, 0, 1, 1)
self.gridLayout_5.addWidget(self.uiAdaptersLabel, 0, 0, 1, 1)
self.uiAdaptersSpinBox = QtGui.QSpinBox(self.tab_7)
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiAdaptersSpinBox.sizePolicy().hasHeightForWidth())
self.uiAdaptersSpinBox.setSizePolicy(sizePolicy)
self.uiAdaptersSpinBox.setMinimum(1)
self.uiAdaptersSpinBox.setMaximum(8)
self.uiAdaptersSpinBox.setMinimum(0)
self.uiAdaptersSpinBox.setMaximum(32)
self.uiAdaptersSpinBox.setObjectName(_fromUtf8("uiAdaptersSpinBox"))
self.gridLayout_8.addWidget(self.uiAdaptersSpinBox, 0, 1, 1, 1)
self.gridLayout_5.addWidget(self.uiAdaptersSpinBox, 0, 1, 1, 1)
self.uiAdapterTypesLabel = QtGui.QLabel(self.tab_7)
self.uiAdapterTypesLabel.setObjectName(_fromUtf8("uiAdapterTypesLabel"))
self.gridLayout_8.addWidget(self.uiAdapterTypesLabel, 1, 0, 1, 1)
self.gridLayout_5.addWidget(self.uiAdapterTypesLabel, 1, 0, 1, 1)
self.uiAdapterTypesComboBox = QtGui.QComboBox(self.tab_7)
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
@@ -131,14 +138,17 @@ class Ui_QemuVMConfigPageWidget(object):
sizePolicy.setHeightForWidth(self.uiAdapterTypesComboBox.sizePolicy().hasHeightForWidth())
self.uiAdapterTypesComboBox.setSizePolicy(sizePolicy)
self.uiAdapterTypesComboBox.setObjectName(_fromUtf8("uiAdapterTypesComboBox"))
self.gridLayout_8.addWidget(self.uiAdapterTypesComboBox, 1, 1, 1, 2)
self.gridLayout_5.addWidget(self.uiAdapterTypesComboBox, 1, 1, 1, 1)
self.uiLegacyNetworkingCheckBox = QtGui.QCheckBox(self.tab_7)
self.uiLegacyNetworkingCheckBox.setObjectName(_fromUtf8("uiLegacyNetworkingCheckBox"))
self.gridLayout_5.addWidget(self.uiLegacyNetworkingCheckBox, 2, 0, 1, 2)
spacerItem2 = QtGui.QSpacerItem(20, 261, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
self.gridLayout_8.addItem(spacerItem2, 2, 1, 1, 1)
self.gridLayout_5.addItem(spacerItem2, 3, 1, 1, 1)
self.uiQemutabWidget.addTab(self.tab_7, _fromUtf8(""))
self.tab_2 = QtGui.QWidget()
self.tab_2.setObjectName(_fromUtf8("tab_2"))
self.gridLayout = QtGui.QGridLayout(self.tab_2)
self.gridLayout.setObjectName(_fromUtf8("gridLayout"))
self.verticalLayout_2 = QtGui.QVBoxLayout(self.tab_2)
self.verticalLayout_2.setObjectName(_fromUtf8("verticalLayout_2"))
self.uiLinuxBootGroupBox = QtGui.QGroupBox(self.tab_2)
self.uiLinuxBootGroupBox.setObjectName(_fromUtf8("uiLinuxBootGroupBox"))
self.gridLayout_2 = QtGui.QGridLayout(self.uiLinuxBootGroupBox)
@@ -169,7 +179,42 @@ class Ui_QemuVMConfigPageWidget(object):
self.uiKernelCommandLineEdit = QtGui.QLineEdit(self.uiLinuxBootGroupBox)
self.uiKernelCommandLineEdit.setObjectName(_fromUtf8("uiKernelCommandLineEdit"))
self.gridLayout_2.addWidget(self.uiKernelCommandLineEdit, 2, 1, 1, 2)
self.gridLayout.addWidget(self.uiLinuxBootGroupBox, 0, 0, 1, 1)
self.verticalLayout_2.addWidget(self.uiLinuxBootGroupBox)
self.uiOptimizationGroupBox = QtGui.QGroupBox(self.tab_2)
self.uiOptimizationGroupBox.setObjectName(_fromUtf8("uiOptimizationGroupBox"))
self.gridLayout = QtGui.QGridLayout(self.uiOptimizationGroupBox)
self.gridLayout.setObjectName(_fromUtf8("gridLayout"))
self.uiActivateCPUThrottlingCheckBox = QtGui.QCheckBox(self.uiOptimizationGroupBox)
self.uiActivateCPUThrottlingCheckBox.setChecked(True)
self.uiActivateCPUThrottlingCheckBox.setObjectName(_fromUtf8("uiActivateCPUThrottlingCheckBox"))
self.gridLayout.addWidget(self.uiActivateCPUThrottlingCheckBox, 0, 0, 1, 2)
self.uiCPUThrottlingLabel = QtGui.QLabel(self.uiOptimizationGroupBox)
self.uiCPUThrottlingLabel.setObjectName(_fromUtf8("uiCPUThrottlingLabel"))
self.gridLayout.addWidget(self.uiCPUThrottlingLabel, 1, 0, 1, 1)
self.uiCPUThrottlingSpinBox = QtGui.QSpinBox(self.uiOptimizationGroupBox)
self.uiCPUThrottlingSpinBox.setMinimum(1)
self.uiCPUThrottlingSpinBox.setMaximum(800)
self.uiCPUThrottlingSpinBox.setProperty("value", 100)
self.uiCPUThrottlingSpinBox.setObjectName(_fromUtf8("uiCPUThrottlingSpinBox"))
self.gridLayout.addWidget(self.uiCPUThrottlingSpinBox, 1, 1, 1, 1)
self.uiProcessPriorityLabel = QtGui.QLabel(self.uiOptimizationGroupBox)
self.uiProcessPriorityLabel.setObjectName(_fromUtf8("uiProcessPriorityLabel"))
self.gridLayout.addWidget(self.uiProcessPriorityLabel, 2, 0, 1, 1)
self.uiProcessPriorityComboBox = QtGui.QComboBox(self.uiOptimizationGroupBox)
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiProcessPriorityComboBox.sizePolicy().hasHeightForWidth())
self.uiProcessPriorityComboBox.setSizePolicy(sizePolicy)
self.uiProcessPriorityComboBox.setObjectName(_fromUtf8("uiProcessPriorityComboBox"))
self.uiProcessPriorityComboBox.addItem(_fromUtf8(""))
self.uiProcessPriorityComboBox.addItem(_fromUtf8(""))
self.uiProcessPriorityComboBox.addItem(_fromUtf8(""))
self.uiProcessPriorityComboBox.addItem(_fromUtf8(""))
self.uiProcessPriorityComboBox.addItem(_fromUtf8(""))
self.uiProcessPriorityComboBox.addItem(_fromUtf8(""))
self.gridLayout.addWidget(self.uiProcessPriorityComboBox, 2, 1, 1, 1)
self.verticalLayout_2.addWidget(self.uiOptimizationGroupBox)
self.groupBox = QtGui.QGroupBox(self.tab_2)
self.groupBox.setObjectName(_fromUtf8("groupBox"))
self.gridLayout_3 = QtGui.QGridLayout(self.groupBox)
@@ -180,20 +225,22 @@ class Ui_QemuVMConfigPageWidget(object):
self.uiQemuOptionsLineEdit = QtGui.QLineEdit(self.groupBox)
self.uiQemuOptionsLineEdit.setObjectName(_fromUtf8("uiQemuOptionsLineEdit"))
self.gridLayout_3.addWidget(self.uiQemuOptionsLineEdit, 0, 2, 1, 1)
self.gridLayout.addWidget(self.groupBox, 1, 0, 1, 1)
self.verticalLayout_2.addWidget(self.groupBox)
spacerItem3 = QtGui.QSpacerItem(20, 90, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
self.gridLayout.addItem(spacerItem3, 2, 0, 1, 1)
self.verticalLayout_2.addItem(spacerItem3)
self.uiQemutabWidget.addTab(self.tab_2, _fromUtf8(""))
self.verticalLayout.addWidget(self.uiQemutabWidget)
self.retranslateUi(QemuVMConfigPageWidget)
self.uiQemutabWidget.setCurrentIndex(0)
self.uiProcessPriorityComboBox.setCurrentIndex(3)
QtCore.QMetaObject.connectSlotsByName(QemuVMConfigPageWidget)
def retranslateUi(self, QemuVMConfigPageWidget):
QemuVMConfigPageWidget.setWindowTitle(_translate("QemuVMConfigPageWidget", "QEMU VM configuration", None))
self.uiQemuListLabel.setText(_translate("QemuVMConfigPageWidget", "Qemu binary:", None))
self.uiConsolePortLabel.setText(_translate("QemuVMConfigPageWidget", "Console port:", None))
self.uiMonitorPortLabel.setText(_translate("QemuVMConfigPageWidget", "Monitor port:", None))
self.uiNameLabel.setText(_translate("QemuVMConfigPageWidget", "VM name:", None))
self.uiRamLabel.setText(_translate("QemuVMConfigPageWidget", "RAM:", None))
self.uiRamSpinBox.setSuffix(_translate("QemuVMConfigPageWidget", " MB", None))
@@ -205,6 +252,7 @@ class Ui_QemuVMConfigPageWidget(object):
self.uiQemutabWidget.setTabText(self.uiQemutabWidget.indexOf(self.tab_3), _translate("QemuVMConfigPageWidget", "HDD", None))
self.uiAdaptersLabel.setText(_translate("QemuVMConfigPageWidget", "Adapters:", None))
self.uiAdapterTypesLabel.setText(_translate("QemuVMConfigPageWidget", "Type:", None))
self.uiLegacyNetworkingCheckBox.setText(_translate("QemuVMConfigPageWidget", "Use the legacy networking mode", None))
self.uiQemutabWidget.setTabText(self.uiQemutabWidget.indexOf(self.tab_7), _translate("QemuVMConfigPageWidget", "Network", None))
self.uiLinuxBootGroupBox.setTitle(_translate("QemuVMConfigPageWidget", "Linux boot specific settings", None))
self.uiKernelCommandLineLabel.setText(_translate("QemuVMConfigPageWidget", "Kernel command line:", None))
@@ -212,6 +260,17 @@ class Ui_QemuVMConfigPageWidget(object):
self.uiKernelImageLabel.setText(_translate("QemuVMConfigPageWidget", "Kernel image:", None))
self.uiInitrdToolButton.setText(_translate("QemuVMConfigPageWidget", "&Browse...", None))
self.uiKernelImageToolButton.setText(_translate("QemuVMConfigPageWidget", "&Browse...", None))
self.uiOptimizationGroupBox.setTitle(_translate("QemuVMConfigPageWidget", "Optimizations", None))
self.uiActivateCPUThrottlingCheckBox.setText(_translate("QemuVMConfigPageWidget", "Activate CPU throttling", None))
self.uiCPUThrottlingLabel.setText(_translate("QemuVMConfigPageWidget", "Percentage of CPU allowed:", None))
self.uiCPUThrottlingSpinBox.setSuffix(_translate("QemuVMConfigPageWidget", " %", None))
self.uiProcessPriorityLabel.setText(_translate("QemuVMConfigPageWidget", "Process priority:", None))
self.uiProcessPriorityComboBox.setItemText(0, _translate("QemuVMConfigPageWidget", "Realtime", None))
self.uiProcessPriorityComboBox.setItemText(1, _translate("QemuVMConfigPageWidget", "Very high", None))
self.uiProcessPriorityComboBox.setItemText(2, _translate("QemuVMConfigPageWidget", "High", None))
self.uiProcessPriorityComboBox.setItemText(3, _translate("QemuVMConfigPageWidget", "Normal", None))
self.uiProcessPriorityComboBox.setItemText(4, _translate("QemuVMConfigPageWidget", "Low", None))
self.uiProcessPriorityComboBox.setItemText(5, _translate("QemuVMConfigPageWidget", "Very low", None))
self.groupBox.setTitle(_translate("QemuVMConfigPageWidget", "Aditional settings", None))
self.uiQemuOptionsLabel.setText(_translate("QemuVMConfigPageWidget", "Options:", None))
self.uiQemutabWidget.setTabText(self.uiQemutabWidget.indexOf(self.tab_2), _translate("QemuVMConfigPageWidget", "Advanced settings", None))

View File

@@ -30,7 +30,7 @@
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
<pointsize>11</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/qemu/ui/qemu_vm_preferences_page.ui'
#
# Created: Fri Oct 3 14:05:58 2014
# Created: Wed Nov 19 18:57:19 2014
# by: PyQt4 UI code generator 4.10.4
#
# WARNING! All changes made in this file will be lost!
@@ -37,7 +37,7 @@ class Ui_QemuVMPreferencesPageWidget(object):
self.uiQemuVMsTreeWidget.setSizePolicy(sizePolicy)
self.uiQemuVMsTreeWidget.setMaximumSize(QtCore.QSize(160, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
font.setPointSize(11)
font.setBold(True)
font.setWeight(75)
self.uiQemuVMsTreeWidget.setFont(font)

View File

@@ -26,6 +26,7 @@ from ..module import Module
from ..module_error import ModuleError
from .virtualbox_vm import VirtualBoxVM
from .settings import VBOX_SETTINGS, VBOX_SETTING_TYPES
from .settings import VBOX_VM_SETTINGS, VBOX_VM_SETTING_TYPES
import logging
log = logging.getLogger(__name__)
@@ -87,29 +88,14 @@ class VirtualBox(Module):
size = settings.beginReadArray("VM")
for index in range(0, size):
settings.setArrayIndex(index)
vmname = settings.value("vmname", "")
adapters = settings.value("adapters", 1, type=int)
default_symbol = settings.value("default_symbol", ":/symbols/vbox_guest.normal.svg")
hover_symbol = settings.value("hover_symbol", ":/symbols/vbox_guest.selected.svg")
category = settings.value("category", Node.end_devices, type=int)
adapter_start_index = settings.value("adapter_start_index", 0, type=int)
adapter_type = settings.value("adapter_type", "Automatic")
headless = settings.value("headless", False, type=bool)
enable_console = settings.value("enable_console", False, type=bool)
server = settings.value("server", "local")
vmname = settings.value("vmname")
server = settings.value("server")
key = "{server}:{vmname}".format(server=server, vmname=vmname)
self._virtualbox_vms[key] = {"vmname": vmname,
"default_symbol": default_symbol,
"hover_symbol": hover_symbol,
"category": category,
"adapters": adapters,
"adapter_start_index": adapter_start_index,
"adapter_type": adapter_type,
"headless": headless,
"enable_console": enable_console,
"server": server}
if key in self._virtualbox_vms or not vmname or not server:
continue
self._virtualbox_vms[key] = {}
for setting_name, default_value in VBOX_VM_SETTINGS.items():
self._virtualbox_vms[key][setting_name] = settings.value(setting_name, default_value, VBOX_VM_SETTING_TYPES[setting_name])
settings.endArray()
settings.endGroup()
@@ -254,6 +240,8 @@ class VirtualBox(Module):
if server.isLocal():
params.update({"working_dir": self._working_dir})
else:
if "vboxmanage_path" in params:
del params["vboxmanage_path"] # do not send VBoxManage path to remote servers
project_name = os.path.basename(self._working_dir)
if project_name.endswith("-files"):
project_name = project_name[:-6]
@@ -277,8 +265,8 @@ class VirtualBox(Module):
if server.isLocal():
params.update({"working_dir": self._working_dir})
else:
if "vboxwrapper_path" in params:
del params["vboxwrapper_path"] # do not send Vboxwrapper path to remote servers
if "vboxmanage_path" in params:
del params["vboxmanage_path"] # do not send VBoxManage path to remote servers
project_name = os.path.basename(self._working_dir)
if project_name.endswith("-files"):
project_name = project_name[:-6]
@@ -347,19 +335,22 @@ class VirtualBox(Module):
else:
vm = selected_vms[0]
for other_node in self._nodes:
if other_node.settings()["vmname"] == self._virtualbox_vms[vm]["vmname"] and \
(self._virtualbox_vms[vm]["server"] == "local" and other_node.server().isLocal() or self._virtualbox_vms[vm]["server"] == other_node.server().host):
raise ModuleError("Sorry a VirtualBox VM can only be used once in your topology (this will change in future versions)")
linked_base = self._virtualbox_vms[vm]["linked_base"]
if not linked_base:
for other_node in self._nodes:
if other_node.settings()["vmname"] == self._virtualbox_vms[vm]["vmname"] and \
(self._virtualbox_vms[vm]["server"] == "local" and other_node.server().isLocal() or self._virtualbox_vms[vm]["server"] == other_node.server().host):
raise ModuleError("Sorry a VirtualBox VM can only be used once in your topology (this will change in future versions)")
settings = {"adapters": self._virtualbox_vms[vm]["adapters"],
"adapter_start_index": self._virtualbox_vms[vm]["adapter_start_index"],
"adapter_type": self._virtualbox_vms[vm]["adapter_type"],
"headless": self._virtualbox_vms[vm]["headless"],
"enable_console": self._virtualbox_vms[vm]["enable_console"]}
"enable_remote_console": self._virtualbox_vms[vm]["enable_remote_console"]}
vmname = self._virtualbox_vms[vm]["vmname"]
node.setup(vmname, initial_settings=settings)
node.setup(vmname, linked_clone=linked_base, initial_settings=settings)
def reset(self):
"""
@@ -404,8 +395,11 @@ class VirtualBox(Module):
raise ModuleError("Could not connect to server {}:{}: {}".format(server.host,
server.port,
e))
server.send_message("virtualbox.vm_list", None, callback)
params = {}
if server.isLocal():
params["vboxmanage_path"] = self._settings["vboxmanage_path"]
params["vbox_user"] = self._settings["vbox_user"]
server.send_message("virtualbox.vm_list", params, callback)
def getVirtualBoxVMList(self):
"""

View File

@@ -19,6 +19,8 @@
Wizard for VirtualBox VMs.
"""
import sys
from gns3.qt import QtCore, QtGui
from gns3.servers import Servers
from gns3.utils.connect_to_server import ConnectToServer
@@ -42,6 +44,9 @@ class VirtualBoxVMWizard(QtGui.QWizard, Ui_VirtualBoxVMWizard):
self.setupUi(self)
self.setPixmap(QtGui.QWizard.LogoPixmap, QtGui.QPixmap(":/icons/virtualbox.png"))
self.setWizardStyle(QtGui.QWizard.ModernStyle)
if sys.platform.startswith("darwin"):
# we want to see the cancel button on OSX
self.setOptions(QtGui.QWizard.NoDefaultButton)
if VirtualBox.instance().settings()["use_local_server"]:
# skip the server page if we use the local server
@@ -67,7 +72,7 @@ class VirtualBoxVMWizard(QtGui.QWizard, Ui_VirtualBoxVMWizard):
VirtualBox.instance().getVirtualBoxVMsFromServer(self._server, self._getVirtualBoxVMsFromServerCallback)
except ModuleError as e:
self._vbox_vms_progress_dialog.reject()
QtGui.QMessageBox.critical(self, "Qemu binaries", "Error while getting the QEMU binaries: {}".format(e))
QtGui.QMessageBox.critical(self, "VirtualBox VMs", "Error while getting the VirtualBox VMs: {}".format(e))
def _getVirtualBoxVMsFromServerCallback(self, result, error=False):
"""
@@ -82,7 +87,7 @@ class VirtualBoxVMWizard(QtGui.QWizard, Ui_VirtualBoxVMWizard):
self._vbox_vms_progress_dialog.accept()
if error:
QtGui.QMessageBox.critical(self, "VirtualBox VMs", "Error: ".format(result["message"]))
QtGui.QMessageBox.critical(self, "VirtualBox VMs", "{}".format(result["message"]))
else:
self.uiVMListComboBox.clear()
existing_vms = []
@@ -108,10 +113,17 @@ class VirtualBoxVMWizard(QtGui.QWizard, Ui_VirtualBoxVMWizard):
if VirtualBox.instance().settings()["use_local_server"] or self.uiLocalRadioButton.isChecked():
server = Servers.instance().localServer()
else:
if not Servers.instance().remoteServers():
QtGui.QMessageBox.critical(self, "Remote server", "There is no remote server registered in VirtualBox preferences")
return False
server = self.uiRemoteServersComboBox.itemData(self.uiRemoteServersComboBox.currentIndex())
if not server.connected() and ConnectToServer(self, server) is False:
return False
self._server = server
if self.currentPage() == self.uiVirtualBoxWizardPage:
if not self.uiVMListComboBox.count():
QtGui.QMessageBox.critical(self, "VirtualBox VMs", "There is no VirtualBox VM available!")
return False
return True
def getSettings(self):
@@ -121,9 +133,6 @@ class VirtualBoxVMWizard(QtGui.QWizard, Ui_VirtualBoxVMWizard):
:return: settings dict
"""
if not self.uiVMListComboBox.count():
return {}
if VirtualBox.instance().settings()["use_local_server"] or self.uiLocalRadioButton.isChecked():
server = "local"
else:
@@ -134,6 +143,7 @@ class VirtualBoxVMWizard(QtGui.QWizard, Ui_VirtualBoxVMWizard):
settings = {
"vmname": vmname,
"server": server,
"linked_base": self.uiBaseVMCheckBox.isChecked()
}
return settings

View File

@@ -40,25 +40,25 @@ class VirtualBoxPreferencesPage(QtGui.QWidget, Ui_VirtualBoxPreferencesPageWidge
# connect signals
self.uiUseLocalServercheckBox.stateChanged.connect(self._useLocalServerSlot)
self.uiRestoreDefaultsPushButton.clicked.connect(self._restoreDefaultsSlot)
self.uiVboxWrapperPathToolButton.clicked.connect(self._vboxPathBrowserSlot)
self.uiVboxManagePathToolButton.clicked.connect(self._vboxPathBrowserSlot)
#FIXME: temporally hide test button
self.uiTestSettingsPushButton.hide()
def _vboxPathBrowserSlot(self):
"""
Slot to open a file browser and select VirtualBox wrapper.
Slot to open a file browser and select VBoxManage.
"""
path = QtGui.QFileDialog.getOpenFileName(self, "Select VirtualBox wrapper", ".")
path = QtGui.QFileDialog.getOpenFileName(self, "Select VBoxManage", ".")
if not path:
return
if not os.access(path, os.X_OK):
QtGui.QMessageBox.critical(self, "VirtualBox wrapper", "{} is not an executable".format(os.path.basename(path)))
QtGui.QMessageBox.critical(self, "VBoxManage", "{} is not an executable".format(os.path.basename(path)))
return
self.uiVboxWrapperPathLineEdit.setText(os.path.normpath(path))
self.uiVboxManagePathLineEdit.setText(os.path.normpath(path))
def _restoreDefaultsSlot(self):
"""
@@ -84,7 +84,8 @@ class VirtualBoxPreferencesPage(QtGui.QWidget, Ui_VirtualBoxPreferencesPageWidge
:param settings: VirtualBox settings
"""
self.uiVboxWrapperPathLineEdit.setText(settings["vboxwrapper_path"])
self.uiVboxManagePathLineEdit.setText(settings["vboxmanage_path"])
self.uiVboxManageUserLineEdit.setText(settings["vbox_user"])
self.uiUseLocalServercheckBox.setChecked(settings["use_local_server"])
self.uiConsoleStartPortSpinBox.setValue(settings["console_start_port_range"])
self.uiConsoleEndPortSpinBox.setValue(settings["console_end_port_range"])
@@ -125,7 +126,8 @@ class VirtualBoxPreferencesPage(QtGui.QWidget, Ui_VirtualBoxPreferencesPageWidge
"""
new_settings = {}
new_settings["vboxwrapper_path"] = self.uiVboxWrapperPathLineEdit.text()
new_settings["vboxmanage_path"] = self.uiVboxManagePathLineEdit.text()
new_settings["vbox_user"] = self.uiVboxManageUserLineEdit.text()
new_settings["use_local_server"] = self.uiUseLocalServercheckBox.isChecked()
new_settings["console_start_port_range"] = self.uiConsoleStartPortSpinBox.value()
new_settings["console_end_port_range"] = self.uiConsoleEndPortSpinBox.value()

View File

@@ -36,8 +36,7 @@ class virtualBoxVMConfigurationPage(QtGui.QWidget, Ui_virtualBoxVMConfigPageWidg
self.setupUi(self)
self.uiAdapterTypesComboBox.clear()
self.uiAdapterTypesComboBox.addItems(["Automatic",
"PCnet-PCI II (Am79C970A)",
self.uiAdapterTypesComboBox.addItems(["PCnet-PCI II (Am79C970A)",
"PCNet-FAST III (Am79C973)",
"Intel PRO/1000 MT Desktop (82540EM)",
"Intel PRO/1000 T Server (82543GC)",
@@ -72,6 +71,11 @@ class virtualBoxVMConfigurationPage(QtGui.QWidget, Ui_virtualBoxVMConfigPageWidg
self.uiConsolePortLabel.hide()
self.uiConsolePortSpinBox.hide()
if "linked_base" in settings:
self.uiBaseVMCheckBox.setChecked(settings["linked_base"])
else:
self.uiBaseVMCheckBox.hide()
else:
self.uiNameLabel.hide()
self.uiNameLineEdit.hide()
@@ -86,7 +90,7 @@ class virtualBoxVMConfigurationPage(QtGui.QWidget, Ui_virtualBoxVMConfigPageWidg
if index != -1:
self.uiAdapterTypesComboBox.setCurrentIndex(index)
self.uiHeadlessModeCheckBox.setChecked(settings["headless"])
self.uiEnableConsoleCheckBox.setChecked(settings["enable_console"])
self.uiEnableConsoleCheckBox.setChecked(settings["enable_remote_console"])
def saveSettings(self, settings, node=None, group=False):
"""
@@ -108,13 +112,18 @@ class virtualBoxVMConfigurationPage(QtGui.QWidget, Ui_virtualBoxVMConfigPageWidg
else:
settings["name"] = name
settings["console"] = self.uiConsolePortSpinBox.value()
settings["enable_console"] = self.uiEnableConsoleCheckBox.isChecked()
if "console" in settings:
settings["console"] = self.uiConsolePortSpinBox.value()
if "linked_base" in settings:
settings["linked_base"] = self.uiBaseVMCheckBox.isChecked()
settings["enable_remote_console"] = self.uiEnableConsoleCheckBox.isChecked()
else:
del settings["name"]
del settings["console"]
del settings["enable_console"]
del settings["enable_remote_console"]
settings["adapter_type"] = self.uiAdapterTypesComboBox.currentText()

View File

@@ -28,6 +28,7 @@ from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
from gns3.dialogs.configuration_dialog import ConfigurationDialog
from .. import VirtualBox
from ..settings import VBOX_VM_SETTINGS
from ..ui.virtualbox_vm_preferences_page_ui import Ui_VirtualBoxVMPreferencesPageWidget
from ..pages.virtualbox_vm_configuration_page import virtualBoxVMConfigurationPage
from ..dialogs.virtualbox_vm_wizard import VirtualBoxVMWizard
@@ -87,12 +88,14 @@ class VirtualBoxVMPreferencesPage(QtGui.QWidget, Ui_VirtualBoxVMPreferencesPageW
section_item = self._createSectionItem("General")
QtGui.QTreeWidgetItem(section_item, ["VM name:", vbox_vm["vmname"]])
QtGui.QTreeWidgetItem(section_item, ["Server:", vbox_vm["server"]])
QtGui.QTreeWidgetItem(section_item, ["Console enabled:", "{}".format(vbox_vm["enable_console"])])
QtGui.QTreeWidgetItem(section_item, ["Remote console enabled:", "{}".format(vbox_vm["enable_remote_console"])])
QtGui.QTreeWidgetItem(section_item, ["Headless mode enabled:", "{}".format(vbox_vm["headless"])])
QtGui.QTreeWidgetItem(section_item, ["Linked base VM:", "{}".format(vbox_vm["linked_base"])])
# fill out the Network section
section_item = self._createSectionItem("Network")
QtGui.QTreeWidgetItem(section_item, ["Adapters:", str(vbox_vm["adapters"])])
QtGui.QTreeWidgetItem(section_item, ["Start at:", str(vbox_vm["adapter_start_index"])])
QtGui.QTreeWidgetItem(section_item, ["Type:", vbox_vm["adapter_type"]])
self.uiVirtualBoxVMInfoTreeWidget.expandAll()
@@ -109,22 +112,10 @@ class VirtualBoxVMPreferencesPage(QtGui.QWidget, Ui_VirtualBoxVMPreferencesPageW
if wizard.exec_():
new_vm_settings = wizard.getSettings()
if not new_vm_settings:
return
key = "{server}:{vmname}".format(server=new_vm_settings["server"], vmname=new_vm_settings["vmname"])
self._virtualbox_vms[key] = {"vmname": "",
"default_symbol": ":/symbols/vbox_guest.normal.svg",
"hover_symbol": ":/symbols/vbox_guest.selected.svg",
"category": Node.end_devices,
"adapters": 1,
"adapter_start_index": 0,
"adapter_type": "Automatic",
"headless": False,
"enable_console": False,
"server": "local"}
self._virtualbox_vms[key] = VBOX_VM_SETTINGS.copy()
self._virtualbox_vms[key].update(new_vm_settings)
item = QtGui.QTreeWidgetItem(self.uiVirtualBoxVMsTreeWidget)
item.setText(0, self._virtualbox_vms[key]["vmname"])
item.setIcon(0, QtGui.QIcon(self._virtualbox_vms[key]["default_symbol"]))
@@ -145,11 +136,16 @@ class VirtualBoxVMPreferencesPage(QtGui.QWidget, Ui_VirtualBoxVMPreferencesPageW
dialog.show()
if dialog.exec_():
if vbox_vm["vmname"] != item.text(0):
if "{}:{}".format(vbox_vm["server"], vbox_vm["vmname"]) in self._virtualbox_vms:
# FIXME: bug when changing name
QtGui.QMessageBox.critical(self, "New VirtualBox VM", "VM name {} already exists".format(vbox_vm["vmname"]))
new_key = "{server}:{vmname}".format(server=vbox_vm["server"], name=vbox_vm["vmname"])
if new_key in self._virtualbox_vms:
QtGui.QMessageBox.critical(self, "VirtualBox VM", "VirtualBox VM name {} already exists for server {}".format(vbox_vm["vmname"],
vbox_vm["server"]))
vbox_vm["vmname"] = item.text(0)
return
self._virtualbox_vms[new_key] = self._virtualbox_vms[key]
del self._virtualbox_vms[key]
item.setText(0, vbox_vm["vmname"])
item.setData(0, QtCore.Qt.UserRole, new_key)
self._refreshInfo(vbox_vm)
def _vboxVMDeleteSlot(self):
@@ -191,16 +187,16 @@ class VirtualBoxVMPreferencesPage(QtGui.QWidget, Ui_VirtualBoxVMPreferencesPageW
Change a symbol for a VirtualBox VM.
"""
dialog = SymbolSelectionDialog(self)
dialog.show()
if dialog.exec_():
normal_symbol, selected_symbol = dialog.getSymbols()
category = dialog.getCategory()
item = self.uiVirtualBoxVMsTreeWidget.currentItem()
if item:
item = self.uiVirtualBoxVMsTreeWidget.currentItem()
if item:
key = item.data(0, QtCore.Qt.UserRole)
vbox_vm = self._virtualbox_vms[key]
dialog = SymbolSelectionDialog(self, symbol=vbox_vm["default_symbol"], category=vbox_vm["category"])
dialog.show()
if dialog.exec_():
normal_symbol, selected_symbol = dialog.getSymbols()
category = dialog.getCategory()
item.setIcon(0, QtGui.QIcon(normal_symbol))
key = item.data(0, QtCore.Qt.UserRole)
vbox_vm = self._virtualbox_vms[key]
vbox_vm["default_symbol"] = normal_symbol
vbox_vm["hover_symbol"] = selected_symbol
vbox_vm["category"] = category

View File

@@ -19,26 +19,36 @@
Default VirtualBox settings.
"""
from gns3.node import Node
import sys
import os
# default path to VirtualBox wrapper executable
if sys.platform.startswith("darwin") and hasattr(sys, "frozen"):
DEFAULT_VBOXWRAPPER_PATH = os.path.join(os.getcwd(), "vboxwrapper")
# default path to VirtualBox vboxmanage executable
if sys.platform.startswith("win"):
if "VBOX_INSTALL_PATH" in os.environ:
DEFAULT_VBOXMANAGE_PATH = os.path.join(os.environ["VBOX_INSTALL_PATH"], "VBoxManage.exe")
elif "VBOX_MSI_INSTALL_PATH" in os.environ:
DEFAULT_VBOXMANAGE_PATH = os.path.join(os.environ["VBOX_MSI_INSTALL_PATH"], "VBoxManage.exe")
else:
DEFAULT_VBOXMANAGE_PATH = "VBoxManage.exe"
elif sys.platform.startswith("darwin"):
DEFAULT_VBOXMANAGE_PATH = "/Applications/VirtualBox.app/Contents/MacOS/VBoxManage"
else:
paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep)
# look for VirtualBox wrapper in the current working directory and $PATH
DEFAULT_VBOXWRAPPER_PATH = "vboxwrapper"
# look for vboxmanage in the current working directory and $PATH
DEFAULT_VBOXMANAGE_PATH = "vboxmanage"
for path in paths:
try:
if "vboxwrapper" in os.listdir(path) and os.access(os.path.join(path, "vboxwrapper"), os.X_OK):
DEFAULT_VBOXWRAPPER_PATH = os.path.join(path, "vboxwrapper")
if "vboxmanage" in os.listdir(path) and os.access(os.path.join(path, "vboxmanage"), os.X_OK):
DEFAULT_VBOXMANAGE_PATH = os.path.join(path, "vboxmanage")
break
except OSError:
continue
VBOX_SETTINGS = {
"vboxwrapper_path": DEFAULT_VBOXWRAPPER_PATH,
"vboxmanage_path": DEFAULT_VBOXMANAGE_PATH,
"vbox_user": "",
"console_start_port_range": 3501,
"console_end_port_range": 4000,
"udp_start_port_range": 35001,
@@ -47,10 +57,39 @@ VBOX_SETTINGS = {
}
VBOX_SETTING_TYPES = {
"vboxwrapper_path": str,
"vboxmanage_path": str,
"vbox_user": str,
"console_start_port_range": int,
"console_end_port_range": int,
"udp_start_port_range": int,
"udp_end_port_range": int,
"use_local_server": bool,
}
VBOX_VM_SETTINGS = {
"vmname": "",
"default_symbol": ":/symbols/vbox_guest.normal.svg",
"hover_symbol": ":/symbols/vbox_guest.selected.svg",
"category": Node.end_devices,
"adapters": 1,
"adapter_start_index": 0,
"adapter_type": "Intel PRO/1000 MT Desktop (82540EM)",
"headless": False,
"enable_remote_console": False,
"linked_base": False,
"server": "local"
}
VBOX_VM_SETTING_TYPES = {
"vmname": str,
"default_symbol": str,
"hover_symbol": str,
"category": int,
"adapters": int,
"adapter_start_index": int,
"adapter_type": str,
"headless": bool,
"enable_remote_console": bool,
"linked_base": bool,
"server": str
}

View File

@@ -28,19 +28,19 @@
</attribute>
<layout class="QGridLayout" name="gridLayout">
<item row="2" column="0">
<widget class="QLabel" name="uiVboxWrapperPathLabel">
<widget class="QLabel" name="uiVboxManagePathLabel">
<property name="text">
<string>Path to the VirtualBox wrapper (Linux/UNIX only):</string>
<string>Path to VBoxManage:</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QLineEdit" name="uiVboxWrapperPathLineEdit"/>
<widget class="QLineEdit" name="uiVboxManagePathLineEdit"/>
</item>
<item>
<widget class="QToolButton" name="uiVboxWrapperPathToolButton">
<widget class="QToolButton" name="uiVboxManagePathToolButton">
<property name="text">
<string>&amp;Browse...</string>
</property>
@@ -51,7 +51,17 @@
</item>
</layout>
</item>
<item row="5" column="0" colspan="2">
<item row="5" column="0">
<widget class="QLabel" name="uiVboxManageUserLabel">
<property name="text">
<string>Run VirtualBox as another user (GNS3 running as root):</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLineEdit" name="uiVboxManageUserLineEdit"/>
</item>
<item row="7" column="0" colspan="2">
<spacer name="spacer">
<property name="orientation">
<enum>Qt::Vertical</enum>

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/virtualbox/ui/virtualbox_preferences_page.ui'
#
# Created: Sun Oct 19 11:35:54 2014
# Created: Mon Jan 5 16:20:49 2015
# by: PyQt4 UI code generator 4.10.4
#
# WARNING! All changes made in this file will be lost!
@@ -36,21 +36,27 @@ class Ui_VirtualBoxPreferencesPageWidget(object):
self.uiGeneralSettingsTabWidget.setObjectName(_fromUtf8("uiGeneralSettingsTabWidget"))
self.gridLayout = QtGui.QGridLayout(self.uiGeneralSettingsTabWidget)
self.gridLayout.setObjectName(_fromUtf8("gridLayout"))
self.uiVboxWrapperPathLabel = QtGui.QLabel(self.uiGeneralSettingsTabWidget)
self.uiVboxWrapperPathLabel.setObjectName(_fromUtf8("uiVboxWrapperPathLabel"))
self.gridLayout.addWidget(self.uiVboxWrapperPathLabel, 2, 0, 1, 1)
self.uiVboxManagePathLabel = QtGui.QLabel(self.uiGeneralSettingsTabWidget)
self.uiVboxManagePathLabel.setObjectName(_fromUtf8("uiVboxManagePathLabel"))
self.gridLayout.addWidget(self.uiVboxManagePathLabel, 2, 0, 1, 1)
self.horizontalLayout_5 = QtGui.QHBoxLayout()
self.horizontalLayout_5.setObjectName(_fromUtf8("horizontalLayout_5"))
self.uiVboxWrapperPathLineEdit = QtGui.QLineEdit(self.uiGeneralSettingsTabWidget)
self.uiVboxWrapperPathLineEdit.setObjectName(_fromUtf8("uiVboxWrapperPathLineEdit"))
self.horizontalLayout_5.addWidget(self.uiVboxWrapperPathLineEdit)
self.uiVboxWrapperPathToolButton = QtGui.QToolButton(self.uiGeneralSettingsTabWidget)
self.uiVboxWrapperPathToolButton.setToolButtonStyle(QtCore.Qt.ToolButtonTextOnly)
self.uiVboxWrapperPathToolButton.setObjectName(_fromUtf8("uiVboxWrapperPathToolButton"))
self.horizontalLayout_5.addWidget(self.uiVboxWrapperPathToolButton)
self.uiVboxManagePathLineEdit = QtGui.QLineEdit(self.uiGeneralSettingsTabWidget)
self.uiVboxManagePathLineEdit.setObjectName(_fromUtf8("uiVboxManagePathLineEdit"))
self.horizontalLayout_5.addWidget(self.uiVboxManagePathLineEdit)
self.uiVboxManagePathToolButton = QtGui.QToolButton(self.uiGeneralSettingsTabWidget)
self.uiVboxManagePathToolButton.setToolButtonStyle(QtCore.Qt.ToolButtonTextOnly)
self.uiVboxManagePathToolButton.setObjectName(_fromUtf8("uiVboxManagePathToolButton"))
self.horizontalLayout_5.addWidget(self.uiVboxManagePathToolButton)
self.gridLayout.addLayout(self.horizontalLayout_5, 3, 0, 1, 2)
self.uiVboxManageUserLabel = QtGui.QLabel(self.uiGeneralSettingsTabWidget)
self.uiVboxManageUserLabel.setObjectName(_fromUtf8("uiVboxManageUserLabel"))
self.gridLayout.addWidget(self.uiVboxManageUserLabel, 5, 0, 1, 1)
self.uiVboxManageUserLineEdit = QtGui.QLineEdit(self.uiGeneralSettingsTabWidget)
self.uiVboxManageUserLineEdit.setObjectName(_fromUtf8("uiVboxManageUserLineEdit"))
self.gridLayout.addWidget(self.uiVboxManageUserLineEdit, 6, 0, 1, 1)
spacerItem = QtGui.QSpacerItem(390, 193, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
self.gridLayout.addItem(spacerItem, 5, 0, 1, 2)
self.gridLayout.addItem(spacerItem, 7, 0, 1, 2)
self.uiTabWidget.addTab(self.uiGeneralSettingsTabWidget, _fromUtf8(""))
self.uiServerSettingsTabWidget = QtGui.QWidget()
self.uiServerSettingsTabWidget.setObjectName(_fromUtf8("uiServerSettingsTabWidget"))
@@ -141,8 +147,9 @@ class Ui_VirtualBoxPreferencesPageWidget(object):
def retranslateUi(self, VirtualBoxPreferencesPageWidget):
VirtualBoxPreferencesPageWidget.setWindowTitle(_translate("VirtualBoxPreferencesPageWidget", "VirtualBox", None))
self.uiVboxWrapperPathLabel.setText(_translate("VirtualBoxPreferencesPageWidget", "Path to the VirtualBox wrapper (Linux/UNIX only):", None))
self.uiVboxWrapperPathToolButton.setText(_translate("VirtualBoxPreferencesPageWidget", "&Browse...", None))
self.uiVboxManagePathLabel.setText(_translate("VirtualBoxPreferencesPageWidget", "Path to VBoxManage:", None))
self.uiVboxManagePathToolButton.setText(_translate("VirtualBoxPreferencesPageWidget", "&Browse...", None))
self.uiVboxManageUserLabel.setText(_translate("VirtualBoxPreferencesPageWidget", "Run VirtualBox as another user (GNS3 running as root):", None))
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.uiGeneralSettingsTabWidget), _translate("VirtualBoxPreferencesPageWidget", "General settings", None))
self.uiUseLocalServercheckBox.setText(_translate("VirtualBoxPreferencesPageWidget", "Always use the local server", None))
self.uiRemoteServersGroupBox.setTitle(_translate("VirtualBoxPreferencesPageWidget", "Remote servers", None))

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>514</width>
<height>365</height>
<width>509</width>
<height>346</height>
</rect>
</property>
<property name="windowTitle">
@@ -82,7 +82,17 @@
</property>
</widget>
</item>
<item row="5" column="1">
<item row="5" column="0" colspan="2">
<widget class="QCheckBox" name="uiBaseVMCheckBox">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Use as a linked base VM (experimental)</string>
</property>
</widget>
</item>
<item row="6" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/virtualbox/ui/virtualbox_vm_configuration_page.ui'
#
# Created: Wed Oct 22 22:07:00 2014
# Created: Tue Dec 2 14:17:34 2014
# by: PyQt4 UI code generator 4.10.4
#
# WARNING! All changes made in this file will be lost!
@@ -26,7 +26,7 @@ except AttributeError:
class Ui_virtualBoxVMConfigPageWidget(object):
def setupUi(self, virtualBoxVMConfigPageWidget):
virtualBoxVMConfigPageWidget.setObjectName(_fromUtf8("virtualBoxVMConfigPageWidget"))
virtualBoxVMConfigPageWidget.resize(514, 365)
virtualBoxVMConfigPageWidget.resize(509, 346)
self.verticalLayout = QtGui.QVBoxLayout(virtualBoxVMConfigPageWidget)
self.verticalLayout.setObjectName(_fromUtf8("verticalLayout"))
self.uiTabWidget = QtGui.QTabWidget(virtualBoxVMConfigPageWidget)
@@ -66,8 +66,12 @@ class Ui_virtualBoxVMConfigPageWidget(object):
self.uiHeadlessModeCheckBox.setChecked(False)
self.uiHeadlessModeCheckBox.setObjectName(_fromUtf8("uiHeadlessModeCheckBox"))
self.gridLayout.addWidget(self.uiHeadlessModeCheckBox, 4, 0, 1, 2)
self.uiBaseVMCheckBox = QtGui.QCheckBox(self.tab)
self.uiBaseVMCheckBox.setEnabled(True)
self.uiBaseVMCheckBox.setObjectName(_fromUtf8("uiBaseVMCheckBox"))
self.gridLayout.addWidget(self.uiBaseVMCheckBox, 5, 0, 1, 2)
spacerItem = QtGui.QSpacerItem(20, 138, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
self.gridLayout.addItem(spacerItem, 5, 1, 1, 1)
self.gridLayout.addItem(spacerItem, 6, 1, 1, 1)
self.uiTabWidget.addTab(self.tab, _fromUtf8(""))
self.tab_2 = QtGui.QWidget()
self.tab_2.setObjectName(_fromUtf8("tab_2"))
@@ -117,6 +121,7 @@ class Ui_virtualBoxVMConfigPageWidget(object):
self.uiConsolePortLabel.setText(_translate("virtualBoxVMConfigPageWidget", "Console port:", None))
self.uiEnableConsoleCheckBox.setText(_translate("virtualBoxVMConfigPageWidget", "Enable remote console", None))
self.uiHeadlessModeCheckBox.setText(_translate("virtualBoxVMConfigPageWidget", "Start VM in headless mode", None))
self.uiBaseVMCheckBox.setText(_translate("virtualBoxVMConfigPageWidget", "Use as a linked base VM (experimental)", None))
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.tab), _translate("virtualBoxVMConfigPageWidget", "General settings", None))
self.uiAdaptersLabel.setText(_translate("virtualBoxVMConfigPageWidget", "Adapters:", None))
self.uiAdapterStartIndexLabel.setText(_translate("virtualBoxVMConfigPageWidget", "Start at:", None))

View File

@@ -84,7 +84,7 @@
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
<pointsize>11</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/virtualbox/ui/virtualbox_vm_preferences_page.ui'
#
# Created: Tue Oct 7 12:02:22 2014
# Created: Wed Nov 19 18:57:20 2014
# by: PyQt4 UI code generator 4.10.4
#
# WARNING! All changes made in this file will be lost!
@@ -57,7 +57,7 @@ class Ui_VirtualBoxVMPreferencesPageWidget(object):
self.uiVirtualBoxVMsTreeWidget.setSizePolicy(sizePolicy)
self.uiVirtualBoxVMsTreeWidget.setMaximumSize(QtCore.QSize(160, 16777215))
font = QtGui.QFont()
font.setPointSize(12)
font.setPointSize(11)
font.setBold(True)
font.setWeight(75)
self.uiVirtualBoxVMsTreeWidget.setFont(font)

View File

@@ -126,6 +126,16 @@
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QCheckBox" name="uiBaseVMCheckBox">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Use as a linked base VM (experimental)</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/virtualbox/ui/virtualbox_vm_wizard.ui'
#
# Created: Wed Oct 22 16:46:37 2014
# Created: Tue Dec 2 14:17:34 2014
# by: PyQt4 UI code generator 4.10.4
#
# WARNING! All changes made in this file will be lost!
@@ -82,6 +82,10 @@ class Ui_VirtualBoxVMWizard(object):
self.uiVMListComboBox.setSizePolicy(sizePolicy)
self.uiVMListComboBox.setObjectName(_fromUtf8("uiVMListComboBox"))
self.gridLayout.addWidget(self.uiVMListComboBox, 0, 1, 1, 1)
self.uiBaseVMCheckBox = QtGui.QCheckBox(self.uiVirtualBoxWizardPage)
self.uiBaseVMCheckBox.setEnabled(True)
self.uiBaseVMCheckBox.setObjectName(_fromUtf8("uiBaseVMCheckBox"))
self.gridLayout.addWidget(self.uiBaseVMCheckBox, 1, 0, 1, 2)
VirtualBoxVMWizard.addPage(self.uiVirtualBoxWizardPage)
self.retranslateUi(VirtualBoxVMWizard)
@@ -100,4 +104,5 @@ class Ui_VirtualBoxVMWizard(object):
self.uiVirtualBoxWizardPage.setTitle(_translate("VirtualBoxVMWizard", "VirtualBox Virtual Machine", None))
self.uiVirtualBoxWizardPage.setSubTitle(_translate("VirtualBoxVMWizard", "Please choose a VirtualBox virtual machine from the list.", None))
self.uiVMListLabel.setText(_translate("VirtualBoxVMWizard", "VM list:", None))
self.uiBaseVMCheckBox.setText(_translate("VirtualBoxVMWizard", "Use as a linked base VM (experimental)", None))

View File

@@ -22,6 +22,7 @@ VirtualBox VM implementation.
from gns3.node import Node
from gns3.ports.port import Port
from gns3.ports.ethernet_port import EthernetPort
from .settings import VBOX_VM_SETTINGS
import logging
log = logging.getLogger(__name__)
@@ -40,6 +41,7 @@ class VirtualBoxVM(Node):
log.info("VirtualBox VM instance is being created")
self._vbox_id = None
self._linked_clone = False
self._defaults = {}
self._inital_settings = None
self._export_directory = None
@@ -49,11 +51,11 @@ class VirtualBoxVM(Node):
self._settings = {"name": "",
"vmname": "",
"console": None,
"adapters": 2,
"adapter_start_index": 0,
"adapter_type": "Automatic",
"headless": False,
"enable_console": False}
"adapters": VBOX_VM_SETTINGS["adapters"],
"adapter_start_index": VBOX_VM_SETTINGS["adapter_start_index"],
"adapter_type": VBOX_VM_SETTINGS["adapter_type"],
"headless": VBOX_VM_SETTINGS["headless"],
"enable_remote_console": VBOX_VM_SETTINGS["enable_remote_console"]}
self._addAdapters(2)
@@ -71,31 +73,39 @@ class VirtualBoxVM(Node):
if port_number < self._settings["adapter_start_index"]:
continue
port_name = EthernetPort.longNameType() + str(port_number)
short_name = EthernetPort.shortNameType() + str(port_number)
new_port = EthernetPort(port_name)
new_port.setShortName(short_name)
new_port.setPortNumber(port_number)
new_port.setPacketCaptureSupported(True)
self._ports.append(new_port)
log.debug("port {} has been added".format(port_name))
def setup(self, vmname, name=None, console=None, vbox_id=None, initial_settings={}):
def setup(self, vmname, name=None, console=None, vbox_id=None, linked_clone=False, initial_settings={}):
"""
Setups this VirtualBox VM.
:param name: optional name
"""
self._linked_clone = linked_clone
# let's create a unique name if none has been chosen
if not name:
name = vmname
self.setName(name)
#name = self.allocateName("VBOX")
if linked_clone:
name = self.allocateName(vmname + "-")
else:
name = vmname
self.setName(name)
if not name:
self.error_signal.emit(self.id(), "could not allocate a name for this VirtualBox VM")
return
params = {"name": name,
"vmname": vmname}
"vmname": vmname,
"linked_clone": linked_clone}
if console:
params["console"] = self._settings["console"] = console
@@ -179,9 +189,13 @@ class VirtualBoxVM(Node):
:param new_settings: settings dictionary
"""
if "name" in new_settings and new_settings["name"] != self.name() and self.hasAllocatedName(new_settings["name"]):
self.error_signal.emit(self.id(), 'Name "{}" is already used by another node'.format(new_settings["name"]))
return
if "name" in new_settings and new_settings["name"] != self.name():
if self.hasAllocatedName(new_settings["name"]):
self.error_signal.emit(self.id(), 'Name "{}" is already used by another node'.format(new_settings["name"]))
return
elif self._linked_clone:
# forces the update of the VM name in VirtualBox.
new_settings["vmname"] = new_settings["name"]
params = {"id": self._vbox_id}
for name, value in new_settings.items():
@@ -469,7 +483,7 @@ class VirtualBoxVM(Node):
try:
port.startPacketCapture(result["capture_file_path"])
except OSError as e:
self.error_signal.emit(self.id(), "could not start the packet capture reader: {}".format(e))
self.error_signal.emit(self.id(), "could not start the packet capture reader: {}: {}".format(e, e.filename))
self.updated_signal.emit()
break
@@ -520,11 +534,13 @@ class VirtualBoxVM(Node):
info = """VirtualBox VM {name} is {state}
Node ID is {id}, server's VirtualBox VM ID is {vbox_id}
VirtualBox name is "{vmname}"
console is on port {console}
""".format(name=self.name(),
id=self.id(),
vbox_id=self._vbox_id,
state=state,
vmname=self._settings["vmname"],
console=self._settings["console"])
port_info = ""
@@ -547,6 +563,7 @@ class VirtualBoxVM(Node):
vbox_vm = {"id": self.id(),
"vbox_id": self._vbox_id,
"linked_clone": self._linked_clone,
"type": self.__class__.__name__,
"description": str(self),
"properties": {},
@@ -575,6 +592,7 @@ class VirtualBoxVM(Node):
self.node_info = node_info
vbox_id = node_info.get("vbox_id")
linked_clone = node_info.get("linked_clone", False)
settings = node_info["properties"]
name = settings.pop("name")
vmname = settings.pop("vmname")
@@ -584,7 +602,7 @@ class VirtualBoxVM(Node):
self._loading = True
log.info("VirtualBox VM {} is loading".format(name))
self.setName(name)
self.setup(vmname, name, console, vbox_id, settings)
self.setup(vmname, name, console, vbox_id, linked_clone, settings)
def _updatePortSettings(self):
"""
@@ -643,7 +661,7 @@ class VirtualBoxVM(Node):
:return: boolean
"""
if self._settings["enable_console"]:
if self._settings["enable_remote_console"]:
return False
return True

View File

@@ -20,12 +20,23 @@ VPCS module implementation.
"""
import os
from gns3.qt import QtCore, QtGui
import subprocess
import sys
import signal
import socket
import re
import pkg_resources
from gns3.qt import QtCore
from gns3.servers import Servers
from gns3.utils.get_resource import get_resource
from gns3.utils.get_default_base_config import get_default_base_config
from ..module import Module
from ..module_error import ModuleError
from .vpcs_device import VPCSDevice
from .settings import VPCS_SETTINGS, VPCS_SETTING_TYPES
from ...settings import ENABLE_CLOUD
import logging
log = logging.getLogger(__name__)
@@ -44,6 +55,9 @@ class VPCS(Module):
self._servers = []
self._working_dir = ""
self._vpcs_multi_host_process = None
self._vpcs_multi_host_port = 0
# load the settings
self._loadSettings()
@@ -59,6 +73,9 @@ class VPCS(Module):
self._settings[name] = settings.value(name, value, type=VPCS_SETTING_TYPES[name])
settings.endGroup()
if not self._settings["base_script_file"]:
self._settings["base_script_file"] = get_default_base_config(get_resource(os.path.join("configs", "vpcs_base_config.txt")))
def _saveSettings(self):
"""
Saves the settings to the persistent settings file.
@@ -171,6 +188,8 @@ class VPCS(Module):
if server.isLocal():
params.update({"working_dir": self._working_dir})
else:
if "path" in params:
del params["path"] # do not send VPCS path to remote servers
project_name = os.path.basename(self._working_dir)
if project_name.endswith("-files"):
project_name = project_name[:-6]
@@ -252,6 +271,7 @@ class VPCS(Module):
"""
log.info("vpcs module reset")
self.stopMultiHostVPCS()
for server in self._servers:
if server.connected():
server.send_notification("vpcs.reset")
@@ -281,8 +301,8 @@ class VPCS(Module):
"""
for node in self._nodes:
if hasattr(node, "exportConfig") and node.initialized():
node.exportConfig(directory)
if node.initialized():
node.exportConfigToDirectory(directory)
def importConfigs(self, directory):
"""
@@ -292,8 +312,81 @@ class VPCS(Module):
"""
for node in self._nodes:
if hasattr(node, "importConfig") and node.initialized():
node.importConfig(directory)
if node.initialized():
node.importConfigFromDirectory(directory)
def _check_vpcs_version(self, working_dir):
"""
Checks if the VPCS executable version is >= 0.5b1.
"""
try:
output = subprocess.check_output([self._settings["path"], "-v"], cwd=working_dir)
match = re.search("Welcome to Virtual PC Simulator, version ([0-9a-z\.]+)", output.decode("utf-8"))
if match:
version = match.group(1)
if pkg_resources.parse_version(version) < pkg_resources.parse_version("0.5b1"):
raise ModuleError("VPCS executable version must be >= 0.5b1")
else:
raise ModuleError("Could not determine the VPCS version for {}".format(self._settings["path"]))
except (OSError, subprocess.SubprocessError) as e:
raise ModuleError("Error while looking for the VPCS version: {}".format(e))
def startMultiHostVPCS(self, working_dir):
"""
Starts a VPCS process for multi-host support.
:param working_dir: VPCS multi-host working directory
:return: VPCS listening port
"""
if self._vpcs_multi_host_process and self._vpcs_multi_host_process.poll() is None:
return self._vpcs_multi_host_port
if not self._settings["path"]:
raise ModuleError("No path to a VPCS executable has been set")
if not os.path.isfile(self._settings["path"]):
raise ModuleError("VPCS program '{}' is not accessible".format(self._settings["path"]))
if not os.access(self._settings["path"], os.X_OK):
raise ModuleError("VPCS program '{}' is not executable".format(self._settings["path"]))
self._check_vpcs_version(working_dir)
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.bind(("127.0.0.1", 0))
self._vpcs_multi_host_port = sock.getsockname()[1]
except OSError as e:
raise ModuleError("Cannot get a free port: {}".format(e))
flags = 0
if sys.platform.startswith("win32"):
flags = subprocess.CREATE_NEW_PROCESS_GROUP
try:
vpcs_command = [self._settings["path"], "-p", str(self._vpcs_multi_host_port), "-F"]
self._vpcs_multi_host_process = subprocess.Popen(vpcs_command, cwd=working_dir, creationflags=flags)
except (OSError, subprocess.SubprocessError) as e:
raise ModuleError("Could not start VPCS {}".format(e))
return self._vpcs_multi_host_port
def stopMultiHostVPCS(self):
"""
Stops the VPCS process for multi-host support.
"""
if self._vpcs_multi_host_process and self._vpcs_multi_host_process.poll() is None:
log.info("stopping VPCS multi-host instance PID={}".format(self._vpcs_multi_host_process.pid))
if sys.platform.startswith("win32"):
self._vpcs_multi_host_process.send_signal(signal.CTRL_BREAK_EVENT)
else:
self._vpcs_multi_host_process.terminate()
self._vpcs_multi_host_process.wait()
self._vpcs_multi_host_process = None
@staticmethod
def getNodeClass(name):
@@ -340,6 +433,15 @@ class VPCS(Module):
"default_symbol": node_class.defaultSymbol(),
"hover_symbol": node_class.hoverSymbol()}
)
if ENABLE_CLOUD:
nodes.append(
{"class": node_class.__name__,
"name": node_class.symbolName() + " (cloud)",
"server": "cloud",
"categories": node_class.categories(),
"default_symbol": node_class.defaultSymbol(),
"hover_symbol": node_class.hoverSymbol()}
)
return nodes
@staticmethod

View File

@@ -20,9 +20,9 @@ Configuration page for VPCS devices.
"""
import os
import sys
import pkg_resources
from gns3.qt import QtGui
from gns3.utils.get_resource import get_resource
from ..ui.vpcs_device_configuration_page_ui import Ui_VPCSDeviceConfigPageWidget
@@ -35,27 +35,24 @@ class VPCSDeviceConfigurationPage(QtGui.QWidget, Ui_VPCSDeviceConfigPageWidget):
QtGui.QWidget.__init__(self)
self.setupUi(self)
self.uiScriptFileToolButton.clicked.connect(self._scriptFileBrowserSlot)
#self.uiScriptFileToolButton.clicked.connect(self._scriptFileBrowserSlot)
def _scriptFileBrowserSlot(self):
"""
Slot to open a file browser and select a script-file file.
"""
if hasattr(sys, "frozen"):
config_dir = "configs"
else:
config_dir = pkg_resources.resource_filename("gns3", "configs")
path = QtGui.QFileDialog.getOpenFileName(self, "Select a startup configuration", config_dir)
if not path:
return
if not os.access(path, os.R_OK):
QtGui.QMessageBox.critical(self, "Startup configuration", "Cannot read {}".format(path))
return
self.uiScriptFileLineEdit.clear()
self.uiScriptFileLineEdit.setText(path)
# def _scriptFileBrowserSlot(self):
# """
# Slot to open a file browser and select a script-file file.
# """
#
# config_dir = get_resource("configs")
# path = QtGui.QFileDialog.getOpenFileName(self, "Select a startup configuration", config_dir)
# if not path:
# return
#
# if not os.access(path, os.R_OK):
# QtGui.QMessageBox.critical(self, "Startup configuration", "Cannot read {}".format(path))
# return
#
# self.uiScriptFileLineEdit.clear()
# self.uiScriptFileLineEdit.setText(path)
def loadSettings(self, settings, node, group=False):
"""
@@ -71,16 +68,16 @@ class VPCSDeviceConfigurationPage(QtGui.QWidget, Ui_VPCSDeviceConfigPageWidget):
self.uiConsolePortSpinBox.setValue(settings["console"])
# load the script-file
self.uiScriptFileLineEdit.setText(settings["script_file"])
# self.uiScriptFileLineEdit.setText(settings["script_file"])
else:
self.uiNameLabel.hide()
self.uiNameLineEdit.hide()
self.uiConsolePortLabel.hide()
self.uiConsolePortSpinBox.hide()
self.uiScriptFileLabel.hide()
self.uiScriptFileLineEdit.hide()
self.uiScriptFileToolButton.hide()
#self.uiScriptFileLabel.hide()
#self.uiScriptFileLineEdit.hide()
#self.uiScriptFileToolButton.hide()
def saveSettings(self, settings, node, group=False):
"""
@@ -104,12 +101,12 @@ class VPCSDeviceConfigurationPage(QtGui.QWidget, Ui_VPCSDeviceConfigPageWidget):
settings["console"] = self.uiConsolePortSpinBox.value()
script_file = self.uiScriptFileLineEdit.text()
if script_file != settings["script_file"]:
if os.access(script_file, os.R_OK):
settings["script_file"] = script_file
else:
QtGui.QMessageBox.critical(self, "Script-file", "Cannot read the script-file file")
#script_file = self.uiScriptFileLineEdit.text()
#if script_file != settings["script_file"]:
# if os.access(script_file, os.R_OK):
# settings["script_file"] = script_file
# else:
# QtGui.QMessageBox.critical(self, "Script-file", "Cannot read the script-file file")
else:
del settings["name"]
del settings["console"]

View File

@@ -21,9 +21,10 @@ Configuration page for VPCS preferences.
import os
import sys
import pkg_resources
from gns3.qt import QtGui
from gns3.qt import QtCore, QtGui
from gns3.servers import Servers
from .. import VPCS
from ..ui.vpcs_preferences_page_ui import Ui_VPCSPreferencesPageWidget
from ..settings import VPCS_SETTINGS
@@ -72,10 +73,7 @@ class VPCSPreferencesPage(QtGui.QWidget, Ui_VPCSPreferencesPageWidget):
Slot to open a file browser and select a base script file for VPCS
"""
if hasattr(sys, "frozen"):
config_dir = "configs"
else:
config_dir = pkg_resources.resource_filename("gns3", "configs")
config_dir = os.path.join(os.path.dirname(QtCore.QSettings().fileName()), "base_configs")
path = QtGui.QFileDialog.getOpenFileName(self, "Select a script file", config_dir)
if not path:
return
@@ -122,14 +120,6 @@ class VPCSPreferencesPage(QtGui.QWidget, Ui_VPCSPreferencesPageWidget):
self.uiUDPStartPortSpinBox.setValue(settings["udp_start_port_range"])
self.uiUDPEndPortSpinBox.setValue(settings["udp_end_port_range"])
if not self.uiScriptFileEdit.text():
resource_name = "configs/vpcs_base_config.txt"
if hasattr(sys, "frozen") and os.path.isfile(resource_name):
self.uiScriptFileEdit.setText(os.path.normpath(resource_name))
elif pkg_resources.resource_exists("gns3", resource_name):
vpcs_base_config_path = pkg_resources.resource_filename("gns3", resource_name)
self.uiScriptFileEdit.setText(os.path.normpath(vpcs_base_config_path))
def _updateRemoteServersSlot(self):
"""
Adds/Updates the available remote servers.

View File

@@ -25,44 +25,20 @@
<widget class="QLineEdit" name="uiNameLineEdit"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="uiScriptFileLabel">
<property name="text">
<string>Script-File:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QLineEdit" name="uiScriptFileLineEdit"/>
</item>
<item>
<widget class="QToolButton" name="uiScriptFileToolButton">
<property name="text">
<string>...</string>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextOnly</enum>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QLabel" name="uiConsolePortLabel">
<property name="text">
<string>Console port:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<item row="1" column="1">
<widget class="QSpinBox" name="uiConsolePortSpinBox">
<property name="maximum">
<number>65535</number>
</property>
</widget>
</item>
<item row="3" column="1">
<item row="2" column="1">
<spacer name="spacer">
<property name="orientation">
<enum>Qt::Vertical</enum>

View File

@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file '/home/jbbowen/Desktop/Toptal/github/gns3-gui/gns3/modules/vpcs/ui/vpcs_device_configuration_page.ui'
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/vpcs/ui/vpcs_device_configuration_page.ui'
#
# Created: Tue May 13 14:41:22 2014
# Created: Wed Dec 24 17:44:48 2014
# by: PyQt4 UI code generator 4.10.4
#
# WARNING! All changes made in this file will be lost!
@@ -35,28 +35,15 @@ class Ui_VPCSDeviceConfigPageWidget(object):
self.uiNameLineEdit = QtGui.QLineEdit(VPCSDeviceConfigPageWidget)
self.uiNameLineEdit.setObjectName(_fromUtf8("uiNameLineEdit"))
self.gridLayout.addWidget(self.uiNameLineEdit, 0, 1, 1, 1)
self.uiScriptFileLabel = QtGui.QLabel(VPCSDeviceConfigPageWidget)
self.uiScriptFileLabel.setObjectName(_fromUtf8("uiScriptFileLabel"))
self.gridLayout.addWidget(self.uiScriptFileLabel, 1, 0, 1, 1)
self.horizontalLayout_4 = QtGui.QHBoxLayout()
self.horizontalLayout_4.setObjectName(_fromUtf8("horizontalLayout_4"))
self.uiScriptFileLineEdit = QtGui.QLineEdit(VPCSDeviceConfigPageWidget)
self.uiScriptFileLineEdit.setObjectName(_fromUtf8("uiScriptFileLineEdit"))
self.horizontalLayout_4.addWidget(self.uiScriptFileLineEdit)
self.uiScriptFileToolButton = QtGui.QToolButton(VPCSDeviceConfigPageWidget)
self.uiScriptFileToolButton.setToolButtonStyle(QtCore.Qt.ToolButtonTextOnly)
self.uiScriptFileToolButton.setObjectName(_fromUtf8("uiScriptFileToolButton"))
self.horizontalLayout_4.addWidget(self.uiScriptFileToolButton)
self.gridLayout.addLayout(self.horizontalLayout_4, 1, 1, 1, 1)
self.uiConsolePortLabel = QtGui.QLabel(VPCSDeviceConfigPageWidget)
self.uiConsolePortLabel.setObjectName(_fromUtf8("uiConsolePortLabel"))
self.gridLayout.addWidget(self.uiConsolePortLabel, 2, 0, 1, 1)
self.gridLayout.addWidget(self.uiConsolePortLabel, 1, 0, 1, 1)
self.uiConsolePortSpinBox = QtGui.QSpinBox(VPCSDeviceConfigPageWidget)
self.uiConsolePortSpinBox.setMaximum(65535)
self.uiConsolePortSpinBox.setObjectName(_fromUtf8("uiConsolePortSpinBox"))
self.gridLayout.addWidget(self.uiConsolePortSpinBox, 2, 1, 1, 1)
self.gridLayout.addWidget(self.uiConsolePortSpinBox, 1, 1, 1, 1)
spacerItem = QtGui.QSpacerItem(263, 212, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
self.gridLayout.addItem(spacerItem, 3, 1, 1, 1)
self.gridLayout.addItem(spacerItem, 2, 1, 1, 1)
self.retranslateUi(VPCSDeviceConfigPageWidget)
QtCore.QMetaObject.connectSlotsByName(VPCSDeviceConfigPageWidget)
@@ -64,7 +51,5 @@ class Ui_VPCSDeviceConfigPageWidget(object):
def retranslateUi(self, VPCSDeviceConfigPageWidget):
VPCSDeviceConfigPageWidget.setWindowTitle(_translate("VPCSDeviceConfigPageWidget", "VPCS device configuration", None))
self.uiNameLabel.setText(_translate("VPCSDeviceConfigPageWidget", "Name:", None))
self.uiScriptFileLabel.setText(_translate("VPCSDeviceConfigPageWidget", "Script-File:", None))
self.uiScriptFileToolButton.setText(_translate("VPCSDeviceConfigPageWidget", "...", None))
self.uiConsolePortLabel.setText(_translate("VPCSDeviceConfigPageWidget", "Console port:", None))

View File

@@ -54,7 +54,9 @@ class VPCSDevice(Node):
"console": None}
port_name = EthernetPort.longNameType() + str(0)
short_name = EthernetPort.shortNameType() + str(0)
port = EthernetPort(port_name)
port.setShortName(short_name)
port.setPortNumber(0)
self._ports.append(port)
log.debug("port {} has been added".format(port_name))
@@ -505,7 +507,39 @@ class VPCSDevice(Node):
self._inital_settings = None
self._loading = False
def exportConfig(self, directory):
def exportConfig(self, config_export_path):
"""
Exports the script file.
:param config_export_path: export path for the script file
"""
self._config_export_path = config_export_path
self._server.send_message("vpcs.export_config", {"id": self._vpcs_id}, self._exportConfigCallback)
def _exportConfigCallback(self, result, error=False):
"""
Callback for exportConfig.
:param result: server response
:param error: indicates an error (boolean)
"""
if error:
log.error("error while exporting {} configs: {}".format(self.name(), result["message"]))
self.server_error_signal.emit(self.id(), result["code"], result["message"])
else:
if "script_file_base64" in result and self._config_export_path:
config = base64.decodebytes(result["script_file_base64"].encode("utf-8"))
try:
with open(self._config_export_path, "wb") as f:
log.info("saving {} script file to {}".format(self.name(), self._config_export_path))
f.write(config)
except OSError as e:
self.error_signal.emit(self.id(), "could not export the script file to {}: {}".format(self._config_export_path, e))
def exportConfigToDirectory(self, directory):
"""
Exports the script-file to a directory.
@@ -513,11 +547,11 @@ class VPCSDevice(Node):
"""
self._export_directory = directory
self._server.send_message("vpcs.export_config", {"id": self._vpcs_id}, self._exportConfigCallback)
self._server.send_message("vpcs.export_config", {"id": self._vpcs_id}, self._exportConfigToDirectoryCallback)
def _exportConfigCallback(self, result, error=False):
def _exportConfigToDirectoryCallback(self, result, error=False):
"""
Callback for exportConfigs.
Callback for exportConfigToDirectory.
:param result: server response
:param error: indicates an error (boolean)
@@ -540,7 +574,17 @@ class VPCSDevice(Node):
self._export_directory = None
def importConfig(self, directory):
def importConfig(self, path):
"""
Imports a script-file.
:param path: path to the script file
"""
new_settings = {"script_file": path}
self.update(new_settings)
def importConfigFromDirectory(self, directory):
"""
Imports an initial-config from a directory.

View File

@@ -17,10 +17,10 @@
import os
import sys
import pkg_resources
from .qt import QtGui, QtCore, QtWebKit
from .ui.news_dock_widget_ui import Ui_NewsDockWidget
from .utils.get_resource import get_resource
import logging
log = logging.getLogger(__name__)
@@ -36,6 +36,8 @@ class NewsDockWidget(QtGui.QDockWidget, Ui_NewsDockWidget):
QtGui.QDockWidget.__init__(self, parent)
self.setupUi(self)
self._visible = True
self.visibilityChanged.connect(self._visibilityChangedSlot)
self.uiWebView.page().setLinkDelegationPolicy(QtWebKit.QWebPage.DelegateAllLinks)
self.uiWebView.linkClicked.connect(self._urlClickedSlot)
self.uiWebView.loadFinished.connect(self._loadFinishedSlot)
@@ -46,7 +48,32 @@ class NewsDockWidget(QtGui.QDockWidget, Ui_NewsDockWidget):
self._timer.timeout.connect(self._loadFinishedSlot)
self._timer.setSingleShot(True)
self._timer.start(5000)
self.uiWebView.load(QtCore.QUrl("http://as.gns3.com/software/docked.html"))
if parent.settings()["default_local_news"]:
self._loadFinishedSlot()
else:
self.uiWebView.load(QtCore.QUrl("http://as.gns3.com/software/docked_200x200.html"))
def _visibilityChangedSlot(self, visible):
"""
Slot for visibility changed signal.
:param visible: either the dock is visible or not
"""
self._visible = visible
def isVisible(self):
return self._visible
def closeEvent(self, event):
"""
You really cannot close that dock (using ATL+F4...)
:param event: closeEvent instance.
"""
event.ignore()
def _refreshSlot(self):
"""
@@ -76,14 +103,12 @@ class NewsDockWidget(QtGui.QDockWidget, Ui_NewsDockWidget):
self._timer.stop()
self._timer.timeout.disconnect()
if result is False:
# load a local resource if the page is not available
resource_name = os.path.join("static", "gns3_jungle.html")
gns3_jungle = None
if hasattr(sys, "frozen") and os.path.isfile(resource_name):
gns3_jungle = os.path.normpath(resource_name)
elif pkg_resources.resource_exists("gns3", resource_name):
gns3_jungle_page = pkg_resources.resource_filename("gns3", resource_name)
gns3_jungle = os.path.normpath(gns3_jungle_page)
if gns3_jungle:
self.uiWebView.load(QtCore.QUrl("file://{}".format(gns3_jungle)))
self._refresh_timer.stop()
# load a local resource if the page is not available
gns3_jungle = get_resource(os.path.join("static", "gns3_jungle.html"))
if gns3_jungle and not (sys.platform.startswith("win") and not sys.maxsize > 2 ** 32):
# do not show the page on Windows 32-bit (crash when no Internet connection)
url = QtCore.QUrl.fromLocalFile(gns3_jungle)
self.uiWebView.load(url)
else:
self.hide()

View File

@@ -20,7 +20,7 @@ Nodes view that list all the available nodes to be dragged and dropped on the QG
"""
import pickle
from .qt import QtCore, QtGui
from .qt import QtCore, QtGui, QtSvg
from .modules import MODULES
@@ -38,22 +38,32 @@ class NodesView(QtGui.QTreeWidget):
# enables the possibility to drag items.
self.setDragEnabled(True)
def populateNodesView(self, category):
def populateNodesView(self, category, project_type):
"""
Populates the nodes view with the device list of the specified
category (None = all devices).
:param category: category of device to list
:param project_type: Filter devices for this type of project (cloud|local)
"""
for module in MODULES:
for node in module.instance().nodes():
if category is not None and category not in node["categories"]:
continue
server_type = node.get("server", None)
if server_type is not None and server_type != project_type:
continue
item = QtGui.QTreeWidgetItem(self)
item.setIcon(0, QtGui.QIcon(node["default_symbol"]))
item.setText(0, node["name"])
item.setData(0, QtCore.Qt.UserRole, node)
svg_renderer = QtSvg.QSvgRenderer(node["default_symbol"])
image = QtGui.QImage(32, 32, QtGui.QImage.Format_ARGB32)
# Set the ARGB to 0 to prevent rendering artifacts
image.fill(0x00000000)
svg_renderer.render(QtGui.QPainter(image))
icon = QtGui.QIcon(QtGui.QPixmap.fromImage(image))
item.setIcon(0, icon)
self.sortByColumn(0, QtCore.Qt.AscendingOrder)

View File

@@ -3,8 +3,7 @@ from ..ui.cloud_preferences_page_ui import Ui_CloudPreferencesPageWidget
from ..settings import CLOUD_PROVIDERS
from ..utils import import_from_string
from PyQt4 import QtGui
from PyQt4 import Qt
from ..qt import QtCore, QtGui
class CloudPreferencesPage(QtGui.QWidget, Ui_CloudPreferencesPageWidget):
@@ -58,7 +57,7 @@ class CloudPreferencesPage(QtGui.QWidget, Ui_CloudPreferencesPageWidget):
return self.uiRememberAPIKeyRadioButton.isChecked()
def _terms_accepted(self):
return self.uiTermsCheckBox.checkState() == Qt.Qt.Checked
return self.uiTermsCheckBox.checkState() == QtCore.Qt.Checked
def _validate(self):
"""
@@ -105,11 +104,10 @@ class CloudPreferencesPage(QtGui.QWidget, Ui_CloudPreferencesPageWidget):
default_image = self.settings['default_image']
default_flavor = self.settings['default_flavor']
new_instance_flavor = self.settings['new_instance_flavor']
gns3_ias_url = self.settings['gns3_ias_url']
# instance a provider controller and try to use it
try:
provider = self.provider_controllers[provider_id](username, apikey, gns3_ias_url)
provider = self.provider_controllers[provider_id](username, apikey)
if provider.authenticate():
provider.set_region(region)
# fill region combo box
@@ -121,11 +119,9 @@ class CloudPreferencesPage(QtGui.QWidget, Ui_CloudPreferencesPageWidget):
self.uiRegionComboBox.addItem(api_region_names[0])
self.region_index_id.append(api_libcloud_names[0])
# fill image template list
self.image_index_id = [""]
self.uiImageTemplateComboBox.addItem("Select default template...")
for image_id, image_name in provider.list_images().items():
self.uiImageTemplateComboBox.addItem(image_name)
self.image_index_id.append(image_id)
self.image_index_id = ["cc6e0096-84f9-4beb-a21e-d80a11a769d8"]
self.uiImageTemplateComboBox.addItem("Ubuntu 14.04")
self.uiImageTemplateComboBox.setCurrentIndex(0)
# fill flavor comboboxes
for id, name in provider.list_flavors().items():
self.uiInstanceFlavorComboBox.addItem(name)

View File

@@ -23,7 +23,7 @@ import os
import shutil
from gns3.qt import QtGui, QtCore
from ..ui.general_preferences_page_ui import Ui_GeneralPreferencesPageWidget
from ..settings import GRAPHICS_VIEW_SETTINGS, GENERAL_SETTINGS, PRECONFIGURED_TELNET_CONSOLE_COMMANDS, PRECONFIGURED_SERIAL_CONSOLE_COMMANDS
from ..settings import GRAPHICS_VIEW_SETTINGS, GENERAL_SETTINGS, PRECONFIGURED_TELNET_CONSOLE_COMMANDS, PRECONFIGURED_SERIAL_CONSOLE_COMMANDS, STYLES
class GeneralPreferencesPage(QtGui.QWidget, Ui_GeneralPreferencesPageWidget):
@@ -58,6 +58,7 @@ class GeneralPreferencesPage(QtGui.QWidget, Ui_GeneralPreferencesPageWidget):
self.uiDefaultLabelFontPushButton.clicked.connect(self._setDefaultLabelFontSlot)
self.uiDefaultLabelColorPushButton.clicked.connect(self._setDefaultLabelColorSlot)
self._default_label_color = QtGui.QColor(QtCore.Qt.black)
self.uiStyleComboBox.addItems(STYLES)
def _projectsPathSlot(self):
"""
@@ -141,10 +142,9 @@ class GeneralPreferencesPage(QtGui.QWidget, Ui_GeneralPreferencesPageWidget):
QtGui.QMessageBox.information(self, "Configuration file", "Configuration file imported, default settings will be applied after a restart")
# restart the application
from ..main_window import MainWindow
main_window = MainWindow.instance()
main_window.reboot_signal.emit()
#TODO: implement restart
#QtCore.QProcess.startDetached(QtGui.QApplication.arguments()[0], QtGui.QApplication.arguments())
QtGui.QApplication.quit()
def _exportConfigurationFileSlot(self):
"""
@@ -195,11 +195,15 @@ class GeneralPreferencesPage(QtGui.QWidget, Ui_GeneralPreferencesPageWidget):
self.uiProjectsPathLineEdit.setText(settings["projects_path"])
self.uiImagesPathLineEdit.setText(settings["images_path"])
self.uiTemporaryFilesPathLineEdit.setText(settings["temporary_files_path"])
self.uiLaunchNewProjectDialogCheckBox.setChecked(settings["auto_launch_project_dialog"])
self.uiCheckForUpdateCheckBox.setChecked(settings["check_for_update"])
self.uiLinkManualModeCheckBox.setChecked(settings["link_manual_mode"])
self.uiSlowStartAllSpinBox.setValue(settings["slow_device_start_all"])
self.uiTelnetConsoleCommandLineEdit.setText(settings["telnet_console_command"])
self.uiTelnetConsoleCommandLineEdit.setCursorPosition(0)
index = self.uiStyleComboBox.findText(settings["style"])
if index != -1:
self.uiStyleComboBox.setCurrentIndex(index)
index = self.uiTelnetConsolePreconfiguredCommandComboBox.findData(settings["telnet_console_command"])
if index != -1:
self.uiTelnetConsolePreconfiguredCommandComboBox.setCurrentIndex(index)
@@ -210,7 +214,7 @@ class GeneralPreferencesPage(QtGui.QWidget, Ui_GeneralPreferencesPageWidget):
self.uiSerialConsolePreconfiguredCommandComboBox.setCurrentIndex(index)
self.uiCloseConsoleWindowsOnDeleteCheckBox.setChecked(settings["auto_close_console"])
self.uiBringConsoleWindowToFrontCheckBox.setChecked(settings["bring_console_to_front"])
self.uiSlowConsoleAllDoubleSpinBox.setValue(settings["slow_console_all"])
self.uiDelayConsoleAllSpinBox.setValue(settings["delay_console_all"])
def _populateGraphicsViewSettingWidgets(self, settings):
"""
@@ -253,6 +257,8 @@ class GeneralPreferencesPage(QtGui.QWidget, Ui_GeneralPreferencesPageWidget):
new_settings["projects_path"] = self.uiProjectsPathLineEdit.text()
new_settings["images_path"] = self.uiImagesPathLineEdit.text()
new_settings["temporary_files_path"] = self.uiTemporaryFilesPathLineEdit.text()
new_settings["auto_launch_project_dialog"] = self.uiLaunchNewProjectDialogCheckBox.isChecked()
new_settings["style"] = self.uiStyleComboBox.currentText()
new_settings["check_for_update"] = self.uiCheckForUpdateCheckBox.isChecked()
new_settings["link_manual_mode"] = self.uiLinkManualModeCheckBox.isChecked()
new_settings["slow_device_start_all"] = self.uiSlowStartAllSpinBox.value()
@@ -260,7 +266,7 @@ class GeneralPreferencesPage(QtGui.QWidget, Ui_GeneralPreferencesPageWidget):
new_settings["serial_console_command"] = self.uiSerialConsoleCommandLineEdit.text()
new_settings["auto_close_console"] = self.uiCloseConsoleWindowsOnDeleteCheckBox.isChecked()
new_settings["bring_console_to_front"] = self.uiBringConsoleWindowToFrontCheckBox.isChecked()
new_settings["slow_console_all"] = self.uiSlowConsoleAllDoubleSpinBox.value()
new_settings["delay_console_all"] = self.uiDelayConsoleAllSpinBox.value()
from ..main_window import MainWindow
MainWindow.instance().setSettings(new_settings)

View File

@@ -179,6 +179,7 @@ class ServerPreferencesPage(QtGui.QWidget, Ui_ServerPreferencesPageWidget):
self.uiLocalServerPortSpinBox.setValue(local_server.port)
self.uiLocalServerPathLineEdit.setText(servers.localServerPath())
self.uiLocalServerAutoStartCheckBox.setChecked(servers.localServerAutoStart())
self.uiConsoleConnectionsToAnyIPCheckBox.setChecked(servers.localServerAllowConsoleFromAnywhere())
# load remote server preferences
self._remote_servers.clear()
@@ -206,6 +207,7 @@ class ServerPreferencesPage(QtGui.QWidget, Ui_ServerPreferencesPageWidget):
local_server_port = self.uiLocalServerPortSpinBox.value()
local_server_path = self.uiLocalServerPathLineEdit.text()
local_server_auto_start = self.uiLocalServerAutoStartCheckBox.isChecked()
local_server_allow_console_from_anywhere = self.uiConsoleConnectionsToAnyIPCheckBox.isChecked()
if local_server_path:
if not os.path.isfile(local_server_path):
@@ -214,7 +216,10 @@ class ServerPreferencesPage(QtGui.QWidget, Ui_ServerPreferencesPageWidget):
QtGui.QMessageBox.critical(self, "Local server", "{} is not an executable".format(local_server_path))
else:
server = servers.localServer()
if servers.localServerPath() != local_server_path or server.host != local_server_host or server.port != local_server_port:
if servers.localServerPath() != local_server_path or \
server.host != local_server_host or \
server.port != local_server_port or \
servers.localServerAllowConsoleFromAnywhere() != local_server_allow_console_from_anywhere:
# first check if we have nodes on the local server
local_nodes = []
@@ -228,6 +233,12 @@ class ServerPreferencesPage(QtGui.QWidget, Ui_ServerPreferencesPageWidget):
MessageBox(self, "Local server", "Please close your project or delete all the nodes running on the local server before changing settings", nodes)
return
servers.setLocalServer(local_server_path,
local_server_host,
local_server_port,
local_server_auto_start,
local_server_allow_console_from_anywhere)
# local server settings have changed, let's stop the current local server.
if server.connected() and not sys.platform.startswith('win'):
server.close_connection()
@@ -240,8 +251,12 @@ class ServerPreferencesPage(QtGui.QWidget, Ui_ServerPreferencesPageWidget):
dialog.exec_()
else:
QtGui.QMessageBox.critical(self, "Local server", "Could not start the local server process: {}".format(local_server_path))
servers.setLocalServer(local_server_path, local_server_host, local_server_port, local_server_auto_start)
else:
servers.setLocalServer(local_server_path,
local_server_host,
local_server_port,
local_server_auto_start,
local_server_allow_console_from_anywhere)
# save the remote server preferences
servers.updateRemoteServers(self._remote_servers)

View File

@@ -57,6 +57,7 @@ class Port(object):
Port._instance_count += 1
self._name = name
self._short_name = None
self._port_number = None
self._slot_number = None
self._stub = stub
@@ -128,6 +129,26 @@ class Port(object):
self._name = new_name
def shortName(self):
"""
Returns the short name of this port.
:returns: current short port name (string)
"""
if not self._short_name:
return self._name
return self._short_name
def setShortName(self, short_name):
"""
Sets a new short name for this port.
:param short_name: short port name (string)
"""
self._short_name = short_name
def status(self):
"""
Returns the status of this port.
@@ -265,16 +286,21 @@ class Port(object):
self._link_id = link_id
def description(self):
def description(self, short=False):
"""
Returns the text description of this port.
:param short: returns a shorter description.
:returns: description
"""
if self._destination_node and self._destination_port:
if short:
return "<-> {port} {name}".format(port=self._destination_port.shortName(),
name=self._destination_node.name())
return "connected to {name} on port {port}".format(name=self._destination_node.name(),
port=self._destination_port.name())
port=self._destination_port.name())
return ""
def setFree(self):

View File

@@ -63,6 +63,6 @@ def serialConsole(vmname):
# use arguments on other platforms
args = shlex.split(command)
subprocess.Popen(args)
except (OSError, ValueError) as e:
except (OSError, subprocess.SubprocessError) as e:
log.warning('could not start serial console "{}": {}'.format(command, e))
raise

View File

@@ -19,13 +19,11 @@
Keeps track of all the local and remote servers and their settings.
"""
import os
import sys
import shlex
import signal
import socket
import subprocess
import ssl
from .qt import QtCore
from .websocket_client import WebSocketClient, SecureWebSocketClient
from .settings import DEFAULT_LOCAL_SERVER_PATH
@@ -53,6 +51,7 @@ class Servers(QtCore.QObject):
self._cloud_servers = {}
self._local_server_path = ""
self._local_server_auto_start = True
self._local_server_allow_console_from_anywhere = False
self._local_server_proccess = None
self._settings = self._loadSettings()
self._remote_server_iter_pos = 0
@@ -78,8 +77,13 @@ class Servers(QtCore.QObject):
local_server_port = settings.value("local_server_port", DEFAULT_LOCAL_SERVER_PORT, type=int)
local_server_path = settings.value("local_server_path", DEFAULT_LOCAL_SERVER_PATH)
local_server_auto_start = settings.value("local_server_auto_start", True, type=bool)
heartbeat_freq = settings.value("heartbeat_freq", DEFAULT_HEARTBEAT_FREQ, type=int)
self.setLocalServer(local_server_path, local_server_host, local_server_port, local_server_auto_start, heartbeat_freq)
local_server_allow_console_from_anywhere = settings.value("local_server_allow_console_from_anywhere", False, type=bool)
self.setLocalServer(local_server_path,
local_server_host,
local_server_port,
local_server_auto_start,
local_server_allow_console_from_anywhere)
# load the remote servers
size = settings.beginReadArray("remote")
@@ -109,6 +113,7 @@ class Servers(QtCore.QObject):
settings.setValue("local_server_port", self._local_server.port)
settings.setValue("local_server_path", self._local_server_path)
settings.setValue("local_server_auto_start", self._local_server_auto_start)
settings.setValue("local_server_allow_console_from_anywhere", self._local_server_allow_console_from_anywhere)
# save the remote servers
settings.beginWriteArray("remote", len(self._remote_servers))
@@ -131,6 +136,16 @@ class Servers(QtCore.QObject):
return self._local_server_auto_start
def localServerAllowConsoleFromAnywhere(self):
"""
Returns either the local server
is allows console connections to any local IP address.
:returns: boolean
"""
return self._local_server_allow_console_from_anywhere
def localServerPath(self):
"""
Returns the local server path.
@@ -145,7 +160,10 @@ class Servers(QtCore.QObject):
Starts the local server process.
"""
command = '"{executable}" --host={host} --port={port}'.format(executable=path, host=host, port=port)
command = '"{executable}" --host={host} --port={port} --console_bind_to_any={bind}'.format(executable=path,
host=host,
port=port,
bind=self._local_server_allow_console_from_anywhere)
# settings_dir = os.path.dirname(QtCore.QSettings().fileName())
# if os.path.isdir(settings_dir):
@@ -172,7 +190,7 @@ class Servers(QtCore.QObject):
# use arguments on other platforms
args = shlex.split(command)
self._local_server_proccess = subprocess.Popen(args)
except OSError as e:
except (OSError, subprocess.SubprocessError) as e:
log.warning('could not start local server "{}": {}'.format(command, e))
return False
@@ -180,18 +198,18 @@ class Servers(QtCore.QObject):
def stopLocalServer(self, wait=False):
if self._local_server and self._local_server.connected() and not sys.platform.startswith('win'):
# only gracefully disconnect if we are not on Windows
self._local_server.close_connection()
#if self._local_server and self._local_server.connected() and not sys.platform.startswith('win'):
# # only gracefully disconnect if we are not on Windows
# self._local_server.close_connection()
if self._local_server_proccess and self._local_server_proccess.poll() is None:
if sys.platform.startswith("win"):
self._local_server_proccess.send_signal(signal.CTRL_BREAK_EVENT)
else:
self._local_server_proccess.send_signal(signal.SIGINT)
if wait:
self._local_server_proccess.wait()
self._local_server_proccess.wait(timeout=3000)
def setLocalServer(self, path, host, port, auto_start, heartbeat_freq=DEFAULT_HEARTBEAT_FREQ):
def setLocalServer(self, path, host, port, auto_start, allow_console_from_anywhere):
"""
Sets the local server.
@@ -200,11 +218,12 @@ class Servers(QtCore.QObject):
:param port: port of the server (integer)
:param auto_start: either the local server should be
automatically started on startup (boolean)
:param heartbeat_freq: The interval to send heartbeats to the server
:param allow_console_from_anywhere: allow console connections to any local IP address
"""
self._local_server_path = path
self._local_server_auto_start = auto_start
self._local_server_allow_console_from_anywhere = allow_console_from_anywhere
if self._local_server:
if self._local_server.host == host and self._local_server.port == port:
return
@@ -215,7 +234,6 @@ class Servers(QtCore.QObject):
url = "ws://{host}:{port}".format(host=host, port=port)
self._local_server = WebSocketClient(url)
self._local_server.setLocal(True)
self._local_server.enableHeartbeatsAt(heartbeat_freq)
log.info("new local server connection {} registered".format(url))
def localServer(self):
@@ -227,7 +245,6 @@ class Servers(QtCore.QObject):
return self._local_server
def _addRemoteServer(self, host, port, heartbeat_freq=DEFAULT_HEARTBEAT_FREQ):
"""
Adds a new remote server.
@@ -299,7 +316,7 @@ class Servers(QtCore.QObject):
return self._remote_servers
def getCloudServer(self, host, port, ca_file):
def getCloudServer(self, host, port, ca_file, auth_user, auth_password, ssh_pkey, instance_id):
"""
Return a websocket connection to the cloud server, creating one if none exists.
@@ -310,13 +327,15 @@ class Servers(QtCore.QObject):
"""
for server in self._remote_servers.values():
if server.host == host and server.port == port:
if server.host == host and int(server.port) == int(port):
return server
heartbeat_freq = self._settings.value("heartbeat_freq", DEFAULT_HEARTBEAT_FREQ)
return self._addCloudServer(host, port, ca_file, heartbeat_freq)
def _addCloudServer(self, host, port, ca_file, heartbeat_freq):
return self._addCloudServer(host, port, ca_file, auth_user, auth_password, ssh_pkey,
heartbeat_freq, instance_id)
def _addCloudServer(self, host, port, ca_file, auth_user, auth_password, ssh_pkey,
heartbeat_freq, instance_id):
"""
Create a websocket connection to the specified cloud server
@@ -331,12 +350,43 @@ class Servers(QtCore.QObject):
url = "wss://{host}:{port}".format(host=host, port=port)
log.debug('Starting SecureWebSocketClient url={}'.format(url))
log.debug('Starting SecureWebSocketClient ca_file={}'.format(ca_file))
server = SecureWebSocketClient(url, ca_file)
log.debug('Starting SecureWebSocketClient ssh_pkey={}'.format(ssh_pkey))
server = SecureWebSocketClient(url, instance_id=instance_id)
server.setSecureOptions(ca_file, auth_user, auth_password, ssh_pkey)
server.setCloud(True)
server.enableHeartbeatsAt(heartbeat_freq)
self._cloud_servers[host] = server
log.info("new remote server connection {} registered".format(url))
return server
def anyCloudServer(self):
# Return the first server for now
for key, value in self._cloud_servers.items():
return value
return None
def cloudServerById(self, instance_id):
"""
Return the server with the specified instance id, or None.
"""
for cs in self.cloud_servers.values():
if cs.instance_id == instance_id:
return cs
return None
def removeCloudServer(self, server):
try:
cs = self.cloud_servers[server.host]
cs.close_connection()
del self.cloud_servers[server.host]
return True
except KeyError:
return False
@property
def cloud_servers(self):
return self._cloud_servers
def __iter__(self):
"""
Creates a round-robin system to pick up a remote server.

Some files were not shown because too many files have changed in this diff Show More