Compare commits

...

184 Commits

Author SHA1 Message Date
Julien Duponchelle
b81a531a7b 2.0.1a1 2017-07-24 16:19:16 +02:00
Jeremy Grossmann
089b4108cc Merge pull request #2179 from GNS3/duplicate
Allow to duplicate a node
2017-07-24 01:39:32 -07:00
grossmj
b89f70370a Updating text for duplicating node/project. 2017-07-24 15:38:54 +07:00
Jeremy Grossmann
81b4ded30a Merge pull request #2174 from GNS3/fix_segfault
Try to fix segfault at exit
2017-07-21 08:44:06 -07:00
Julien Duponchelle
b658eea427 Poll local server return code when waiting for server stop 2017-07-21 13:36:03 +02:00
Julien Duponchelle
da225ffdf9 Try to avoid websocket garbage collection 2017-07-21 13:25:02 +02:00
Jeremy Grossmann
b7fb6e6b13 Merge pull request #2171 from GNS3/fix_wifi_off
Fix issues when Wifi is turned off
2017-07-21 02:50:08 -07:00
Julien Duponchelle
078cef064b Allow to duplicate a node
Ref #1065
2017-07-20 18:05:46 +02:00
Julien Duponchelle
bec1c41f75 Handle recent version of Chicken of VNC
Fix #2146
2017-07-20 16:15:35 +02:00
Julien Duponchelle
64f3516153 Catch unknown protocol errors
The server should use a different port automaticaly.

Fix #2120, #2131
2017-07-20 15:31:02 +02:00
Julien Duponchelle
558e8ad8ce Set main window as parent of LocalServer class 2017-07-20 11:51:05 +02:00
Julien Duponchelle
5f7408809e Fix a race condition when we got a project error 2017-07-20 11:09:38 +02:00
Julien Duponchelle
8359da3c76 Increase timeout before quitting GNS3 because server could be slow to stop 2017-07-20 11:07:11 +02:00
Julien Duponchelle
c613e20971 Try to avoid multiple error dialog in case of network issues 2017-07-20 11:03:39 +02:00
Julien Duponchelle
34ab6c2e1b Try to fix segfault at exit
Fix #2166
2017-07-20 10:46:34 +02:00
Julien Duponchelle
5382a8a397 Fix image permissions
Fix #2169
2017-07-20 09:35:05 +02:00
grossmj
507f104ae5 os.geteuid() doesn't exist on Windows. 2017-07-20 12:15:00 +07:00
Jeremy Grossmann
ada2f647a0 Merge pull request #2170 from GNS3/appliances_dir
Add an appliance templates directory
2017-07-19 22:10:03 -07:00
grossmj
347b76d39e Update text for "My custom appliances" 2017-07-20 12:08:58 +07:00
Jeremy Grossmann
3749819016 Merge pull request #2172 from GNS3/suspend_link
Suspend link
2017-07-20 11:15:28 +07:00
grossmj
4d4871d165 Improve link suspend support. 2017-07-20 11:13:17 +07:00
Julien Duponchelle
59f6a22e81 Suspend link
Fix https://github.com/GNS3/gns3-gui/issues/1295
2017-07-19 17:23:19 +02:00
Julien Duponchelle
0982338e2c Fix tests suite for 2.1 GUI 2017-07-19 15:55:07 +02:00
Julien Duponchelle
23c3576256 Fix docker test env 2017-07-19 15:11:31 +02:00
Julien Duponchelle
1dbf30c6cb Fix issues when Wifi is turned off
Fix #2104
2017-07-19 14:50:40 +02:00
Julien Duponchelle
2081689c12 Change text for my appliances 2017-07-19 13:47:02 +02:00
Julien Duponchelle
983c55928e Add an appliance templates directory
* Settings in general preferences to configure it
* When you import a gns3a the file is copy to this directory
* A filter my appliances is available

Fix https://github.com/GNS3/gns3-gui/issues/2133
2017-07-19 11:55:46 +02:00
Julien Duponchelle
de625d6cfc Allow duplicate name in nodes view if emulator are different
Fix #2158
2017-07-19 09:49:03 +02:00
Jeremy Grossmann
523d791cac Merge pull request #2164 from GNS3/tabs-filters
Tabs in packet filters dialog. Fixes #2159
2017-07-17 21:35:43 +07:00
grossmj
270518f294 Add reset button and tab icons to indicate filter status. 2017-07-17 21:33:55 +07:00
Julien Duponchelle
7ab8d679f7 Fix a rare crash 2017-07-17 12:06:27 +02:00
ziajka
f518464eb2 Tabs in packet filters dialog. Fixes #2159 2017-07-17 10:44:15 +02:00
grossmj
321685acb8 Fixes tests. 2017-07-17 13:25:41 +07:00
grossmj
747ca36a5a Fixes uncaught exception in select server. Fixes #2106. 2017-07-17 12:59:17 +07:00
grossmj
485844f8de Fixes missing "style" in label data. Fixes #2110. 2017-07-17 12:40:14 +07:00
grossmj
b7c0a8c368 Fixes bug when no server selected. Fixes #2163. 2017-07-17 12:25:06 +07:00
grossmj
678c42f941 Fixes uBridge check. Fixes #2143. 2017-07-16 16:56:41 +07:00
Jeremy Grossmann
1eaf6c97e0 Merge pull request #2160 from GNS3/run-as-root
Fixes #2048. Inform the user about running as root, disallow change o…
2017-07-16 12:27:28 +07:00
grossmj
f92282f823 Update message to prevent to run as a user. 2017-07-16 12:26:45 +07:00
ziajka
9ef90210d8 Better os handling and message improvement, fixes #2160 2017-07-14 13:11:41 +02:00
Jeremy Grossmann
f280ea4c68 Merge pull request #2161 from GNS3/fix_2157
Fix Wireshark is restarted when updating packet filters
2017-07-12 22:09:42 +07:00
Julien Duponchelle
0067634990 Fix Wireshark is restarted when updating packet filters
Fix #2157
2017-07-12 16:40:52 +02:00
ziajka
4799fc7c93 Fixes #2048. Inform the user about running as root, disallow change of user and disable crash reports in this case 2017-07-12 14:07:49 +02:00
Jeremy Grossmann
829154fb1c Merge pull request #2156 from GNS3/bpf_filter
BPF filter support
2017-07-12 16:23:48 +07:00
grossmj
7e0caba4b0 Small changes for packet filters Ui. 2017-07-12 16:23:29 +07:00
grossmj
3c3890ff21 Fix typo. 2017-07-12 14:12:48 +07:00
Julien Duponchelle
65411d1742 Merge branch 'master' into 2.1 2017-07-11 17:59:12 +02:00
Julien Duponchelle
146a6a5af2 BPF filter support
Fix #765
2017-07-11 17:28:38 +02:00
grossmj
fc72140402 Bring to front complete (Windows only). Fixes #847. 2017-07-11 21:22:22 +07:00
grossmj
1b13d83e38 New icon for bring to front. Ref #847. 2017-07-11 20:02:43 +07:00
grossmj
e3f073d74b Generic best effort bring console to front for all nodes. Ref #847. 2017-07-11 16:31:18 +07:00
grossmj
8f6e84f8a9 Support bring to front for VMware VMs. Ref #847. 2017-07-11 15:31:42 +07:00
grossmj
c594c3d8a7 Fixes bring to front for VirtualBox VMs. Ref #847. 2017-07-11 14:30:26 +07:00
Jeremy Grossmann
a550527e6d Merge pull request #2153 from GNS3/idlepc_apicall
Idlepc apicall
2017-07-10 11:33:26 +07:00
Jeremy Grossmann
0ce377f321 Merge pull request #2145 from GNS3/preserve-file-permissions
Preserve permissions while copying files. Fixes #2125.
2017-07-08 21:56:50 +07:00
grossmj
ab37a6237c Fix small bug when not capturing/filtering on a link. 2017-07-08 21:47:30 +07:00
Jeremy Grossmann
ecc57133c6 Merge pull request #2139 from GNS3/filters
Filter packet interface
2017-07-08 18:51:50 +07:00
grossmj
fc6c2c0304 Filter icons and bug fixes for topology summary view. 2017-07-08 18:46:31 +07:00
Julien Duponchelle
92c731a9c9 Merge pull request #2151 from GNS3/improve-tpl-error
Fixes #2122. Warning dialog on Win and Mac when user has not choice.
2017-07-07 16:58:42 +02:00
Julien Duponchelle
bc433e5281 Merge pull request #2152 from GNS3/bugfix-2121
New appliance - case without versions in file. Fixes #2121
2017-07-07 16:58:04 +02:00
Julien Duponchelle
21eb0b0f03 Call the new api for getting IDLE PC values
Ref #2103
2017-07-07 16:41:17 +02:00
ziajka
3f70d0238f New appliance - case without versions in file. Fixes #2121 2017-07-07 12:59:41 +02:00
ziajka
8fd3f67378 Fixes #2122. Warning dialog on Win and Mac when user has not choice. 2017-07-07 11:54:27 +02:00
Julien Duponchelle
4e9cb90468 Remove dead code 2017-07-07 11:10:40 +02:00
Julien Duponchelle
d38e62fa38 Renable auth
Fix #2148
2017-07-07 11:09:39 +02:00
Julien Duponchelle
4b7df545aa Test if auth is enabled
Ref #2148
2017-07-07 10:20:22 +02:00
grossmj
6af5f5f3fb Stop all packet filters from the topology summary view. 2017-07-07 14:12:36 +07:00
Julien Duponchelle
b9318dfe6a Merge pull request #2140 from GNS3/bugfix-2137
Fixes #2137
2017-07-07 09:01:32 +02:00
Julien Duponchelle
6ffc2c807b Merge pull request #2144 from GNS3/bugfix-2129
Bugfix 2129 - ValueError: stat: path too long for Windows
2017-07-07 09:00:51 +02:00
grossmj
d177ea44bd Change the way the help is displayed for filter dialog. 2017-07-06 20:40:55 +07:00
ziajka
39f3b22817 Removed initial checks in tests due to Windows 2017-07-06 14:53:44 +02:00
ziajka
d157295550 Preserve permissions while copying files. Fixes #2125. 2017-07-06 14:47:08 +02:00
ziajka
d7b9465850 Typo. Fixes #2129 2017-07-06 13:30:59 +02:00
ziajka
0766dac62b Handle ValueError on Windows during checking path with SVG data. Fixes #2129 2017-07-06 13:29:34 +02:00
ziajka
4f81dde2fd Checking if new project dialog is already open, #2140 2017-07-06 11:47:58 +02:00
Julien Duponchelle
4a716000ff Do not crash if zoom is None 2017-07-06 09:58:13 +02:00
Jeremy Grossmann
d8b0e9234e Merge pull request #2134 from GNS3/use_websocket_for_notification
Use websocket for notifications
2017-07-05 15:42:50 +07:00
Julien Duponchelle
bf0af2a929 Cleanup double include of QtWebsockets 2017-07-05 10:33:48 +02:00
Jeremy Grossmann
a05e47a4d2 Merge pull request #2135 from GNS3/bugfix-725
Bugfix 725 (IPV6)
2017-07-05 14:47:56 +07:00
grossmj
996c5c927c Fix call to protocol() and add :: IPv6 address. 2017-07-05 14:47:07 +07:00
Julien Duponchelle
dab7569575 Fix compatibility with Qt version before 5.6 2017-07-04 18:24:50 +02:00
Julien Duponchelle
2a0e8a3b4f Merge pull request #2138 from GNS3/bugfix-1095
Bugfix 1095
2017-07-04 15:20:42 +02:00
ziajka
872e7199e4 Fixes #2137 2017-07-04 15:14:15 +02:00
Julien Duponchelle
67b57a8d78 Filter packet interface
Ref #765
2017-07-04 15:05:41 +02:00
ziajka
ba3c1e6969 Added protocol filtering on available server bind addresses 2017-07-04 14:00:33 +02:00
ziajka
1879172505 Persistent zoom level - refactor 2017-07-04 09:33:25 +02:00
ziajka
22fe51fe5a Show interface labels - persistance 2017-07-04 08:36:37 +02:00
ziajka
b5ac40896f Snap to grid persistance 2017-07-04 08:27:54 +02:00
ziajka
2d6b53245b Show the grid - save the state 2017-07-04 08:14:19 +02:00
ziajka
eee377c4fc Show layers persistent state 2017-07-03 15:09:12 +02:00
ziajka
c34b82c255 State of the zoom settings 2017-07-03 10:40:23 +02:00
grossmj
c750ce8d80 Add QtWebsockets dependency. 2017-06-30 17:49:30 +08:00
ziajka
2d2e682540 Add warning when user tries to run vncviewer on IPv6 host 2017-06-30 10:19:07 +02:00
Julien Duponchelle
e06cf5b9a1 Use websocket for notifications
Fix #2127
2017-06-29 10:58:06 +02:00
ziajka
d25258c47f IPV6 support for spice client 2017-06-28 15:50:25 +02:00
grossmj
1e40a36a48 Delete duplicated code. 2017-06-28 17:20:25 +08:00
Jeremy Grossmann
d5059d22fc Merge pull request #2119 from GNS3/drop_trusty
Drop support for Qt before 5.5 (ubuntu trusty)
2017-06-24 13:54:29 +02:00
grossmj
bae61bdcaa Allow IOU 64-bit images. 2017-06-23 12:00:33 +02:00
Julien Duponchelle
eb44226ee4 Drop support for Qt before 5.5 (ubuntu trusty)
Fix #2080
2017-06-22 14:32:53 +02:00
Jeremy Grossmann
8ee251cbb2 Merge pull request #2087 from GNS3/ethernet_switch_console
Console for ethernet switch
2017-06-21 23:53:07 +02:00
Julien Duponchelle
a84e081a75 Merge pull request #2116 from GNS3/spice-feature
Spice support
2017-06-21 11:56:15 +02:00
ziajka
d92db4e99d Spice support
* SPICE console type for QEMU settings pages
* SPICE settings tab at General Preferences
* Executing SPICE console type
* Preconfigured SPICE client for Windows
* SPICE commands for Win and Mac
2017-06-21 11:21:33 +02:00
Julien Duponchelle
873e04ed9d Merge branch 'master' into 2.1 2017-06-21 10:39:10 +02:00
Julien Duponchelle
c0c41b99eb Merge pull request #2114 from GNS3/revert-2112-spice-feature
Revert "Spice feature"
2017-06-21 10:31:21 +02:00
Julien Duponchelle
12b694047a Revert "Spice feature" 2017-06-21 10:30:49 +02:00
Julien Duponchelle
59651a3fe5 Merge pull request #2112 from GNS3/spice-feature
Spice feature
2017-06-21 10:29:58 +02:00
ziajka
02ad5d2f3a SPICE commands for Win and Mac 2017-06-20 12:37:44 +02:00
ziajka
a31b98f781 Preconfigured SPICE client for Windows 2017-06-20 10:18:25 +02:00
ziajka
e9a674c4e9 Executing SPICE console type 2017-06-16 14:16:54 +02:00
ziajka
4b383e2b06 SPICE settings tab at General Preferences 2017-06-16 11:11:40 +02:00
ziajka
6d2ca353a3 SPICE console type for QEMU settings pages 2017-06-16 10:57:00 +02:00
ziajka
d8b5caf679 Ignore env directory inside the project 2017-06-16 10:27:24 +02:00
Bernhard Ehlers
d61088e3a7 Sync appliance.json with gns3-registry repository
Fix #2107

Signed-off-by: Julien Duponchelle <julien@gns3.net>
2017-06-15 15:39:08 +02:00
Julien Duponchelle
a3f0569663 2.0.4dev1 2017-06-13 10:34:20 +02:00
grossmj
55b80cc9cb Merge remote-tracking branch 'origin/2.1' into 2.1 2017-06-09 22:37:29 +02:00
grossmj
aec6c37016 Do not enable authentication by default. 2017-06-09 22:37:10 +02:00
Julien Duponchelle
117f6ec3b1 Merge branch '2.1' into ethernet_switch_console 2017-06-09 14:30:39 +02:00
Julien Duponchelle
574d6b3792 Merge branch 'master' into 2.1 2017-06-09 14:16:17 +02:00
Julien Duponchelle
9608614aa9 Merge branch 'master' into 2.1 2017-06-09 11:39:24 +02:00
Julien Duponchelle
fe5f80382a Merge branch '2.1' into ethernet_switch_console 2017-06-07 18:30:42 +02:00
Julien Duponchelle
a4ed59200d Merge branch 'master' into 2.1 2017-06-07 18:28:54 +02:00
Julien Duponchelle
3ca05c7427 Console for ethernet switch
Ref https://github.com/GNS3/gns3-server/issues/454
2017-05-31 13:23:37 +02:00
Julien Duponchelle
6a16bcedc0 Reduce debug noise 2017-05-31 12:05:48 +02:00
Julien Duponchelle
56ebfc7fd0 Drop SSL support
Fix #1022
2017-05-26 15:52:09 +02:00
Julien Duponchelle
9b559d43be Fix duplicate node in node view
Fix #2004
2017-05-19 17:22:29 +02:00
grossmj
ad7d06ef21 Fixes typo. 2017-05-19 00:06:50 +02:00
Julien Duponchelle
b88bf71be9 Clean IOU code
Ref https://github.com/GNS3/gns3-gui/issues/2065
2017-05-18 17:13:11 +02:00
Julien Duponchelle
3b019edc82 Avoid an error when downloading symbols not available 2017-05-16 11:22:06 +02:00
grossmj
f3504809ed Bring VirtualBox and VMware VM window to front on Windows. Ref #847. 2017-05-16 11:14:53 +02:00
Julien Duponchelle
23735f35ad Rename linked_base to linked_clone
Ref https://github.com/GNS3/gns3-server/issues/1034
2017-05-16 10:28:11 +02:00
Julien Duponchelle
3adc46fbe2 Merge branch '2.0' into 2.1 2017-05-16 09:31:28 +02:00
Julien Duponchelle
8a303e4563 Merge branch '2.0' into 2.1 2017-05-16 08:38:43 +02:00
Julien Duponchelle
842ad8ae26 Merge branch '2.0' into 2.1 2017-05-15 16:07:32 +02:00
Julien Duponchelle
466c349642 Remove log noise 2017-04-28 12:51:26 +02:00
Julien Duponchelle
1356fd9c69 Reduce log info noise 2017-04-27 15:46:39 +02:00
Julien Duponchelle
2d1c9444c5 Delete noise 2017-04-27 15:13:56 +02:00
Julien Duponchelle
22d7815d8e Fix can't drag VPCS to topology
Fix #2001
2017-04-27 14:28:58 +02:00
Julien Duponchelle
53487d5937 Merge branch '2.0' into 2.1 2017-04-27 10:56:41 +02:00
Julien Duponchelle
ab729d8f67 Close the program if you close the profile select dialog
Fix #1922
2017-04-26 17:05:16 +02:00
Julien Duponchelle
eb5c10de3d Support node uuid is telnet console parameter
Fix #1918
2017-04-26 16:19:59 +02:00
Julien Duponchelle
1b6d534b8e Merge branch '2.0' into 2.1 2017-04-20 10:30:58 +02:00
Julien Duponchelle
a9b5b9eda2 Merge pull request #1906 from GNS3/appliances_api
Move appliances management to the server
2017-04-12 14:36:10 +02:00
Julien Duponchelle
ce12eb86e8 Merge branch '2.0' into 2.1 2017-03-30 10:07:55 +02:00
Julien Duponchelle
d4ffbd9f97 Fix an error on Windows when loading SVG files
Fix #1913
2017-03-09 10:10:06 +01:00
Julien Duponchelle
4c01a465ac Merge branch '2.0' into 2.1 2017-03-08 18:14:32 +01:00
Julien Duponchelle
012bc1e406 Display the appliances in the application
Ref #1045
2017-03-07 18:10:15 +01:00
Julien Duponchelle
05ba772715 Remove log noise 2017-03-07 10:30:59 +01:00
Jeremy Grossmann
9ea57f511b Merge pull request #1830 from GNS3/applicance_in_nodes
Display the appliances in the application
2017-03-06 19:49:48 -07:00
grossmj
5aaa2d7280 Some tweaks for appliance wizard. 2017-03-07 08:40:56 +08:00
Julien Duponchelle
497eb19369 Fix a resize notifications dialog for the first notification 2017-02-28 17:42:23 +01:00
Julien Duponchelle
70049aa877 Merge branch '2.1' into applicance_in_nodes 2017-02-28 15:59:52 +01:00
Julien Duponchelle
ece7930cb1 Merge branch '2.0' into 2.1 2017-02-28 15:59:29 +01:00
Julien Duponchelle
c7df589857 Fix noisy dialog and an error with right click 2017-02-28 15:37:15 +01:00
Julien Duponchelle
8bcc92f319 Merge branch '2.1' into applicance_in_nodes 2017-02-28 15:03:28 +01:00
Julien Duponchelle
dedde63b60 Merge branch '2.0' into 2.1 2017-02-28 14:10:07 +01:00
Jeremy Grossmann
a81d1443f9 Merge pull request #1835 from GNS3/base_config_server_side
Manage base configuration on server
2017-02-19 22:59:27 -08:00
Jeremy Grossmann
e69089f4cf Merge pull request #1865 from GNS3/status_bar_error
Display a count of errors at the bottom of the screen
2017-02-18 05:53:27 -08:00
Julien Duponchelle
0c052542b3 Display a count of errors at the bottom of the screen
You can test it by typing in the console:
log error test
log warning test

Click on the count hide / show the console and reset the counters.

Also now status bar is a dedicated class we can easyly extend it.

Ref #1864
2017-02-17 16:24:58 +01:00
Julien Duponchelle
00e402f28c Merge pull request #1814 from GNS3/show_error
Display an overlay popup with log messages
2017-02-17 11:20:37 +01:00
Julien Duponchelle
0742b282a3 Change some log level to avoid notifications noises 2017-02-17 11:09:18 +01:00
Jeremy Grossmann
2b588aa0bf Merge pull request #1820 from GNS3/line
Allow drawing lines
2017-02-17 02:08:36 -08:00
Julien Duponchelle
92fb8418ab Add a checkbox to display or not notification in app 2017-02-17 11:08:31 +01:00
Julien Duponchelle
9c7dbc864e Display an overlay popup with log messages
Ref #1334
2017-02-17 11:08:31 +01:00
Julien Duponchelle
25aebaa46c Better support for Vertical Line 2017-02-17 11:01:57 +01:00
Jeremy Grossmann
0e30b3cf5f Merge pull request #1842 from GNS3/qemu_more_adapters
Allow up to 275 adapters for qemu
2017-02-17 01:41:24 -08:00
Julien Duponchelle
755667c4d5 Spawn line at correct position 2017-02-17 10:37:27 +01:00
grossmj
16dbdf70d9 Add line icons 2017-02-17 17:17:05 +08:00
Jeremy Grossmann
806c7479ee Merge pull request #1831 from GNS3/scale_percent
Display zoom percentage when changing scale.
2017-02-17 00:25:04 -08:00
Jeremy Grossmann
cc8b84725a Update graphics_view.py 2017-02-17 16:24:46 +08:00
Julien Duponchelle
e01701614e Remember last appliance filter 2017-02-16 17:21:42 +01:00
Julien Duponchelle
efaffac801 Merge branch '2.0' into 2.1 2017-02-16 16:28:21 +01:00
Julien Duponchelle
c58366e9cb When an appliance template is added we hide it 2017-02-10 15:55:48 +01:00
Julien Duponchelle
068ebcdea0 "/appliances" => "/appliances/templates" 2017-02-10 15:55:47 +01:00
Julien Duponchelle
51f2b4bfa8 Display the appliances in the application
Ref #1045
2017-02-10 15:55:47 +01:00
Julien Duponchelle
168e4ab86e Merge branch '2.0' into 2.1 2017-02-10 15:55:17 +01:00
Julien Duponchelle
7ae18ff82a Allow up to 275 adapters for qemu
See https://github.com/GNS3/gns3-server/pull/895 for server part
2017-02-07 17:37:10 +01:00
Julien Duponchelle
c694173f9d Merge branch '2.0' into 2.1 2017-02-07 17:36:45 +01:00
Julien Duponchelle
b58b92c9f0 Merge branch '2.0' into 2.1 2017-02-07 15:03:44 +01:00
Julien Duponchelle
3ddb2e70d4 Manage base configuration on server
Fix #786
2017-02-03 13:18:04 +01:00
Julien Duponchelle
05966a9119 Display zoom percentage when changing scale.
It's like other messages display during 2 seconds.

Fix #1263
2017-02-01 14:51:58 +01:00
Julien Duponchelle
8ea24e9920 Remove unused variables 2017-02-01 14:32:05 +01:00
Julien Duponchelle
47f34fd5af Merge branch '2.0' into 2.1 2017-01-31 17:00:40 +01:00
Julien Duponchelle
89321a6cad Allow drawing lines
Ref #997
2017-01-27 10:15:05 +01:00
Julien Duponchelle
6690ba7108 2.1.0dev1 2017-01-24 10:38:13 +01:00
341 changed files with 140464 additions and 135539 deletions

3
.gitignore vendored
View File

@@ -60,3 +60,6 @@ keys
updates
.cache
__pycache__
# Virtualenv
env

View File

@@ -1,5 +1,13 @@
# Change Log
## 2.1.0a1 24/07/2017
* Packet filtering
* Suspend a link
* Duplicate a node
* Move config to central server
* Appliance templates on server
## 2.0.3 13/06/2017
* Display error when we can't export files

View File

@@ -1,12 +1,12 @@
# Run tests inside a container
FROM ubuntu:vivid
FROM ubuntu:yakkety
MAINTAINER GNS3 Team
#ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update
RUN apt-get install -y --force-yes python3.4 python3-pyqt5 python3-pip python3-pyqt5.qtsvg python3.4-dev xvfb
RUN apt-get install -y --force-yes python3.5 python3-pyqt5 python3-pip python3-pyqt5.qtsvg python3-pyqt5.qtwebsockets python3.5-dev xvfb
RUN apt-get clean
@@ -19,4 +19,4 @@ ADD . /src
WORKDIR /src
CMD xvfb-run python3.4 -m pytest -vv
CMD xvfb-run python3.5 -m pytest -vv

126
gns3/appliance_manager.py Normal file
View File

@@ -0,0 +1,126 @@
#!/usr/bin/env python
#
# Copyright (C) 2016 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 .qt import QtCore
from .controller import Controller
from .utils.server_select import server_select
import logging
log = logging.getLogger(__name__)
class ApplianceManager(QtCore.QObject):
appliances_changed_signal = QtCore.Signal()
def __init__(self):
super().__init__()
self._appliance_templates = []
self._appliances = []
self._controller = Controller.instance()
self._controller.connected_signal.connect(self.refresh)
self._controller.disconnected_signal.connect(self._controllerDisconnectedSlot)
self.refresh()
def refresh(self):
if self._controller.connected():
self._controller.get("/appliances/templates", self._listApplianceTemplateCallback)
self._controller.get("/appliances", self._listAppliancesCallback)
def _controllerDisconnectedSlot(self):
self._appliance_templates = []
self._appliances = []
self.appliances_changed_signal.emit()
def appliance_templates(self):
return self._appliance_templates
def appliances(self):
return self._appliances
def getAppliance(self, appliance_id):
"""
Look for an appliance by appliance ID
"""
for appliance in self._appliances:
if appliance["appliance_id"] == appliance_id:
return appliance
return None
def _listAppliancesCallback(self, result, error=False, **kwargs):
if error is True:
log.error("Error while getting appliances list: {}".format(result["message"]))
return
self._appliances = result
self.appliances_changed_signal.emit()
def _listApplianceTemplateCallback(self, result, error=False, **kwargs):
if error is True:
log.error("Error while getting appliance templates list: {}".format(result["message"]))
return
self._appliance_templates = result
self.appliances_changed_signal.emit()
def createNodeFromApplianceId(self, project, appliance_id, x, y):
for appliance in self._appliances:
if appliance["appliance_id"] == appliance_id:
break
if appliance.get("compute_id") is None:
from .main_window import MainWindow
server = server_select(MainWindow.instance(), node_type=appliance["node_type"])
if server is None:
return False
self._controller.post("/projects/" + project.id() + "/appliances/" + appliance_id, self._createNodeFromApplianceCallback, {
"compute_id": server.id(),
"x": int(x),
"y": int(y)
})
else:
self._controller.post("/projects/" + project.id() + "/appliances/" + appliance_id, self._createNodeFromApplianceCallback, {
"x": int(x),
"y": int(y)
})
return True
def _createNodeFromApplianceCallback(self, result, error=False, **kwargs):
if error:
if "message" in result:
log.error("Error while creating node: {}".format(result["message"]))
return
def _controllerDisconnectedSlot(self):
self._appliances = []
def appliances(self):
return self._appliances
def _listAppliancesCallback(self, result, error=False, **kwargs):
if error is True:
log.error("Error while getting appliance list: {}".format(result["message"]))
return
self._appliances = result
@staticmethod
def instance():
"""
Singleton to return only on instance of ApplianceManager.
:returns: instance of ApplianceManager
"""
if not hasattr(ApplianceManager, '_instance') or ApplianceManager._instance is None:
ApplianceManager._instance = ApplianceManager()
return ApplianceManager._instance

View File

@@ -19,8 +19,12 @@
Base class for node classes.
"""
import os
import pathlib
from .qt import QtCore
from .ports.port import Port
from .utils.normalize_filename import normalize_filename
import logging
@@ -176,19 +180,16 @@ class BaseNode(QtCore.QObject):
# set ports as started
port.setStatus(Port.started)
self.started_signal.emit()
log.info("{} has started".format(self.name()))
elif status == self.stopped:
for port in self._ports:
# set ports as stopped
port.setStatus(Port.stopped)
self.stopped_signal.emit()
log.info("{} has stopped".format(self.name()))
elif status == self.suspended:
for port in self._ports:
# set ports as suspended
port.setStatus(Port.suspended)
self.suspended_signal.emit()
log.info("{} has suspended".format(self.name()))
def initialized(self):
"""
@@ -326,3 +327,73 @@ class BaseNode(QtCore.QObject):
"""
self._project.delete(path, callback, context=context, **kwargs)
def exportConfigToDirectory(self, directory):
"""
Exports the initial-config to a directory.
:param directory: destination directory path
"""
if not hasattr(self, "configFiles"):
return
for file in self.configFiles():
self.controllerHttpGet("/nodes/{node_id}/files/{file}".format(node_id=self._node_id, file=file),
self._exportConfigToDirectoryCallback,
context={"directory": directory, "file": file},
raw=True)
def _exportConfigToDirectoryCallback(self, result, error=False, raw_body=None, context={}, **kwargs):
"""
Callback for exportConfigToDirectory.
:param result: server response
:param error: indicates an error (boolean)
"""
if error:
# The file could be missing if you have not private config for
# exemple
return
export_directory = context["directory"]
filename = normalize_filename(self.name()) + "_{}".format(context["file"].replace("/", "_")) # We can have / in the case of Docker
config_path = os.path.join(export_directory, filename)
try:
with open(config_path, "wb") as f:
log.debug("saving {} config to {}".format(self.name(), config_path))
f.write(raw_body)
except OSError as e:
self.error_signal.emit(self.id(), "could not export config to {}: {}".format(config_path, e))
def importConfigFromDirectory(self, directory):
"""
Imports an initial-config from a directory.
:param directory: source directory path
"""
if not hasattr(self, "configFiles"):
return
try:
contents = os.listdir(directory)
except OSError as e:
self.warning_signal.emit(self.id(), "Can't list file in {}: {}".format(directory, str(e)))
return
for file in self.configFiles():
filename = normalize_filename(self.name()) + "_{}".format(file.replace("/", "_")) # We can have / in the case of Docker
if filename in contents:
self.controllerHttpPost("/nodes/{node_id}/files/{file}".format(
node_id=self._node_id,
file=file), self._importConfigCallback,
pathlib.Path(os.path.join(directory, filename)))
else:
self.warning_signal.emit(self.id(), "no script file could be found, expected file name: {}".format(filename))
def _importConfigCallback(self, result, error=False, **kwargs):
if error:
if "message" in result:
log.error("Error while import config: {}".format(result["message"]))
return

View File

@@ -1,26 +0,0 @@
!
service timestamps debug datetime msec
service timestamps log datetime msec
no service password-encryption
!
hostname %h
!
ip cef
no ip domain-lookup
no ip icmp rate-limit unreachable
ip tcp synwait 5
no cdp log mismatch duplex
!
line con 0
exec-timeout 0 0
logging synchronous
privilege level 15
no login
line aux 0
exec-timeout 0 0
logging synchronous
privilege level 15
no login
!
!
end

View File

@@ -1,181 +0,0 @@
!
service timestamps debug datetime msec
service timestamps log datetime msec
no service password-encryption
no service dhcp
!
hostname %h
!
ip cef
no ip routing
no ip domain-lookup
no ip icmp rate-limit unreachable
ip tcp synwait 5
no cdp log mismatch duplex
vtp file nvram:vlan.dat
!
!
interface FastEthernet0/0
description *** Unused for Layer2 EtherSwitch ***
no ip address
shutdown
!
interface FastEthernet0/1
description *** Unused for Layer2 EtherSwitch ***
no ip address
shutdown
!
interface FastEthernet1/0
no shutdown
duplex full
speed 100
!
interface FastEthernet1/1
no shutdown
duplex full
speed 100
!
interface FastEthernet1/2
no shutdown
duplex full
speed 100
!
interface FastEthernet1/3
no shutdown
duplex full
speed 100
!
interface FastEthernet1/4
no shutdown
duplex full
speed 100
!
interface FastEthernet1/5
no shutdown
duplex full
speed 100
!
interface FastEthernet1/6
no shutdown
duplex full
speed 100
!
interface FastEthernet1/7
no shutdown
duplex full
speed 100
!
interface FastEthernet1/8
no shutdown
duplex full
speed 100
!
interface FastEthernet1/9
no shutdown
duplex full
speed 100
!
interface FastEthernet1/10
no shutdown
duplex full
speed 100
!
interface FastEthernet1/11
no shutdown
duplex full
speed 100
!
interface FastEthernet1/12
no shutdown
duplex full
speed 100
!
interface FastEthernet1/13
no shutdown
duplex full
speed 100
!
interface FastEthernet1/14
no shutdown
duplex full
speed 100
!
interface FastEthernet1/15
no shutdown
duplex full
speed 100
!
interface Vlan1
no ip address
shutdown
!
!
line con 0
exec-timeout 0 0
logging synchronous
privilege level 15
no login
line aux 0
exec-timeout 0 0
logging synchronous
privilege level 15
no login
!
!
banner exec $
***************************************************************
This is a normal Router with a SW module inside (NM-16ESW)
It has been preconfigured with hard coded speed and duplex
To create vlans use the command "vlan database" from exec mode
After creating all desired vlans use "exit" to apply the config
To view existing vlans use the command "show vlan-switch brief"
Warning: You are using an old IOS image for this router.
Please update the IOS to enable the "macro" command!
***************************************************************
$
!
!Warning: If the IOS is old and doesn't support macro, it will stop the configuration loading from this point!
!
macro name add_vlan
end
vlan database
vlan $v
exit
@
macro name del_vlan
end
vlan database
no vlan $v
exit
@
!
!
banner exec $
***************************************************************
This is a normal Router with a Switch module inside (NM-16ESW)
It has been pre-configured with hard-coded speed and duplex
To create vlans use the command "vlan database" in exec mode
After creating all desired vlans use "exit" to apply the config
To view existing vlans use the command "show vlan-switch brief"
Alias(exec) : vl - "show vlan-switch brief" command
Alias(configure): va X - macro to add vlan X
Alias(configure): vd X - macro to delete vlan X
***************************************************************
$
!
alias configure va macro global trace add_vlan $v
alias configure vd macro global trace del_vlan $v
alias exec vl show vlan-switch brief
!
!
end

View File

@@ -1,132 +0,0 @@
!
service timestamps debug datetime msec
service timestamps log datetime msec
no service password-encryption
!
hostname %h
!
!
!
logging discriminator EXCESS severity drops 6 msg-body drops EXCESSCOLL
logging buffered 50000
logging console discriminator EXCESS
!
no ip icmp rate-limit unreachable
!
ip cef
no ip domain-lookup
!
!
!
!
!
!
ip tcp synwait-time 5
!
!
!
!
!
!
interface Ethernet0/0
no ip address
no shutdown
duplex auto
!
interface Ethernet0/1
no ip address
no shutdown
duplex auto
!
interface Ethernet0/2
no ip address
no shutdown
duplex auto
!
interface Ethernet0/3
no ip address
no shutdown
duplex auto
!
interface Ethernet1/0
no ip address
no shutdown
duplex auto
!
interface Ethernet1/1
no ip address
no shutdown
duplex auto
!
interface Ethernet1/2
no ip address
no shutdown
duplex auto
!
interface Ethernet1/3
no ip address
no shutdown
duplex auto
!
interface Ethernet2/0
no ip address
no shutdown
duplex auto
!
interface Ethernet2/1
no ip address
no shutdown
duplex auto
!
interface Ethernet2/2
no ip address
no shutdown
duplex auto
!
interface Ethernet2/3
no ip address
no shutdown
duplex auto
!
interface Ethernet3/0
no ip address
no shutdown
duplex auto
!
interface Ethernet3/1
no ip address
no shutdown
duplex auto
!
interface Ethernet3/2
no ip address
no shutdown
duplex auto
!
interface Ethernet3/3
no ip address
no shutdown
duplex auto
!
interface Vlan1
no ip address
shutdown
!
!
!
!
!
!
!
!
!
line con 0
exec-timeout 0 0
privilege level 15
logging synchronous
line aux 0
exec-timeout 0 0
privilege level 15
logging synchronous
!
end

View File

@@ -1,108 +0,0 @@
!
service timestamps debug datetime msec
service timestamps log datetime msec
no service password-encryption
!
hostname %h
!
!
!
no ip icmp rate-limit unreachable
!
!
!
!
ip cef
no ip domain-lookup
!
!
ip tcp synwait-time 5
!
!
!
!
interface Ethernet0/0
no ip address
shutdown
!
interface Ethernet0/1
no ip address
shutdown
!
interface Ethernet0/2
no ip address
shutdown
!
interface Ethernet0/3
no ip address
shutdown
!
interface Ethernet1/0
no ip address
shutdown
!
interface Ethernet1/1
no ip address
shutdown
!
interface Ethernet1/2
no ip address
shutdown
!
interface Ethernet1/3
no ip address
shutdown
!
interface Serial2/0
no ip address
shutdown
serial restart-delay 0
!
interface Serial2/1
no ip address
shutdown
serial restart-delay 0
!
interface Serial2/2
no ip address
shutdown
serial restart-delay 0
!
interface Serial2/3
no ip address
shutdown
serial restart-delay 0
!
interface Serial3/0
no ip address
shutdown
serial restart-delay 0
!
interface Serial3/1
no ip address
shutdown
serial restart-delay 0
!
interface Serial3/2
no ip address
shutdown
serial restart-delay 0
!
interface Serial3/3
no ip address
shutdown
serial restart-delay 0
!
!
no cdp log mismatch duplex
!
line con 0
exec-timeout 0 0
privilege level 15
logging synchronous
line aux 0
exec-timeout 0 0
privilege level 15
logging synchronous
!
end

View File

@@ -1 +0,0 @@
set pcname %h

View File

@@ -21,7 +21,6 @@ Handles commands typed in the GNS3 console.
import sys
import cmd
import logging
import struct
import sip
import json
@@ -30,6 +29,9 @@ from .node import Node
from .qt import QtCore
from .version import __version__
import logging
log = logging.getLogger(__name__)
class ConsoleCmd(cmd.Cmd):
@@ -177,6 +179,24 @@ class ConsoleCmd(cmd.Cmd):
print("Cannot console to {}".format(device))
break
def do_log(self, args):
"""
Log a message
log level message
"""
args = args.split()
if len(args) == 0:
return
level = args.pop(0)
if level == "info":
log.info(" ".join(args))
elif level == "warning":
log.warning(" ".join(args))
else:
log.error(" ".join(args))
def _start_console(self, node):
"""
Starts a console application for a specific node.

View File

@@ -197,6 +197,9 @@ class Controller(QtCore.QObject):
def getSynchronous(self, endpoint, timeout=2):
return self._http_client.getSynchronous(endpoint, timeout)
def connectWebSocket(self, path, *args):
return self._http_client.connectWebSocket(path)
@staticmethod
def instance():
"""
@@ -208,12 +211,13 @@ class Controller(QtCore.QObject):
Controller._instance = Controller()
return Controller._instance
def getStatic(self, url, callback):
def getStatic(self, url, callback, fallback=None):
"""
Get a URL from the /static on controller and cache it on disk
:param url: URL without the protocol and host part
:param callback: Callback to call when file is ready
:param fallback: Fallback url in case of error
"""
if not self._http_client:
@@ -229,16 +233,25 @@ class Controller(QtCore.QObject):
if os.path.exists(path):
callback(path)
elif path in self._static_asset_download_queue:
self._static_asset_download_queue[path].append(callback)
self._static_asset_download_queue[path].append((callback, fallback, ))
else:
self._static_asset_download_queue[path] = [callback]
self._static_asset_download_queue[path] = [(callback, fallback, )]
self._http_client.createHTTPQuery("GET", url, qpartial(self._getStaticCallback, url, path))
def _getStaticCallback(self, url, path, result, error=False, raw_body=None, **kwargs):
if path not in self._static_asset_download_queue:
return
if error:
fallback_used = False
for callback, fallback in self._static_asset_download_queue[path]:
if fallback:
self.getStatic(fallback, callback)
fallback_used = True
if fallback_used:
log.error("Error while downloading file: {}".format(url))
log.error("Error while downloading file: {}".format(url))
if path in self._static_asset_download_queue:
del self._static_asset_download_queue[path]
del self._static_asset_download_queue[path]
return
try:
with open(path, "wb+") as f:
@@ -247,18 +260,24 @@ class Controller(QtCore.QObject):
log.error("Can't write to {}: {}".format(path, str(e)))
return
log.debug("File stored {} for {}".format(path, url))
for callback in self._static_asset_download_queue[path]:
for callback, fallback in self._static_asset_download_queue[path]:
callback(path)
del self._static_asset_download_queue[path]
def getSymbolIcon(self, symbol_id, callback):
def getSymbolIcon(self, symbol_id, callback, fallback=None):
"""
Get a QIcon for a symbol from the controller
:param url: URL without the protocol and host part
:param symbol_id: Symbol id
:param callback: Callback to call when file is ready
:param fallback: Fallback symbol if not found
"""
self.getStatic(Symbol(symbol_id).url(), qpartial(self._getIconCallback, callback))
if symbol_id is None:
self.getStatic(Symbol(fallback).url(), qpartial(self._getIconCallback, callback))
else:
if fallback:
fallback = Symbol(fallback).url()
self.getStatic(Symbol(symbol_id).url(), qpartial(self._getIconCallback, callback), fallback=fallback)
def _getIconCallback(self, callback, path):
icon = QtGui.QIcon()

View File

@@ -41,7 +41,7 @@ if __version_info__[3] != 0:
import faulthandler
# Display a traceback in case of segfault crash. Usefull when frozen
# Not enabled by default for security reason
log.info("Enable catching segfault")
log.debug("Enable catching segfault")
faulthandler.enable()
@@ -51,7 +51,7 @@ class CrashReport:
Report crash to a third party service
"""
DSN = "sync+https://cf90c68e9c81469499e1417714cf2e79:a8ac6a66779a433497c8e9cb32c2a668@sentry.io/38506"
DSN = "sync+https://a6a4a48f46c84f2b9474cd48db6eeb66:f77e467cb5bf484496e4e6eb45700d13@sentry.io/38506"
if hasattr(sys, "frozen"):
cacert = get_resource("cacert.pem")
if cacert is not None and os.path.isfile(cacert):
@@ -70,11 +70,18 @@ class CrashReport:
def captureException(self, exception, value, tb):
from .local_server import LocalServer
from .local_config import LocalConfig
local_server = LocalServer.instance().localServerSettings()
if local_server["report_errors"]:
if not RAVEN_AVAILABLE:
return
if os.path.exists(LocalConfig.instance().runAsRootPath()):
log.warning("User has run application as root. Crash reports are disabled.")
sys.exit(1)
return
if os.path.exists(".git"):
log.warning("A .git directory exist crash report is turn off for developers. Instant exit")
sys.exit(1)
@@ -104,7 +111,7 @@ class CrashReport:
except Exception as e:
log.error("Can't send crash report to Sentry: {}".format(e))
return
log.info("Crash report sent with event ID: {}".format(client.get_ident(report)))
log.debug("Crash report sent with event ID: {}".format(client.get_ident(report)))
def _add_qt_information(self, context):
try:

View File

@@ -17,6 +17,7 @@
import os
import sip
import shutil
from ..qt import QtWidgets, QtCore, QtGui, qpartial, qslot
from ..ui.appliance_wizard_ui import Ui_ApplianceWizard
@@ -71,7 +72,16 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
self.uiLocalRadioButton.toggled.connect(self._localToggledSlot)
if Controller.instance().isRemote():
self.uiLocalRadioButton.setText("Run the appliance on the main server")
self.uiLocalRadioButton.setText("Install the appliance on the main server")
else:
if not path.endswith('.builtin.gns3a'):
try:
destination = Config().appliances_dir
os.makedirs(destination, exist_ok=True)
destination = os.path.join(destination, os.path.basename(path))
shutil.copy(path, destination)
except OSError as e:
QtWidgets.QMessageBox.warning(self.parent(), "Can't copy {} to {}".format(path, destination), str(e))
self.uiServerWizardPage.isComplete = self._uiServerWizardPage_isComplete
@@ -128,6 +138,9 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
self.uiInfoTreeWidget.addTopLevelItem(item)
elif self.page(page_id) == self.uiServerWizardPage:
is_mac = ComputeManager.instance().localPlatform().startswith("darwin")
is_win = ComputeManager.instance().localPlatform().startswith("win")
self.uiRemoteServersComboBox.clear()
if len(ComputeManager.instance().remoteComputes()) == 0:
self.uiRemoteRadioButton.setEnabled(False)
@@ -141,7 +154,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
if ComputeManager.instance().localPlatform() is None:
self.uiLocalRadioButton.setEnabled(False)
elif (ComputeManager.instance().localPlatform().startswith("darwin") or ComputeManager.instance().localPlatform().startswith("win")):
elif is_mac or is_win:
if type == "qemu":
# Qemu has issues on OSX and Windows we disallow usage of the local server
if not LocalConfig.instance().experimental():
@@ -158,6 +171,14 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
else:
self.uiRemoteRadioButton.setChecked(False)
if is_mac or is_win:
if not self.uiRemoteRadioButton.isEnabled() \
and not self.uiVMRadioButton.isEnabled() \
and not self.uiLocalRadioButton.isEnabled():
QtWidgets.QMessageBox.warning(
self, "No GNS3 VM available.",
"GNS3 VM is not available, please configure GNS3 VM before adding new Appliance.")
elif self.page(page_id) == self.uiFilesWizardPage:
self._registry.getRemoteImageList(self._appliance.emulator(), self._compute_id)

View File

@@ -23,6 +23,7 @@ from gns3.local_config import LocalConfig
from gns3.ui.console_command_dialog_ui import Ui_uiConsoleCommandDialog
from gns3.settings import PRECONFIGURED_TELNET_CONSOLE_COMMANDS, \
PRECONFIGURED_VNC_CONSOLE_COMMANDS, \
PRECONFIGURED_SPICE_CONSOLE_COMMANDS, \
CUSTOM_CONSOLE_COMMANDS_SETTINGS
@@ -38,7 +39,7 @@ class ConsoleCommandDialog(QtWidgets.QDialog, Ui_uiConsoleCommandDialog):
def __init__(self, parent, console_type="telnet", current=None):
"""
:params console_type: telnet, serial or vnc
:params console_type: telnet, serial, vnc or spice
:params current: Current console command
"""
super().__init__(parent)
@@ -62,6 +63,9 @@ class ConsoleCommandDialog(QtWidgets.QDialog, Ui_uiConsoleCommandDialog):
elif self._console_type == "vnc":
self._consoles = copy.copy(PRECONFIGURED_VNC_CONSOLE_COMMANDS)
self._consoles.update(self._settings[self._console_type])
elif self._console_type == "spice":
self._consoles = copy.copy(PRECONFIGURED_SPICE_CONSOLE_COMMANDS)
self._consoles.update(self._settings[self._console_type])
self.uiCommandComboBox.clear()
self.uiCommandComboBox.addItem("Custom", "")

View File

@@ -136,22 +136,15 @@ class DoctorDialog(QtWidgets.QDialog, Ui_DoctorDialog):
if not os.path.exists(path):
return (2, "Ubridge path {path} doesn't exists".format(path=path))
request_setuid = False
if sys.platform.startswith("linux"):
try:
if "security.capability" in os.listxattr(path):
caps = os.getxattr(path, "security.capability")
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
if not struct.unpack("<IIIII", caps)[1] & 1 << 13:
return (2, "Ubridge requires CAP_NET_RAW. Run sudo setcap cap_net_admin,cap_net_raw=ep {path}".format(path=path))
else:
# capabilities not supported
request_setuid = True
except AttributeError:
if "security.capability" not in os.listxattr(path) or not struct.unpack("<IIIII", os.getxattr(path, "security.capability"))[1] & 1 << 13:
return (2, "Ubridge requires CAP_NET_RAW. Run sudo setcap cap_net_admin,cap_net_raw=ep {path}".format(path=path))
except (OSError, AttributeError) as e:
# Due to a Python bug, os.listxattr could be missing: https://github.com/GNS3/gns3-gui/issues/2010
return (1, "Could not determine if CAP_NET_RAW capability is set for uBridge (Python bug)".format(path=path))
return (1, "Could not determine if CAP_NET_RAW capability is set for uBridge: {}".format(e))
if sys.platform.startswith("darwin") or request_setuid:
if sys.platform.startswith("darwin"):
if os.stat(path).st_uid != 0 or not os.stat(path).st_mode & stat.S_ISUID:
return (2, "Ubridge should be setuid. Run sudo chown root:admin {path} and sudo chmod 4750 {path}".format(path=path))
return (0, None)

View File

@@ -41,9 +41,6 @@ class EditComputeDialog(QtWidgets.QDialog, Ui_EditComputeDialog):
self.uiServerHostLineEdit.setText(self._compute.host())
self.uiServerPortSpinBox.setValue(self._compute.port())
index = self.uiServerProtocolComboBox.findText(self._compute.protocol().upper())
self.uiServerProtocolComboBox.setCurrentIndex(index)
if self._compute.user():
self.uiEnableAuthenticationCheckBox.setChecked(True)
self.uiServerUserLineEdit.setText(self._compute.user())
@@ -81,7 +78,7 @@ class EditComputeDialog(QtWidgets.QDialog, Ui_EditComputeDialog):
host = self.uiServerHostLineEdit.text().strip()
name = self.uiServerNameLineEdit.text().strip()
protocol = self.uiServerProtocolComboBox.currentText().lower()
protocol = "http"
port = self.uiServerPortSpinBox.value()
user = self.uiServerUserLineEdit.text().strip()
password = self.uiServerPasswordLineEdit.text().strip()

View File

@@ -62,7 +62,7 @@ class ExportDebugDialog(QtWidgets.QDialog, Ui_ExportDebugDialog):
self._exportDebugCallback({}, error=True)
def _exportDebugCallback(self, result, error=False, **kwargs):
log.info("Export debug information to %s", self._path)
log.debug("Export debug information to %s", self._path)
try:
with ZipFile(self._path, 'w') as zip:

View File

@@ -0,0 +1,174 @@
# -*- 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 ..qt import QtGui, QtWidgets, qslot
from ..ui.filter_dialog_ui import Ui_FilterDialog
class FilterDialog(QtWidgets.QDialog, Ui_FilterDialog):
"""
Filter dialog.
"""
def __init__(self, parent, link):
super().__init__(parent)
self.setupUi(self)
self._link = link
self._link.updated_link_signal.connect(self._updateUiSlot)
self._link.listAvailableFilters(self._listAvailableFiltersCallback)
self._initialized = False
self._filter_items = {}
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).clicked.connect(self._applyPreferencesSlot)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).clicked.connect(self._helpSlot)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Reset).clicked.connect(self._resetSlot)
def _listAvailableFiltersCallback(self, result, error=False, *args, **kwargs):
if error:
QtWidgets.QMessageBox.warning(None, "Link", "Error while listing information about the link: {}".format(result["message"]))
return
self._filters = result
self._initialized = True
self._updateUiSlot()
@qslot
def _updateUiSlot(self, *args):
# Empty the main layout
while True:
item = self.uiVerticalLayout.takeAt(0)
if item is None:
break
elif item.widget():
item.widget().deleteLater()
if len(self._filters) == 0:
QtWidgets.QMessageBox.critical(self, "Link", "No filter available for this link. Try with a different node type.")
self.reject()
self._tabWidget = QtWidgets.QTabWidget(self)
for i, filter in enumerate(self._filters):
tab = QtWidgets.QWidget()
self._tabWidget.addTab(tab, filter['name'])
self._tabWidget.setTabToolTip(i, filter['description'])
self._tabWidget.setTabIcon(i, QtGui.QIcon(':/icons/led_red.svg'))
vlayout = QtWidgets.QVBoxLayout()
gridLayout = QtWidgets.QGridLayout()
line = 0
filter["spinBoxes"] = []
filter["textEdits"] = []
nb_spin = 0
for param in filter["parameters"]:
label = QtWidgets.QLabel()
label.setText(param["name"] + ":")
gridLayout.addWidget(label, line, 0, 1, 1)
if param["type"] == "int":
spinBox = QtWidgets.QSpinBox()
filter["spinBoxes"].append(spinBox)
spinBox.setMinimum(param["minimum"])
spinBox.setMaximum(param["maximum"])
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(spinBox.sizePolicy().hasHeightForWidth())
spinBox.setSizePolicy(sizePolicy)
try:
value = self._link.filters()[filter["type"]][nb_spin]
spinBox.setValue(value)
if value != 0:
self._tabWidget.setTabIcon(i, QtGui.QIcon(':/icons/led_green.svg'))
except(KeyError, IndexError):
pass
nb_spin += 1
gridLayout.addWidget(spinBox, line, 1, 1, 1)
unit = QtWidgets.QLabel()
unit.setText(param["unit"])
gridLayout.addWidget(unit, line, 2, 1, 1)
elif param["type"] == "text":
textEdit = QtWidgets.QTextEdit()
textEdit.setAcceptRichText(False)
filter["textEdits"].append(textEdit)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
textEdit.setMinimumWidth(300)
textEdit.setSizePolicy(sizePolicy)
try:
text = self._link.filters()[filter["type"]][0]
textEdit.setPlainText(text)
if text:
self._tabWidget.setTabIcon(i, QtGui.QIcon(':/icons/led_green.svg'))
except(KeyError, IndexError):
pass
gridLayout.addWidget(textEdit, line, 1, 1, 1)
line += 1
spacerItem = QtWidgets.QSpacerItem(1, 1, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
gridLayout.addItem(spacerItem, line, 0, 1, 1)
vlayout.addLayout(gridLayout)
tab.setLayout(vlayout)
self.uiVerticalLayout.addWidget(self._tabWidget)
@qslot
def _applyPreferencesSlot(self, *args):
new_filters = {}
for filter in self._filters:
new_filters[filter["type"]] = []
for spinBox in filter["spinBoxes"]:
new_filters[filter["type"]].append(spinBox.value())
for spinBox in filter["textEdits"]:
new_filters[filter["type"]].append(spinBox.toPlainText())
self._link.setFilters(new_filters)
self._link.update()
@qslot
def _helpSlot(self, *args):
help_text = "Filters are applied to packets in both direction.\n\n"
filter_nb = 0
for filter in self._filters:
help_text += "{}: {}".format(filter["name"], filter["description"])
filter_nb += 1
if len(self._filters) != filter_nb:
help_text += "\n\n"
QtWidgets.QMessageBox.information(self, "Help for filters", help_text)
@qslot
def _resetSlot(self, *args):
filters = {}
self._link.setFilters(filters)
self._link.update()
def done(self, result):
"""
Called when the dialog is closed.
:param result: boolean (accepted or rejected)
"""
if result and self._initialized:
self._applyPreferencesSlot()
super().done(result)

View File

@@ -0,0 +1,191 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2016 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/>.
"""
Display error to the user in an overlay popup
"""
import os
import time
from gns3.qt import QtWidgets, QtCore, qslot
import logging
log = logging.getLogger(__name__)
MAX_ELEMENTS = 3
DISPLAY_DURATION = {
"CRITICAL": 120,
"ERROR": 120,
"WARNING": 20,
"INFO": 5
}
class NotifDialogHandler(logging.StreamHandler):
def __init__(self, dialog):
super().__init__()
self._dialog = dialog
self.setLevel(logging.INFO)
self._dialog.show()
def emit(self, record):
self._dialog.addNotif(record.levelname, record.getMessage())
class NotifDialog(QtWidgets.QWidget):
def __init__(self, parent):
super().__init__(parent)
self._notifs = []
self.setWindowFlags(QtCore.Qt.FramelessWindowHint |
QtCore.Qt.WindowDoesNotAcceptFocus |
QtCore.Qt.SubWindow)
# QtCore.Qt.Tool)
# QtCore.Qt.WindowStaysOnTopHint)
self.setAttribute(QtCore.Qt.WA_ShowWithoutActivating) # | QtCore.Qt.WA_TranslucentBackground)
self._layout = QtWidgets.QVBoxLayout()
self._timer = QtCore.QTimer()
self._timer.setInterval(1000)
self._timer.timeout.connect(self._refreshSlot)
self._timer.start()
for i in range(0, MAX_ELEMENTS):
l = QtWidgets.QLabel()
l.setAlignment(QtCore.Qt.AlignTop)
l.setWordWrap(True)
l.hide()
self._layout.addWidget(l)
self.setLayout(self._layout)
@qslot
def addNotif(self, level, message):
if not self.parent().settings()["overlay_notifications"]:
return
# This unicode char prevent the wordwrap at /
message = message.replace("/", "\u2060/\u2060")
if len(self._notifs) == MAX_ELEMENTS:
self._notifs.pop(0)
self._notifs.append((level, message, time.time()))
self.update()
@qslot
def _refreshSlot(self):
"""
Hide the notifs after some delay
"""
notifs = []
for (i, (level, message, when)) in enumerate(self._notifs):
if when + DISPLAY_DURATION[level] > time.time():
notifs.append((level, message, when))
if notifs != self._notifs:
self._notifs = notifs
self.update()
elif len(notifs) > 0:
self.resize()
def update(self):
if len(self._notifs) == 0:
self.hide()
else:
for (i, (level, message, when)) in enumerate(self._notifs):
w = self._layout.itemAt(i).widget()
w.setText(message)
if level == "ERROR" or level == "CRITICAL":
w.setStyleSheet("""
color: black;
padding-left: 12px;
background-color: rgb(247, 205, 198);
border-left: 10px solid red;
""")
elif level == "WARNING":
w.setStyleSheet("""
color: black;
padding-left: 12px;
background-color: #f4f2b5;
border-left: 10px solid orange;
""")
elif level == "INFO":
w.setStyleSheet("""
color: black;
padding-left: 12px;
background-color: #cfffc9;
border-left: 10px solid green;
""")
w.show()
for i in range(i + 1, MAX_ELEMENTS):
w = self._layout.itemAt(i).widget()
w.hide()
self.resize()
self.show()
def resize(self):
x = self.parent().width() - self.width() - 10
y = 10
self.setGeometry(x, y, self.sizeHint().width(), self.sizeHint().height())
@qslot
def mousePressEvent(self, event):
self._notifs.clear()
self.update()
if __name__ == '__main__':
"""
A demo main for testing the features
"""
import sys
app = QtWidgets.QApplication(sys.argv)
logging.basicConfig(level=logging.INFO)
class MainWindow(QtWidgets.QWidget):
def __init__(self):
super().__init__()
l1 = QtWidgets.QLabel()
l1.setText("Hello World")
vbox = QtWidgets.QVBoxLayout()
vbox.addWidget(l1)
self.setLayout(vbox)
self.setStyleSheet("background-color:blue;")
self._dialog = NotifDialog(self)
log.addHandler(NotifDialogHandler(self._dialog))
log.info("test")
def moveEvent(self, event):
log.error("An error")
log.info("An info with an url http://test")
log.warning("A warning with a long long long longlong longlong longlong longlong longlong longlong longlong long message")
self._dialog.update()
def resizeEvent(self, event):
self._dialog.update()
main = MainWindow()
main.setMinimumWidth(600)
main.setMinimumHeight(600)
main.show()
exit_code = app.exec_()

View File

@@ -144,7 +144,7 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
def _duplicateCallback(self, result, error=False, **kwargs):
if error:
log.error("Error while duplicate project: {}".format(result["message"]))
log.error("Error while duplicating project: {}".format(result["message"]))
return
Controller.instance().refreshProjectList()

View File

@@ -216,7 +216,6 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
self.uiRemoteMainServerUserLineEdit.setText(local_server_settings["user"])
self.uiRemoteMainServerPasswordLineEdit.setText(local_server_settings["password"])
self.uiRemoteMainServerPortSpinBox.setValue(local_server_settings["port"])
self.uiRemoteMainServerProtocolComboBox.setCurrentText(local_server_settings["protocol"])
elif self.page(page_id) == self.uiLocalServerStatusWizardPage:
self._refreshLocalServerStatusSlot()
@@ -331,7 +330,7 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
local_server_settings["auto_start"] = False
local_server_settings["host"] = self.uiRemoteMainServerHostLineEdit.text()
local_server_settings["port"] = self.uiRemoteMainServerPortSpinBox.value()
local_server_settings["protocol"] = self.uiRemoteMainServerProtocolComboBox.currentText()
local_server_settings["protocol"] = "http"
local_server_settings["user"] = self.uiRemoteMainServerUserLineEdit.text()
local_server_settings["password"] = self.uiRemoteMainServerPasswordLineEdit.text()
local_server_settings["auth"] = self.uiRemoteMainServerAuthCheckBox.isChecked()

View File

@@ -52,12 +52,18 @@ class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
# use the first item in the list as the model
first_item = items[0]
pen = first_item.pen()
brush = first_item.brush()
self._color = brush.color()
self.uiColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(self._color.red(),
self._color.green(),
self._color.blue(),
self._color.alpha()))
if hasattr(first_item, "brush"): # Line don't have brush
brush = first_item.brush()
self._color = brush.color()
self.uiColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(self._color.red(),
self._color.green(),
self._color.blue(),
self._color.alpha()))
else:
self.uiColorLabel.hide()
self.uiColorPushButton.hide()
self._color = None
self._border_color = pen.color()
self.uiBorderColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(self._border_color.red(),
self._border_color.green(),
@@ -102,11 +108,15 @@ class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
border_style = QtCore.Qt.PenStyle(self.uiBorderStyleComboBox.itemData(self.uiBorderStyleComboBox.currentIndex()))
pen = QtGui.QPen(self._border_color, self.uiBorderWidthSpinBox.value(), border_style, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
brush = QtGui.QBrush(self._color)
if self._color:
brush = QtGui.QBrush(self._color)
else:
brush = None
for item in self._items:
item.setPen(pen)
item.setBrush(brush)
if brush:
item.setBrush(brush)
item.setRotation(self.uiRotationSpinBox.value())
def done(self, result):

View File

@@ -22,7 +22,7 @@ Graphical view on the scene where items are drawn.
import logging
import os
import sip
import pickle
import sys
from .qt import QtCore, QtGui, QtNetwork, QtWidgets, qpartial, qslot
from .items.node_item import NodeItem
@@ -33,6 +33,7 @@ from .modules import MODULES
from .modules.module_error import ModuleError
from .settings import GRAPHICS_VIEW_SETTINGS
from .topology import Topology
from .appliance_manager import ApplianceManager
from .dialogs.style_editor_dialog import StyleEditorDialog
from .dialogs.text_editor_dialog import TextEditorDialog
from .dialogs.symbol_selection_dialog import SymbolSelectionDialog
@@ -55,6 +56,7 @@ from .items.text_item import TextItem
from .items.shape_item import ShapeItem
from .items.drawing_item import DrawingItem
from .items.rectangle_item import RectangleItem
from .items.line_item import LineItem
from .items.ellipse_item import EllipseItem
from .items.image_item import ImageItem
@@ -82,6 +84,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
self._adding_note = False
self._adding_rectangle = False
self._adding_ellipse = False
self._adding_line = False
self._newlink = None
self._dragging = False
self._last_mouse_position = None
@@ -113,6 +116,16 @@ class GraphicsView(QtWidgets.QGraphicsView):
def setSceneSize(self, width, height):
self.scene().setSceneRect(-(width / 2), -(height / 2), width, height)
def setZoom(self, zoom):
"""
Sets zoom of the Graphics View
:param zoom:
:return:
"""
if zoom:
factor = zoom / 100.
self.scale(factor, factor)
def setEnabled(self, enabled):
if enabled is False:
@@ -233,6 +246,20 @@ class GraphicsView(QtWidgets.QGraphicsView):
self._adding_ellipse = False
self.setCursor(QtCore.Qt.ArrowCursor)
def addLine(self, state):
"""
Adds a line.
:param state: boolean
"""
if state:
self._adding_line = True
self.setCursor(QtCore.Qt.PointingHandCursor)
else:
self._adding_line = False
self.setCursor(QtCore.Qt.ArrowCursor)
def addImage(self, image_path):
"""
Adds an image.
@@ -437,6 +464,12 @@ class GraphicsView(QtWidgets.QGraphicsView):
self._main_window.uiDrawEllipseAction.setChecked(False)
self.setCursor(QtCore.Qt.ArrowCursor)
self._adding_ellipse = False
elif event.button() == QtCore.Qt.LeftButton and self._adding_line:
pos = self.mapToScene(event.pos())
self.createDrawingItem("line", pos.x(), pos.y(), 0)
self._main_window.uiDrawLineAction.setChecked(False)
self.setCursor(QtCore.Qt.ArrowCursor)
self._adding_line = False
else:
super().mousePressEvent(event)
@@ -474,6 +507,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
if delta is not None and delta.x() == 0:
# CTRL is pressed then use the mouse wheel to zoom in or out.
self.scaleView(pow(2.0, delta.y() / 240.0))
self._topology.project().setZoom(round(self.transform().m11() * 100))
self._topology.project().update()
else:
super().wheelEvent(event)
@@ -486,6 +521,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
if factor < 0.10 or factor > 10:
return
self.scale(scale_factor, scale_factor)
self._main_window.uiStatusBar.showMessage("Zoom: {}%".format(round(self.transform().m11() * 100)), 2000)
def keyPressEvent(self, event):
"""
@@ -554,6 +590,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
self.configureSlot()
return
else:
if sys.platform.startswith("win") and item.node().bringToFront():
return
self.consoleFromItems(self.scene().selectedItems())
return
elif isinstance(item, NoteItem) and isinstance(item.parentItem(), NodeItem):
@@ -587,7 +625,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
"""
# check if what is dragged is handled by this view
if event.mimeData().hasFormat("application/x-gns3-node") or event.mimeData().hasFormat("text/uri-list"):
if event.mimeData().hasFormat("text/uri-list") \
or event.mimeData().hasFormat("application/x-gns3-appliance"):
event.acceptProposedAction()
event.accept()
else:
@@ -601,10 +640,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
"""
# check if what has been dropped is handled by this view
if event.mimeData().hasFormat("application/x-gns3-node"):
data = event.mimeData().data("application/x-gns3-node")
# load the pickled node data
node_data = pickle.loads(data)
if event.mimeData().hasFormat("application/x-gns3-appliance"):
appliance_id = event.mimeData().data("application/x-gns3-appliance").data().decode()
event.setDropAction(QtCore.Qt.CopyAction)
event.accept()
if event.keyboardModifiers() == QtCore.Qt.ShiftModifier:
@@ -615,12 +652,12 @@ class GraphicsView(QtWidgets.QGraphicsView):
for node_number in range(integer):
x = event.pos().x() - (150 / 2) + (node_number % max_nodes_per_line) * offset
y = event.pos().y() - (70 / 2) + (node_number // max_nodes_per_line) * offset
node_item = self.createNode(node_data, QtCore.QPoint(x, y))
if node_item is None:
# stop if there is any error
if self.createNodeFromApplianceId(appliance_id, QtCore.QPoint(x, y)) is False:
event.ignore()
break
else:
self.createNode(node_data, event.pos())
if self.createNodeFromApplianceId(appliance_id, event.pos()) is False:
event.ignore()
elif event.mimeData().hasFormat("text/uri-list") and event.mimeData().hasUrls():
# This should not arrive but we received bug report with it...
if len(event.mimeData().urls()) == 0:
@@ -678,6 +715,12 @@ class GraphicsView(QtWidgets.QGraphicsView):
change_symbol_action.triggered.connect(self.changeSymbolActionSlot)
menu.addAction(change_symbol_action)
if True in list(map(lambda item: isinstance(item, DrawingItem) or isinstance(item, NodeItem), items)):
duplicate_action = QtWidgets.QAction("Duplicate", menu)
duplicate_action.setIcon(QtGui.QIcon(':/icons/new.svg'))
duplicate_action.triggered.connect(self.duplicateActionSlot)
menu.addAction(duplicate_action)
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "nodeDir"), items)):
# Action: Show in file manager
show_in_file_manager_action = QtWidgets.QAction("Show in file manager", menu)
@@ -703,6 +746,13 @@ class GraphicsView(QtWidgets.QGraphicsView):
aux_console_action.triggered.connect(self.auxConsoleActionSlot)
menu.addAction(aux_console_action)
if sys.platform.startswith("win") and True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "bringToFront"), items)):
# Action: bring console or window to front (Windows only)
bring_to_front_action = QtWidgets.QAction("Bring to front", menu)
bring_to_front_action.setIcon(QtGui.QIcon(':/icons/front.svg'))
bring_to_front_action.triggered.connect(self.bringToFrontSlot)
menu.addAction(bring_to_front_action)
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "configFiles"), items)):
import_config_action = QtWidgets.QAction("Import config", menu)
import_config_action.setIcon(QtGui.QIcon(':/icons/import_config.svg'))
@@ -757,12 +807,6 @@ class GraphicsView(QtWidgets.QGraphicsView):
reload_action.triggered.connect(self.reloadActionSlot)
menu.addAction(reload_action)
if True in list(map(lambda item: isinstance(item, DrawingItem), items)):
duplicate_action = QtWidgets.QAction("Duplicate", menu)
duplicate_action.setIcon(QtGui.QIcon(':/icons/new.svg'))
duplicate_action.triggered.connect(self.duplicateActionSlot)
menu.addAction(duplicate_action)
if True in list(map(lambda item: isinstance(item, NoteItem), items)):
text_edit_action = QtWidgets.QAction("Text edit", menu)
text_edit_action.setIcon(QtGui.QIcon(':/icons/show-hostname.svg'))
@@ -775,7 +819,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
text_edit_action.triggered.connect(self.textEditActionSlot)
menu.addAction(text_edit_action)
if True in list(map(lambda item: isinstance(item, ShapeItem), items)):
if True in list(map(lambda item: isinstance(item, ShapeItem) or isinstance(item, LineItem), items)):
style_action = QtWidgets.QAction("Style", menu)
style_action.setIcon(QtGui.QIcon(':/icons/drawing.svg'))
style_action.triggered.connect(self.styleActionSlot)
@@ -952,6 +996,11 @@ class GraphicsView(QtWidgets.QGraphicsView):
# returns True to ignore this node.
return True
# TightVNC has lack support of IPv6 host at this moment
if "vncviewer" in node.consoleCommand() and ":" in node.consoleHost():
QtWidgets.QMessageBox.warning(
self, "TightVNC", "TightVNC (vncviewer) may not start because of lack of IPv6 support.")
try:
node.openConsole(aux=aux)
except (OSError, ValueError) as e:
@@ -1006,7 +1055,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
console_type = "telnet"
for item in self.scene().selectedItems():
if isinstance(item, NodeItem) and hasattr(item.node(), "console") and item.node().initialized() and item.node().status() == Node.started:
if item.node().consoleType() not in ("telnet", "serial", "vnc"):
if item.node().consoleType() not in ("telnet", "serial", "vnc", "spice"):
continue
current_cmd = item.node().consoleCommand()
console_type = item.node().consoleType()
@@ -1016,7 +1065,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
for item in self.scene().selectedItems():
if isinstance(item, NodeItem) and hasattr(item.node(), "console") and item.node().initialized() and item.node().status() == Node.started:
node = item.node()
if node.consoleType() not in ("telnet", "serial", "vnc"):
if node.consoleType() not in ("telnet", "serial", "vnc", "spice"):
continue
try:
node.openConsole(command=cmd)
@@ -1171,6 +1220,16 @@ class GraphicsView(QtWidgets.QGraphicsView):
dialog.show()
dialog.exec_()
def bringToFrontSlot(self):
"""
Slot to receive events from the bring to front action in the
contextual menu.
"""
for item in self.scene().selectedItems():
if isinstance(item, NodeItem) and hasattr(item.node(), "bringToFront"):
item.node().bringToFront()
def idlepcActionSlot(self):
"""
Slot to receive events from the idlepc action in the
@@ -1195,7 +1254,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
QtWidgets.QMessageBox.critical(self, "Idle-PC", "Error: {}".format(result["message"]))
else:
router = context["router"]
log.info("{} has received Idle-PC proposals".format(router.name()))
log.debug("{} has received Idle-PC proposals".format(router.name()))
idlepcs = result
if idlepcs and idlepcs[0] != "0x0":
dialog = IdlePCDialog(router, idlepcs, parent=self)
@@ -1229,7 +1288,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
else:
router = context["router"]
idlepc = result["idlepc"]
log.info("{} has received the auto idle-pc value: {}".format(router.name(), idlepc))
log.debug("{} has received the auto idle-pc value: {}".format(router.name(), idlepc))
router.setIdlepc(idlepc)
# apply Idle-PC to all routers with the same IOS image
ios_image = os.path.basename(router.settings()["image"])
@@ -1259,6 +1318,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
else:
type = "image"
self.createDrawingItem(type, item.pos().x() + 20, item.pos().y() + 20, item.zValue(), rotation=item.rotation(), svg=item.toSvg())
elif isinstance(item, NodeItem):
item.node().duplicate(item.pos().x() + 20, item.pos().y() + 20, item.zValue())
def styleActionSlot(self):
"""
@@ -1268,7 +1329,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
items = []
for item in self.scene().selectedItems():
if isinstance(item, ShapeItem):
if isinstance(item, ShapeItem) or isinstance(item, LineItem):
items.append(item)
if items:
style_dialog = StyleEditorDialog(self._main_window, items)
@@ -1413,48 +1474,18 @@ class GraphicsView(QtWidgets.QGraphicsView):
raise ModuleError("Please select a server")
return server
def createNode(self, node_data, pos):
def createNodeFromApplianceId(self, appliance_id, pos):
"""
Creates a new node on the scene.
:param node_data: node data to create a new node
:param pos: position of the drop event
:returns: NodeItem instance
Ask the server to create a node using this appliance
"""
try:
node_module = None
for module in MODULES:
instance = module.instance()
node_class = module.getNodeClass(node_data["class"])
if node_class in instance.classes():
node_module = instance
break
if not node_module:
raise ModuleError("Could not find any module for {}".format(node_class))
if self._topology.project() is None:
return
node = node_module.instantiateNode(node_class, self.allocateCompute(node_data, instance), self._topology.project())
# If no server is available a ValueError is raised
except (ModuleError, ValueError) as e:
QtWidgets.QMessageBox.critical(self, "Node creation", "{}".format(e))
return
pos = self.mapToScene(pos)
node_item = self.createNodeItem(node, node_data["symbol"], pos.x(), pos.y())
node.setGraphics(node_item)
try:
node_module.createNode(node, node_data["name"])
except ModuleError as e:
QtWidgets.QMessageBox.critical(self, "Node creation", "{}".format(e))
return
return node_item
return ApplianceManager().instance().createNodeFromApplianceId(self._topology.project(), appliance_id, pos.x(), pos.y())
def createNodeItem(self, node, symbol, x, y):
node.setSymbol(symbol)
node.setPos(x, y)
node_item = NodeItem(node)
self.scene().addItem(node_item)
self._topology.addNode(node)
@@ -1482,6 +1513,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
item = EllipseItem(pos=QtCore.QPoint(x, y), z=z, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
elif type == "rect":
item = RectangleItem(pos=QtCore.QPoint(x, y), z=z, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
elif type == "line":
item = LineItem(pos=QtCore.QPoint(x, y), dst=QtCore.QPoint(200, 0), z=z, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
elif type == "image":
item = ImageItem(pos=QtCore.QPoint(x, y), z=z, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
elif type == "text":

View File

@@ -27,7 +27,7 @@ import ipaddress
import urllib.request
from .version import __version__, __version_info__
from .qt import QtCore, QtNetwork, qpartial, sip_is_deleted
from .qt import QtCore, QtNetwork, qpartial, sip_is_deleted, QtWebSockets
from .utils import parse_version
import logging
@@ -95,6 +95,8 @@ class HTTPClient(QtCore.QObject):
# List of query waiting for the connection
self._query_waiting_connections = []
self._websocket = QtWebSockets.QWebSocket()
def setMaxTimeDifferenceBetweenQueries(self, value):
self._max_time_difference_between_queries = value
@@ -309,7 +311,7 @@ class HTTPClient(QtCore.QObject):
self._query_waiting_connections.append((request, callback))
# If we are not connected and we enqueue the first query we open the conection
if len(self._query_waiting_connections) == 1:
log.info("Connection to {}".format(self.url()))
log.debug("Connection to {}".format(self.url()))
self._executeHTTPQuery("GET", "/version", self._callbackConnect, {}, server=server, timeout=5, showProgress=False)
def _connectionError(self, callback, msg="", server=None):
@@ -441,6 +443,30 @@ class HTTPClient(QtCore.QObject):
request.setRawHeader(b"Authorization", auth_string.encode())
return request
def connectWebSocket(self, path, prefix="/v2"):
"""
Path of the websocket endpoint
"""
host = self._getHostForQuery()
request = self._websocket.request()
request.setUrl(QtCore.QUrl("ws://{host}:{port}{prefix}{path}".format(host=host, port=self._port, path=path, prefix=prefix)))
self._addAuth(request)
self._websocket.open(request)
return self._websocket
def _getHostForQuery(self):
"""
Get hostname that could be use by Qt
"""
try:
ip = self._host.rsplit('%', 1)[0]
ipaddress.IPv6Address(ip) # remove any scope ID
# this is an IPv6 address, we must surround it with brackets to be used with QUrl.
host = "[{}]".format(ip)
except ipaddress.AddressValueError:
host = self._host
return host
def _executeHTTPQuery(self, method, path, callback, body, context={}, downloadProgressCallback=None, showProgress=True, ignoreErrors=False, progressText=None, server=None, timeout=120, prefix="/v2", params={}, networkManager=None, **kwargs):
"""
Call the remote server
@@ -461,14 +487,7 @@ class HTTPClient(QtCore.QObject):
:returns: QNetworkReply
"""
try:
ip = self._host.rsplit('%', 1)[0]
ipaddress.IPv6Address(ip) # remove any scope ID
# this is an IPv6 address, we must surround it with brackets to be used with QUrl.
host = "[{}]".format(ip)
except ipaddress.AddressValueError:
host = self._host
host = self._getHostForQuery()
if params == {}:
query_string = ""
else:
@@ -607,7 +626,7 @@ class HTTPClient(QtCore.QObject):
if not body or content_type != "application/json":
callback({"message": error_message}, error=True, server=server, context=context)
else:
log.debug(body)
# log.debug(body)
try:
callback(json.loads(body), error=True, server=server, context=context)
except ValueError:
@@ -637,7 +656,7 @@ class HTTPClient(QtCore.QObject):
except UnicodeDecodeError:
body = None
content_type = response.header(QtNetwork.QNetworkRequest.ContentTypeHeader)
log.debug(body)
# log.debug(body)
if body and len(body.strip(" \n\t")) > 0 and content_type == "application/json":
try:
params = json.loads(body)

View File

@@ -15,7 +15,8 @@
# 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 ..qt import QtCore, QtWidgets, qslot
from ..qt import QtCore, QtWidgets, qslot, QtGui
from .utils import colorFromSvg
import uuid
import logging
@@ -24,6 +25,15 @@ log = logging.getLogger(__name__)
class DrawingItem:
# Map QT stroke to SVG style
QT_DASH_TO_SVG = {
QtCore.Qt.SolidLine: "",
QtCore.Qt.NoPen: None,
QtCore.Qt.DashLine: "25, 25",
QtCore.Qt.DotLine: "5, 25",
QtCore.Qt.DashDotLine: "5, 25, 25",
QtCore.Qt.DashDotDotLine: "25, 25, 5, 25, 5"
}
show_layer = False
@@ -215,3 +225,48 @@ class DrawingItem:
painter.setPen(QtCore.Qt.black)
zval = str(int(self.zValue()))
painter.drawText(QtCore.QPointF(center.x() - 4, center.y() + 4), zval)
def _styleSvg(self, element):
"""
Add style from the shape item to the SVG element that we will
export
"""
style = ""
pen = self.pen()
if hasattr(self, "brush"): # Line don't have a brush
element.set("fill", "#{}".format(hex(self.brush().color().rgba())[4:]))
element.set("fill-opacity", str(self.brush().color().alphaF()))
dasharray = self.QT_DASH_TO_SVG[pen.style()]
if dasharray is None: # No border to the element
return element
elif dasharray == "":
pass # Solid line
else:
element.set("stroke-dasharray", dasharray)
element.set("stroke-width", str(pen.width()))
element.set("stroke", "#" + hex(pen.color().rgba())[4:])
return element
def _penFromSVGElement(self, svg):
"""
Get a pen from a SVG element
:param svg:
"""
pen = QtGui.QPen()
if svg.get("stroke-width"):
pen.setWidth(int(svg.get("stroke-width")))
if svg.get("stroke"):
pen.setColor(colorFromSvg(svg.get("stroke")))
# Map SVG stroke style (border of the element to the Qt version)
if not svg.get("stroke"):
pen.setStyle(QtCore.Qt.NoPen)
else:
pen.setStyle(QtCore.Qt.SolidLine)
stroke = svg.get("stroke-dasharray")
if stroke:
for (qt_stroke, svg_stroke) in self.QT_DASH_TO_SVG.items():
if svg_stroke == stroke:
pen.setStyle(qt_stroke)
return pen

View File

@@ -112,14 +112,14 @@ class EthernetLinkItem(LinkItem):
if self.length < 100:
return
if self._source_port.status() == Port.started:
if self._link.suspended() or self._source_port.status() == Port.suspended:
# link or port is suspended
color = QtCore.Qt.yellow
shape = QtCore.Qt.RoundCap
elif self._source_port.status() == Port.started:
# port is active
color = QtCore.Qt.green
shape = QtCore.Qt.RoundCap
elif self._source_port.status() == Port.suspended:
# port is suspended
color = QtCore.Qt.yellow
shape = QtCore.Qt.RoundCap
else:
color = QtCore.Qt.red
shape = QtCore.Qt.SquareCap
@@ -153,14 +153,14 @@ class EthernetLinkItem(LinkItem):
painter.drawPoint(point1)
if self._destination_port.status() == Port.started:
if self._link.suspended() or self._destination_port.status() == Port.suspended:
# link or port is suspended
color = QtCore.Qt.yellow
shape = QtCore.Qt.RoundCap
elif self._destination_port.status() == Port.started:
# port is active
color = QtCore.Qt.green
shape = QtCore.Qt.RoundCap
elif self._destination_port.status() == Port.suspended:
# port is suspended
color = QtCore.Qt.yellow
shape = QtCore.Qt.RoundCap
else:
color = QtCore.Qt.red
shape = QtCore.Qt.SquareCap
@@ -194,4 +194,4 @@ class EthernetLinkItem(LinkItem):
painter.drawPoint(point2)
self._drawCaptureSymbol()
self._drawSymbol()

216
gns3/items/line_item.py Normal file
View File

@@ -0,0 +1,216 @@
# -*- 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/>.
"""
Graphical representation of a rectangle on the QGraphicsScene.
"""
import xml.etree.ElementTree as ET
from ..qt import QtCore, QtGui, QtWidgets
from .drawing_item import DrawingItem
class LineItem(QtWidgets.QGraphicsLineItem, DrawingItem):
"""
Class to draw a rectangle on the scene.
"""
def __init__(self, dst=None, svg=None, **kws):
super().__init__(svg=svg, **kws)
self.setAcceptHoverEvents(True)
self._edge = None
self._border = 20
if svg is None:
if dst is not None:
self.setLine(0,
0,
dst.x(),
dst.y())
pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
self.setPen(pen)
else:
self.fromSvg(svg)
if self._id is None:
self.create()
def paint(self, painter, option, widget=None):
"""
Paints the contents of an item in local coordinates.
:param painter: QPainter instance
:param option: QStyleOptionGraphicsItem instance
:param widget: QWidget instance
"""
super().paint(painter, option, widget)
self.drawLayerInfo(painter)
def toSvg(self):
"""
Return an SVG version of the shape
"""
svg = ET.Element("svg")
width = abs(self.line().x1() - self.line().x2())
height = abs(self.line().y1() - self.line().y2())
svg.set("width", str(int(width)))
svg.set("height", str(int(height)))
line = ET.SubElement(svg, "line")
line.set("x1", str(int(self.line().x1())))
line.set("x2", str(int(self.line().x2())))
line.set("y1", str(int(self.line().y1())))
line.set("y2", str(int(self.line().y2())))
line = self._styleSvg(line)
return ET.tostring(svg, encoding="utf-8").decode("utf-8")
def fromSvg(self, svg):
"""
Import element informations from an SVG
"""
svg = ET.fromstring(svg)
width = float(svg.get("width", 0))
height = float(svg.get("height", 0))
# Backup the pos and restore it
pos = self.pos()
y1 = self.line().y1()
self.setLine(0, 0, width, height)
pen = QtGui.QPen()
if len(svg):
pen = self._penFromSVGElement(svg[0])
self.setLine(
float(svg[0].get("x1")),
float(svg[0].get("y1")),
float(svg[0].get("x2")),
float(svg[0].get("y2"))
)
self.setPos(pos)
self.setPen(pen)
self.update()
def _isHorizontalLine(self):
return abs(self.line().x1() - self.line().x2()) > abs(self.line().y1() - self.line().y2())
def hoverMoveEvent(self, event):
"""
Handles all hover move events.
:param event: QGraphicsSceneHoverEvent instance
"""
# objects on the background layer don't need cursors
if self.zValue() >= 0:
if self._isHorizontalLine():
if event.pos().x() > (self.line().x2() - self._border):
self._graphics_view.setCursor(QtCore.Qt.SizeHorCursor)
elif event.pos().x() < self._border:
self._graphics_view.setCursor(QtCore.Qt.SizeHorCursor)
else:
self._graphics_view.setCursor(QtCore.Qt.SizeAllCursor)
# Vertical line
else:
if event.pos().y() > (self.line().y2() - self._border):
self._graphics_view.setCursor(QtCore.Qt.SizeVerCursor)
elif event.pos().y() < self._border:
self._graphics_view.setCursor(QtCore.Qt.SizeVerCursor)
else:
self._graphics_view.setCursor(QtCore.Qt.SizeAllCursor)
def mouseMoveEvent(self, event):
"""
Handles all mouse move events.
:param event: QMouseEvent instance
"""
self.update()
if self._edge:
scenePos = event.scenePos()
if self._edge == "left" or self._edge == "bottom":
diff_x = self.x() - scenePos.x()
diff_y = self.y() - scenePos.y()
self.setPos(scenePos.x(), scenePos.y())
self.setLine(
0,
0,
self.line().x2() + diff_x,
self.line().y2() + diff_y)
elif self._edge == "right" or self._edge == "top":
pos = self.mapFromScene(scenePos)
self.setLine(
0,
0,
pos.x(),
pos.y())
self.setPos(self.x(), self.y())
super().mouseMoveEvent(event)
def mousePressEvent(self, event):
"""
Handles all mouse press events.
:param event: QMouseEvent instance
"""
self.update()
if self._isHorizontalLine():
if event.pos().x() > (self.line().x2() - self._border):
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
self._edge = "right"
elif event.pos().x() < (self.line().x1() + self._border):
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
self._edge = "left"
else:
if event.pos().y() > (self.line().y2() - self._border):
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
self._edge = "top"
elif event.pos().y() < (self.line().y1() + self._border):
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
self._edge = "bottom"
super().mousePressEvent(event)
def mouseReleaseEvent(self, event):
"""
Handles all mouse release events.
:param: QMouseEvent instance
"""
self.update()
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable)
self._edge = None
super().mouseReleaseEvent(event)
def hoverLeaveEvent(self, event):
"""
Handles all hover leave events.
:param event: QGraphicsSceneHoverEvent instance
"""
# objects on the background layer don't need cursors
if self.zValue() >= 0:
self._graphics_view.setCursor(QtCore.Qt.ArrowCursor)

View File

@@ -24,9 +24,10 @@ import math
from ..qt import QtCore, QtGui, QtWidgets, QtSvg, qslot
from ..packet_capture import PacketCapture
from ..dialogs.filter_dialog import FilterDialog
class SvgCaptureItem(QtSvg.QGraphicsSvgItem):
class SvgIconItem(QtSvg.QGraphicsSvgItem):
def __init__(self, symbol, parent):
@@ -86,11 +87,17 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
# QGraphicsSvgItem to indicate a capture
self._capturing_item = None
# QGraphicsSvgItem to indicate a filter is applied
self._filter_item = None
# QGraphicsSvgItem to indicate we suspend a link
self._suspend_item = None
# QGraphicsSvgItem to indicate a filter is applied and a capture is active
self._filter_capturing_item = None
if not self._adding_flag:
# there is a destination
self._link = link
self._link.updated_link_signal.connect(self._drawCaptureSymbol)
self._link.updated_link_signal.connect(self._drawSymbol)
self._link.delete_link_signal.connect(self._linkDeletedSlot)
self.setFlag(self.ItemIsFocusable)
source_item.addLink(self)
@@ -118,6 +125,16 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
if self in self.scene().items():
self.scene().removeItem(self)
@qslot
def _filterActionSlot(self, *args):
dialog = FilterDialog(self._main_window, self._link)
dialog.show()
dialog.exec_()
@qslot
def _suspendActionSlot(self, *args):
self._link.toggleSuspend()
def delete(self):
"""
Delete this link
@@ -228,12 +245,32 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
analyze_action.triggered.connect(self._analyzeCaptureActionSlot)
menu.addAction(analyze_action)
if self._link.suspended() is False:
# Edit filters
filter_action = QtWidgets.QAction("Packet filters", menu)
filter_action.setIcon(QtGui.QIcon(':/icons/filter.svg'))
filter_action.triggered.connect(self._filterActionSlot)
menu.addAction(filter_action)
# Suspend link
suspend_action = QtWidgets.QAction("Suspend", menu)
suspend_action.setIcon(QtGui.QIcon(':/icons/pause.svg'))
suspend_action.triggered.connect(self._suspendActionSlot)
menu.addAction(suspend_action)
else:
# Resume link
resume_action = QtWidgets.QAction("Resume", menu)
resume_action.setIcon(QtGui.QIcon(':/icons/start.svg'))
resume_action.triggered.connect(self._suspendActionSlot)
menu.addAction(resume_action)
# delete
delete_action = QtWidgets.QAction("Delete", menu)
delete_action.setIcon(QtGui.QIcon(':/icons/delete.svg'))
delete_action.triggered.connect(self._deleteActionSlot)
menu.addAction(delete_action)
@qslot
def mousePressEvent(self, event):
"""
Called when the link is clicked and shows a contextual menu.
@@ -433,19 +470,90 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
self.update()
@qslot
def _drawCaptureSymbol(self, *args):
def _drawSymbol(self, *args):
"""
Draws a capture symbol in the middle of the link to indicate a capture is active.
Draws a symbol in the middle of the link to indicate a capture, a suspend or a filter is active.
"""
#FIXME: refactor ugly symbol management
if not self._adding_flag:
if self._link.capturing() and self.length >= 150:
link_center = QtCore.QPointF(self.source.x() + self.dx / 2.0 - 11, self.source.y() + self.dy / 2.0 - 11)
if self._capturing_item is None:
self._capturing_item = SvgCaptureItem(':/icons/inspect.svg', self)
self._capturing_item.setScale(0.6)
self._capturing_item.setPos(link_center)
if not self._capturing_item.isVisible():
self._capturing_item.show()
elif self._capturing_item:
self._capturing_item.hide()
if self._link.suspended():
if self.length >= 150:
link_center = QtCore.QPointF(self.source.x() + self.dx / 2.0 - 11, self.source.y() + self.dy / 2.0 - 11)
if self._suspend_item is None:
self._suspend_item = SvgIconItem(':/icons/pause.svg', self)
self._suspend_item.setScale(0.6)
if not self._suspend_item.isVisible():
self._suspend_item.show()
self._suspend_item.setPos(link_center)
if self._filter_item:
self._filter_item.hide()
elif self._suspend_item:
self._suspend_item.hide()
if self._filter_capturing_item:
self._filter_capturing_item.hide()
if self._capturing_item:
self._capturing_item.hide()
if self._filter_item:
self._filter_item.hide()
elif self._link.capturing() and len(self._link.filters()) > 0:
if self.length >= 150:
link_center = QtCore.QPointF(self.source.x() + self.dx / 2.0 - 11, self.source.y() + self.dy / 2.0 - 11)
if self._filter_capturing_item is None:
self._filter_capturing_item = SvgIconItem(':/icons/filter-capture.svg', self)
self._filter_capturing_item.setScale(0.6)
if not self._filter_capturing_item.isVisible():
self._filter_capturing_item.show()
self._filter_capturing_item.setPos(link_center)
elif self._filter_capturing_item:
self._filter_capturing_item.hide()
if self._capturing_item:
self._capturing_item.hide()
if self._filter_item:
self._filter_item.hide()
if self._suspend_item:
self._suspend_item.hide()
elif self._link.capturing():
if self.length >= 150:
link_center = QtCore.QPointF(self.source.x() + self.dx / 2.0 - 11, self.source.y() + self.dy / 2.0 - 11)
if self._capturing_item is None:
self._capturing_item = SvgIconItem(':/icons/inspect.svg', self)
self._capturing_item.setScale(0.6)
self._capturing_item.setPos(link_center)
if not self._capturing_item.isVisible():
self._capturing_item.show()
elif self._capturing_item:
self._capturing_item.hide()
if self._filter_capturing_item:
self._filter_capturing_item.hide()
if self._suspend_item:
self._suspend_item.hide()
elif len(self._link.filters()) > 0:
if self.length >= 150:
link_center = QtCore.QPointF(self.source.x() + self.dx / 2.0 - 11, self.source.y() + self.dy / 2.0 - 11)
if self._filter_item is None:
self._filter_item = SvgIconItem(':/icons/filter.svg', self)
self._filter_item.setScale(0.6)
if not self._filter_item.isVisible():
self._filter_item.show()
self._filter_item.setPos(link_center)
elif self._filter_item:
self._filter_item.hide()
if self._filter_capturing_item:
self._filter_capturing_item.hide()
if self._suspend_item:
self._suspend_item.hide()
else:
if self._capturing_item:
self._capturing_item.hide()
if self._suspend_item:
self._suspend_item.hide()
if self._filter_item:
self._filter_item.hide()
if self._filter_capturing_item:
self._filter_capturing_item.hide()

View File

@@ -114,19 +114,19 @@ class SerialLinkItem(LinkItem):
return
# source point color
if self._source_port.status() == Port.started:
if self._link.suspended() or self._source_port.status() == Port.suspended:
# link or port is suspended
shape = QtCore.Qt.RoundCap
color = QtCore.Qt.yellow
elif self._source_port.status() == Port.started:
# port is active
shape = QtCore.Qt.RoundCap
color = QtCore.Qt.green
elif self._source_port.status() == Port.suspended:
# port is suspended
shape = QtCore.Qt.RoundCap
color = QtCore.Qt.yellow
else:
shape = QtCore.Qt.SquareCap
color = QtCore.Qt.red
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.MiterJoin))
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, shape, QtCore.Qt.MiterJoin))
source_port_label = self._source_port.label()
if source_port_label is None:
@@ -143,14 +143,14 @@ class SerialLinkItem(LinkItem):
painter.drawPoint(self.source_point)
# destination point color
if self._destination_port.status() == Port.started:
if self._link.suspended() or self._destination_port.status() == Port.suspended:
# link or port is suspended
color = QtCore.Qt.yellow
shape = QtCore.Qt.RoundCap
elif self._destination_port.status() == Port.started:
# port is active
color = QtCore.Qt.green
shape = QtCore.Qt.RoundCap
elif self._destination_port.status() == Port.suspended:
# port is suspended
color = QtCore.Qt.yellow
shape = QtCore.Qt.RoundCap
else:
color = QtCore.Qt.red
shape = QtCore.Qt.SquareCap
@@ -172,4 +172,4 @@ class SerialLinkItem(LinkItem):
painter.drawPoint(self.destination_point)
self._drawCaptureSymbol()
self._drawSymbol()

View File

@@ -30,16 +30,6 @@ log = logging.getLogger(__name__)
class ShapeItem(DrawingItem):
# Map QT stroke to SVG style
QT_DASH_TO_SVG = {
QtCore.Qt.SolidLine: "",
QtCore.Qt.NoPen: None,
QtCore.Qt.DashLine: "25, 25",
QtCore.Qt.DotLine: "5, 25",
QtCore.Qt.DashDotLine: "5, 25, 25",
QtCore.Qt.DashDotDotLine: "25, 25, 5, 25, 5"
}
"""
Base class to draw shapes on the scene.
"""
@@ -180,27 +170,6 @@ class ShapeItem(DrawingItem):
if self.zValue() >= 0:
self._graphics_view.setCursor(QtCore.Qt.ArrowCursor)
def _styleSvg(self, element):
"""
Add style from the shape item to the SVG element that we will
export
"""
style = ""
pen = self.pen()
element.set("fill", "#{}".format(hex(self.brush().color().rgba())[4:]))
element.set("fill-opacity", str(self.brush().color().alphaF()))
dasharray = self.QT_DASH_TO_SVG[pen.style()]
if dasharray is None: # No border to the element
return element
elif dasharray == "":
pass # Solid line
else:
element.set("stroke-dasharray", dasharray)
element.set("stroke-width", str(pen.width()))
element.set("stroke", "#" + hex(pen.color().rgba())[4:])
return element
def fromSvg(self, svg):
"""
Import element informations from an SVG
@@ -214,10 +183,7 @@ class ShapeItem(DrawingItem):
brush = QtGui.QBrush(QtCore.Qt.SolidPattern)
if len(svg):
if svg[0].get("stroke-width"):
pen.setWidth(int(svg[0].get("stroke-width")))
if svg[0].get("stroke"):
pen.setColor(colorFromSvg(svg[0].get("stroke")))
pen = self._penFromSVGElement(svg[0])
if svg[0].get("fill"):
new_color = colorFromSvg(svg[0].get("fill"))
color = brush.color()
@@ -230,17 +196,6 @@ class ShapeItem(DrawingItem):
color.setAlphaF(float(svg[0].get("fill-opacity")))
brush.setColor(color)
# Map SVG stroke style (border of the element to the Qt version)
if not svg[0].get("stroke"):
pen.setStyle(QtCore.Qt.NoPen)
else:
pen.setStyle(QtCore.Qt.SolidLine)
stroke = svg[0].get("stroke-dasharray")
if stroke:
for (qt_stroke, svg_stroke) in self.QT_DASH_TO_SVG.items():
if svg_stroke == stroke:
pen.setStyle(qt_stroke)
self.setPen(pen)
self.setBrush(brush)
self.update()

View File

@@ -59,10 +59,10 @@ class Link(QtCore.QObject):
super().__init__()
log.info("adding link from {} {} to {} {}".format(source_node.name(),
source_port.name(),
destination_node.name(),
destination_port.name()))
log.debug("adding link from {} {} to {} {}".format(source_node.name(),
source_port.name(),
destination_node.name(),
destination_port.name()))
# create an unique ID
self._id = Link._instance_count
@@ -79,10 +79,12 @@ class Link(QtCore.QObject):
self._capture_file_path = None
self._capture_file = None
self._initialized = False
self._filters = {}
self._suspend = False
# Boolean if True we are creatin the first instance of this node
# Boolean if True we are creating the first instance of this node
# if false the node already exist in the topology
# use to avoid erasing informations when reloading
# use to avoid erasing information when reloading
self._creator = False
self._nodes = []
@@ -124,11 +126,22 @@ class Link(QtCore.QObject):
if "nodes" in result:
self._nodes = result["nodes"]
self._updateLabels()
if "filters" in result:
self._filters = result["filters"]
if "suspend" in result:
self._suspend = result["suspend"]
self.updated_link_signal.emit(self._id)
def creator(self):
return self._creator
def suspended(self):
return self._suspend
def toggleSuspend(self):
self._suspend = not self._suspend
self.update()
def initialized(self):
return self._initialized
@@ -149,6 +162,12 @@ class Link(QtCore.QObject):
body = self._prepareParams()
Controller.instance().put("/projects/{project_id}/links/{link_id}".format(project_id=self._source_node.project().id(), link_id=self._link_id), self.updateLinkCallback, body=body)
def listAvailableFilters(self, callback):
"""
Get the list of available filters
"""
Controller.instance().get("/projects/{project_id}/links/{link_id}/available_filters".format(project_id=self._source_node.project().id(), link_id=self._link_id), callback)
def updateLinkCallback(self, result, error=False, *args, **kwargs):
if error:
QtWidgets.QMessageBox.warning(None, "Update link", "Error while updating link: {}".format(result["message"]))
@@ -167,10 +186,14 @@ class Link(QtCore.QObject):
def _updateLabel(self, label, label_data):
if not label or sip.isdeleted(label):
return
label.setPlainText(label_data["text"])
label.setPos(label_data["x"], label_data["y"])
label.setStyle(label_data["style"])
label.setRotation(label_data["rotation"])
if "text" in label_data:
label.setPlainText(label_data["text"])
if "x" in label_data and "y" in label_data:
label.setPos(label_data["x"], label_data["y"])
if "style" in label_data:
label.setStyle(label_data["style"])
if "rotation" in label_data:
label.setRotation(label_data["rotation"])
def _prepareParams(self):
body = {
@@ -185,7 +208,9 @@ class Link(QtCore.QObject):
"adapter_number": self._destination_port.adapterNumber(),
"port_number": self._destination_port.portNumber()
}
]
],
"filters": self._filters,
"suspend": self._suspend
}
if self._source_port.label():
body["nodes"][0]["label"] = self._source_port.label().dump()
@@ -243,10 +268,18 @@ class Link(QtCore.QObject):
def __str__(self):
return "Link from {} port {} to {} port {}".format(self._source_node.name(),
self._source_port.name(),
self._destination_node.name(),
self._destination_port.name())
description = "Link from {} port {} to {} port {}".format(self._source_node.name(),
self._source_port.name(),
self._destination_node.name(),
self._destination_port.name())
if self.capturing():
description += "\nPacket capture is active"
for filter_type in self._filters.keys():
description += "\nPacket filter '{}' is active".format(filter_type)
return description
def capture_file_name(self):
"""
@@ -264,10 +297,10 @@ class Link(QtCore.QObject):
Deletes this link.
"""
log.info("deleting link from {} {} to {} {}".format(self._source_node.name(),
self._source_port.name(),
self._destination_node.name(),
self._destination_port.name()))
log.debug("deleting link from {} {} to {} {}".format(self._source_node.name(),
self._source_port.name(),
self._destination_node.name(),
self._destination_port.name()))
if skip_controller:
self._linkDeletedCallback({})
@@ -409,3 +442,15 @@ class Link(QtCore.QObject):
if self._destination_node == node:
return self._destination_port
return self._source_port
def filters(self):
"""
:returns: List the filters active on the node
"""
return self._filters
def setFilters(self, filters):
"""
:params filters: List of filters
"""
self._filters = filters

View File

@@ -183,6 +183,13 @@ class LocalConfig(QtCore.QObject):
return os.path.normpath(path)
def runAsRootPath(self):
"""
Gets run as root filename
:return: string
"""
return os.path.join(self.configDirectory(), "run_as_root")
def _migrateOldConfigPath(self):
"""
Migrate pre 1.4 config path
@@ -269,7 +276,7 @@ class LocalConfig(QtCore.QObject):
Read the configuration file.
"""
log.info("Load config from %s", config_path)
log.debug("Load config from %s", config_path)
try:
with open(config_path, "r", encoding="utf-8") as f:
self._last_config_changed = os.stat(config_path).st_mtime
@@ -296,7 +303,7 @@ class LocalConfig(QtCore.QObject):
with open(temporary, "w", encoding="utf-8") as f:
json.dump(self._settings, f, sort_keys=True, indent=4)
shutil.move(temporary, self._config_file)
log.info("Configuration save to %s", self._config_file)
log.debug("Configuration save to %s", self._config_file)
self._last_config_changed = os.stat(self._config_file).st_mtime
except (ValueError, OSError) as e:
log.error("Could not write the config file {}: {}".format(self._config_file, e))
@@ -324,7 +331,7 @@ class LocalConfig(QtCore.QObject):
try:
if self._last_config_changed and self._last_config_changed < os.stat(self._config_file).st_mtime:
log.info("Client config has changed, reloading it...")
log.debug("Client config has changed, reloading it...")
self._readConfig(self._config_file)
self.config_changed_signal.emit()
except OSError as e:
@@ -402,9 +409,8 @@ class LocalConfig(QtCore.QObject):
self._settings[section] = settings
if changed:
log.info("Section %s has missing default values. Adding keys %s Saving configuration", section, ','.join(set(default_settings.keys()) - set(settings.keys())))
log.debug("Section %s has missing default values. Adding keys %s Saving configuration", section, ','.join(set(default_settings.keys()) - set(settings.keys())))
self.writeConfig()
return copy.deepcopy(settings)
def saveSectionSettings(self, section, settings):
@@ -420,7 +426,7 @@ class LocalConfig(QtCore.QObject):
if self._settings[section] != settings:
self._settings[section].update(copy.deepcopy(settings))
log.info("Section %s has changed. Saving configuration", section)
log.debug("Section %s has changed. Saving configuration", section)
self.writeConfig()
else:
log.debug("Section %s has not changed. Skip saving configuration", section)

View File

@@ -30,7 +30,7 @@ import signal
import subprocess
from gns3.qt import QtWidgets, QtCore
from gns3.qt import QtWidgets, QtCore, qslot
from gns3.settings import LOCAL_SERVER_SETTINGS
from gns3.local_config import LocalConfig
from gns3.local_server_config import LocalServerConfig
@@ -59,17 +59,20 @@ class StopLocalServerWorker(QtCore.QObject):
def __init__(self, local_server_process):
super().__init__()
self._local_server_process = local_server_process
self._precision = 100 # In MS
self._remaining_trial = int(10 * (1000 / self._precision))
@qslot
def _callbackSlot(self, *params):
self._local_server_process.poll()
if self._local_server_process.returncode is None and self._remaining_trial > 0:
self._remaining_trial -= 1
QtCore.QTimer.singleShot(self._precision, self._callbackSlot)
else:
self.finished.emit()
def run(self):
precision = 1
remaining_trial = 4 / precision # 4 Seconds
while remaining_trial > 0:
if self._local_server_process.returncode is None:
remaining_trial -= 1
self.thread().sleep(precision)
else:
break
self.finished.emit()
QtCore.QTimer.singleShot(1000, self._callbackSlot)
def cancel(self):
return
@@ -157,28 +160,23 @@ class LocalServer(QtCore.QObject):
if sys.platform.startswith("linux"):
# test if the executable has the CAP_NET_RAW capability (Linux only)
try:
if "security.capability" in os.listxattr(path):
caps = os.getxattr(path, "security.capability")
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
if not struct.unpack("<IIIII", caps)[1] & 1 << 13:
proceed = QtWidgets.QMessageBox.question(
self.parent(),
"uBridge",
"uBridge requires CAP_NET_RAW capability to interact with network interfaces. Set the capability to uBridge? All users on the system will be able to read packet from the network interfaces.",
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No)
if proceed == QtWidgets.QMessageBox.Yes:
sudo(["setcap", "cap_net_admin,cap_net_raw=ep"])
else:
# capabilities not supported
request_setuid = True
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
if "security.capability" not in os.listxattr(path) or not struct.unpack("<IIIII", os.getxattr(path, "security.capability"))[1] & 1 << 13:
proceed = QtWidgets.QMessageBox.question(
self.parent(),
"uBridge",
"uBridge requires CAP_NET_RAW capability to interact with network interfaces. Set the capability to uBridge? All users on the system will be able to read packet from the network interfaces.",
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No)
if proceed == QtWidgets.QMessageBox.Yes:
sudo(["setcap", "cap_net_admin,cap_net_raw=ep", path])
except AttributeError:
# Due to a Python bug, os.listxattr could be missing: https://github.com/GNS3/gns3-gui/issues/2010
log.warning("Could not determine if CAP_NET_RAW capability is set for uBridge (Python bug)")
return True
except OSError as e:
QtWidgets.QMessageBox.critical(self.parent(), "uBridge", "Can't set CAP_NET_RAW capability to uBridge {}: {}".format(path, str(e)))
return False
request_setuid = True
if sys.platform.startswith("darwin") or request_setuid:
try:
@@ -186,7 +184,7 @@ class LocalServer(QtCore.QObject):
proceed = QtWidgets.QMessageBox.question(
self.parent(),
"uBridge",
"uBridge requires root permissions to interact with network interfaces. Set root permissions to uBridge? All admin users on the system will be able to read packet from the network interfaces.",
"uBridge requires root permissions to interact with network interfaces. Set root permissions to uBridge? All admin users on the system will be able to read packet from the network interfaces.",
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No)
if proceed == QtWidgets.QMessageBox.Yes:
@@ -332,7 +330,7 @@ class LocalServer(QtCore.QObject):
return True
if self.isLocalServerRunning():
log.info("A local server already running on this host")
log.debug("A local server already running on this host")
# Try to kill the server. The server can be still running after
# if the server was started by hand
self._killAlreadyRunningServer()
@@ -467,7 +465,7 @@ class LocalServer(QtCore.QObject):
log.warn("could not delete server log file {}: {}".format(logpath, e))
command += ' --log="{}" --pid="{}"'.format(logpath, self._pid_path())
log.info("Starting local server process with {}".format(command))
log.debug("Starting local server process with {}".format(command))
try:
if sys.platform.startswith("win"):
# use the string on Windows
@@ -480,7 +478,7 @@ class LocalServer(QtCore.QObject):
log.warning('Could not start local server "{}": {}'.format(command, e))
return False
log.info("Local server process has started (PID={})".format(self._local_server_process.pid))
log.debug("Local server process has started (PID={})".format(self._local_server_process.pid))
return True
def _checkLocalServerRunningSlot(self):
@@ -539,7 +537,7 @@ class LocalServer(QtCore.QObject):
if self.localServerProcessIsRunning():
self._stopping = True
log.info("Stopping local server (PID={})".format(self._local_server_process.pid))
log.debug("Stopping local server (PID={})".format(self._local_server_process.pid))
# local server is running, let's stop it
if self._http_client:
self._http_client.shutdown()

View File

@@ -184,8 +184,8 @@ def main():
if sys.version_info < (3, 4):
raise SystemExit("Python 3.4 or higher is required")
if parse_version(QtCore.QT_VERSION_STR) < parse_version("5.0.0"):
raise SystemExit("Requirement is PyQt5 version 5.0.0 or higher, got version {}".format(QtCore.QT_VERSION_STR))
if parse_version(QtCore.QT_VERSION_STR) < parse_version("5.5.0"):
raise SystemExit("Requirement is PyQt5 version 5.5.0 or higher, got version {}".format(QtCore.QT_VERSION_STR))
if parse_version(psutil.__version__) < parse_version("2.2.1"):
raise SystemExit("Requirement is psutil version 2.2.1 or higher, got version {}".format(psutil.__version__))
@@ -228,8 +228,10 @@ def main():
if local_config.multiProfiles() and not options.profile:
profile_select = ProfileSelectDialog()
profile_select.show()
profile_select.exec_()
options.profile = profile_select.profile()
if profile_select.exec_():
options.profile = profile_select.profile()
else:
sys.exit(0)
# Init the config
if options.config:

View File

@@ -51,6 +51,8 @@ from .update_manager import UpdateManager
from .utils.analytics import AnalyticsClient
from .dialogs.appliance_wizard import ApplianceWizard
from .dialogs.new_appliance_dialog import NewApplianceDialog
from .dialogs.notif_dialog import NotifDialog, NotifDialogHandler
from .status_bar import StatusBarHandler
from .registry.appliance import ApplianceError
log = logging.getLogger(__name__)
@@ -75,6 +77,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
super().__init__(parent)
self.setupUi(self)
self._notif_dialog = NotifDialog(self)
# Setup logger
logging.getLogger().addHandler(NotifDialogHandler(self._notif_dialog))
logging.getLogger().addHandler(StatusBarHandler(self.uiStatusBar))
self._open_file_at_startup = open_file
MainWindow._instance = self
@@ -82,6 +89,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
topology.setMainWindow(self)
topology.project_changed_signal.connect(self._projectChangedSlot)
Controller.instance().setParent(self)
LocalServer.instance().setParent(self)
self._settings = {}
HTTPClient.setProgressCallback(Progress.instance(self))
@@ -212,6 +220,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.uiResetPortLabelsAction.triggered.connect(self._resetPortLabelsActionSlot)
self.uiShowPortNamesAction.triggered.connect(self._showPortNamesActionSlot)
self.uiShowGridAction.triggered.connect(self._showGridActionSlot)
self.uiSnapToGridAction.triggered.connect(self._snapToGridActionSlot)
# tool menu connections
self.uiWebInterfaceAction.triggered.connect(self._openWebInterfaceActionSlot)
@@ -232,6 +241,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.uiInsertImageAction.triggered.connect(self._insertImageActionSlot)
self.uiDrawRectangleAction.triggered.connect(self._drawRectangleActionSlot)
self.uiDrawEllipseAction.triggered.connect(self._drawEllipseActionSlot)
self.uiDrawLineAction.triggered.connect(self._drawLineActionSlot)
self.uiEditReadmeAction.triggered.connect(self._editReadmeActionSlot)
# help menu connections
@@ -299,8 +309,26 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
"""
Called when we ask to display the grid
"""
self.showGrid(self.uiShowGridAction.isChecked())
self.uiGraphicsView.viewport().update()
# save settings
project = Topology.instance().project()
if project is not None:
project.setShowGrid(self.uiShowGridAction.isChecked())
project.update()
def _snapToGridActionSlot(self):
"""
Called when user click on the snap to grid menu item
:return: None
"""
self.snapToGrid(self.uiSnapToGridAction.isChecked())
# save settings
project = Topology.instance().project()
if project is not None:
project.setSnapToGrid(self.uiSnapToGridAction.isChecked())
project.update()
def analyticsClient(self):
"""
@@ -313,6 +341,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
"""
Slot called to create a new project.
"""
# prevents race condition
if self._project_dialog is not None:
return
self._project_dialog = ProjectDialog(self)
self._project_dialog.show()
create_new_project = self._project_dialog.exec_()
@@ -321,7 +354,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.uiNodesDockWidget.setWindowTitle("")
if create_new_project:
Topology.instance().createLoadProject(self._project_dialog.getProjectSettings())
Topology.instance().createLoadProject(
self._project_dialog.getProjectSettings())
self._project_dialog = None
@@ -537,6 +571,60 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
# TODO: quality option
return image.save(path)
def showLayers(self, show_layers):
"""
Shows layers in GUI
:param show_layers: boolean
:return: None
"""
NodeItem.show_layer = show_layers
ShapeItem.show_layer = show_layers
for item in self.uiGraphicsView.items():
item.update()
def showGrid(self, show_grid):
"""
Shows grid in GUI
:param show_grid: boolean
:return: None
"""
self.uiGraphicsView.viewport().update()
def snapToGrid(self, snap_to_grid):
"""
Snap to grid in GUI
:param snap_to_grid: boolean
:return: None
"""
self.uiGraphicsView.viewport().update()
def showInterfaceLabels(self, show_interface_labels):
"""
Show interface labels in GUI
:param show_interface_labels: boolean
:return: None
"""
LinkItem.showPortLabels(show_interface_labels)
for item in self.uiGraphicsView.scene().items():
if isinstance(item, LinkItem):
item.adjust()
def _updateZoomSettings(self, zoom=None):
"""
Updates zoom settings
:param zoom integer optional, when not provided then calculated from current view
:return: None
"""
if zoom is None:
zoom = round(self.uiGraphicsView.transform().m11() * 100)
# save settings
project = Topology.instance().project()
if project is not None:
project.setZoom(zoom)
project.update()
def _screenshotActionSlot(self):
"""
Slot called to take a screenshot of the scene.
@@ -605,6 +693,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
factor_in = pow(2.0, 120 / 240.0)
self.uiGraphicsView.scaleView(factor_in)
self._updateZoomSettings()
def _zoomOutActionSlot(self):
"""
@@ -613,6 +702,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
factor_out = pow(2.0, -120 / 240.0)
self.uiGraphicsView.scaleView(factor_out)
self._updateZoomSettings()
def _zoomResetActionSlot(self):
"""
@@ -620,6 +710,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
"""
self.uiGraphicsView.resetTransform()
self._updateZoomSettings()
def _fitInViewActionSlot(self):
"""
@@ -635,11 +726,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
"""
Slot called to show the layer positions on the scene.
"""
self.showLayers(self.uiShowLayersAction.isChecked())
NodeItem.show_layer = self.uiShowLayersAction.isChecked()
ShapeItem.show_layer = self.uiShowLayersAction.isChecked()
for item in self.uiGraphicsView.items():
item.update()
# save settings
project = Topology.instance().project()
if project is not None:
project.setShowLayers(self.uiShowLayersAction.isChecked())
project.update()
def _resetPortLabelsActionSlot(self):
"""
@@ -656,10 +749,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
Slot called to show the port names on the scene.
"""
LinkItem.showPortLabels(self.uiShowPortNamesAction.isChecked())
for item in self.uiGraphicsView.scene().items():
if isinstance(item, LinkItem):
item.adjust()
self.showInterfaceLabels(self.uiShowPortNamesAction.isChecked())
# save settings
project = Topology.instance().project()
if project is not None:
project.setShowInterfaceLabels(self.uiShowPortNamesAction.isChecked())
project.update()
def _startAllActionSlot(self):
"""
@@ -737,7 +833,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
return
self._pictures_dir = os.path.dirname(path)
image = QtGui.QPixmap(path)
QtGui.QPixmap(path)
self.uiGraphicsView.addImage(path)
def _drawRectangleActionSlot(self):
@@ -754,6 +850,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.uiGraphicsView.addEllipse(self.uiDrawEllipseAction.isChecked())
def _drawLineActionSlot(self):
"""
Slot called when adding a line on the scene.
"""
self.uiGraphicsView.addLine(self.uiDrawLineAction.isChecked())
def _onlineHelpActionSlot(self):
"""
Slot to launch a browser pointing to the documentation page.
@@ -842,8 +945,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
else:
self.uiNodesDockWidget.setWindowTitle(title)
self.uiNodesDockWidget.setVisible(True)
self.uiNodesView.clear()
self.uiNodesView.populateNodesView(category)
self.uiNodesDockWidget.populateNodesView(category)
def _localConfigChangedSlot(self):
"""
@@ -918,6 +1020,10 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
"""
Topology.instance().editReadme()
def resizeEvent(self, event):
self._notif_dialog.resize()
super().resizeEvent(event)
def keyPressEvent(self, event):
"""
Handles all key press events for the main window.
@@ -1000,9 +1106,28 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
sys.exit(1)
return
run_as_root_path = LocalConfig.instance().runAsRootPath()
if not sys.platform.startswith("win") and os.geteuid() == 0:
# touches file to know that user has run GNS3 as root and to prevent
# from running as user
if not os.path.exists(run_as_root_path):
try:
open(run_as_root_path, 'a').close()
except OSError as e:
log.warning("Cannot write `run_as_root` file due to: {}".format(str(e)))
QtWidgets.QMessageBox.warning(self, "Root", "Running GNS3 as root is not recommended and could be dangerous")
if not sys.platform.startswith("win") and os.geteuid() != 0 and os.path.exists(run_as_root_path):
QtWidgets.QMessageBox.critical(
self, "Run as user",
"GNS3 has been previously run as root. It is not possible "
"to change to another user and GNS3 will be shutdown. Please delete the '{}' file "
"and start the program again.".format(run_as_root_path))
sys.exit(1)
# restore debug level
if self._settings["debug_level"]:
root = logging.getLogger()

View File

@@ -33,7 +33,6 @@ from .atm_switch import ATMSwitch
from .settings import (
BUILTIN_SETTINGS,
CLOUD_SETTINGS,
NAT_SETTINGS,
ETHERNET_HUB_SETTINGS,
ETHERNET_SWITCH_SETTINGS
)
@@ -224,45 +223,9 @@ class Builtin(Module):
:param project: Project instance
"""
log.info("instantiating node {}".format(node_class))
# create an instance of the node class
return node_class(self, server, project)
def createNode(self, node, node_name):
"""
Creates a node.
:param node: Node instance
:param node_name: Node name
"""
log.info("creating node {}".format(node))
if isinstance(node, Cloud):
for key, info in self._cloud_nodes.items():
if node_name == info["name"]:
default_name_format = info["default_name_format"].replace('{name}', node_name)
node.create(ports=info["ports_mapping"], default_name_format=default_name_format)
return
elif isinstance(node, Nat):
for key, info in self._nat_nodes.items():
if node_name == info["name"]:
default_name_format = info["default_name_format"].replace('{name}', node_name)
node.create(default_name_format=default_name_format)
return
elif isinstance(node, EthernetHub):
for key, info in self._ethernet_hubs.items():
if node_name == info["name"]:
default_name_format = info["default_name_format"].replace('{name}', node_name)
node.create(ports=info["ports_mapping"], default_name_format=default_name_format)
return
elif isinstance(node, EthernetSwitch):
for key, info in self._ethernet_switches.items():
if node_name == info["name"]:
default_name_format = info["default_name_format"].replace('{name}', node_name)
node.create(ports=info["ports_mapping"], default_name_format=default_name_format)
return
node.create()
@staticmethod
def findAlternativeInterface(node, missing_interface):
@@ -330,17 +293,6 @@ class Builtin(Module):
"""
nodes = []
for node_class in Builtin.classes():
nodes.append(
{"class": node_class.__name__,
"name": node_class.symbolName(),
"categories": node_class.categories(),
"symbol": node_class.defaultSymbol(),
"builtin": True,
"node_type": node_class.URL_PREFIX
}
)
# add custom cloud node templates
for cloud_node in self._cloud_nodes.values():
nodes.append(
@@ -371,9 +323,8 @@ class Builtin(Module):
"server": switch["server"],
"symbol": switch["symbol"],
"categories": [switch["category"]]
}
}
)
return nodes
@staticmethod

View File

@@ -44,20 +44,6 @@ class ATMSwitch(Node):
self._always_on = True
self.settings().update({"mappings": {}})
def create(self, name=None, node_id=None, mappings=None, default_name_format="ATM{0}"):
"""
Creates this ATM switch.
:param name: optional name for this switch.
:param node_id: Node identifier on the server
:param mappings: mappings to be automatically added when creating this ATM switch
"""
params = {}
if mappings:
params["mappings"] = mappings
self._create(name, node_id, params, default_name_format)
def _createCallback(self, result):
"""
Callback for create.
@@ -167,28 +153,6 @@ class ATMSwitch(Node):
atmsw["properties"]["mappings"] = self._settings["mappings"]
return atmsw
def load(self, node_info):
"""
Loads an ATM switch representation
(from a topology file).
:param node_info: representation of the node (dictionary)
"""
super().load(node_info)
properties = node_info["properties"]
name = properties.pop("name")
# ATM switches do not have an UUID before version 2.0
node_id = properties.get("node_id", str(uuid.uuid4()))
mappings = {}
if "mappings" in properties:
mappings = properties["mappings"]
log.info("ATM switch {} is loading".format(name))
self.create(name, node_id, mappings)
def configPage(self):
"""
Returns the configuration page widget to be used by the node properties dialog.

View File

@@ -46,20 +46,6 @@ class Cloud(Node):
return self._interfaces
def create(self, name=None, node_id=None, ports=None, default_name_format="Cloud{0}"):
"""
Creates this cloud.
:param name: optional name for this cloud
:param node_id: Node identifier on the server
:param ports: ports to be automatically added when creating this cloud
"""
params = {}
if ports:
params["ports_mapping"] = ports
self._create(name, node_id, params, default_name_format)
def _createCallback(self, result, error=False, **kwargs):
"""
Callback for create.

View File

@@ -39,20 +39,6 @@ class EthernetHub(Node):
self._always_on = True
self.settings().update({"ports_mapping": []})
def create(self, name=None, node_id=None, ports=None, default_name_format="Hub{0}"):
"""
Creates this hub.
:param name: optional name for this hub
:param node_id: node identifier on the server
:param ports: ports to automatically be added when creating this hub
"""
params = {}
if ports:
params["ports_mapping"] = ports
self._create(name, node_id, params, default_name_format)
def _createCallback(self, result):
"""
Callback for create.

View File

@@ -38,21 +38,7 @@ class EthernetSwitch(Node):
# this is an always-on node
self.setStatus(Node.started)
self._always_on = True
self.settings().update({"ports_mapping": []})
def create(self, name=None, node_id=None, ports=None, default_name_format="SW{0}"):
"""
Creates this Ethernet switch.
:param name: optional name for this switch
:param node_id: node identifier on the server
:param ports: ports to be automatically added when creating this switch
"""
params = {}
if ports:
params["ports_mapping"] = ports
self._create(name, node_id, params, default_name_format)
self.settings().update({"ports_mapping": [], "console": None, "console_type": "telnet"})
def _createCallback(self, result):
"""
@@ -61,6 +47,10 @@ class EthernetSwitch(Node):
:param result: server response (dict)
"""
self.settings()["ports_mapping"] = result["ports_mapping"]
self.settings()["console"] = result["console"]
def console(self):
return self.settings()["console"]
def update(self, new_settings):
"""

View File

@@ -41,20 +41,6 @@ class FrameRelaySwitch(Node):
self._always_on = True
self.settings().update({"mappings": {}})
def create(self, name=None, node_id=None, mappings={}, default_name_format="FR{0}"):
"""
Creates this Frame Relay switch.
:param name: name for this switch.
:param node_id: node identifier on the server
:param mappings: mappings to be automatically added when creating this Frame relay switch
"""
params = {}
if mappings:
params["mappings"] = mappings
self._create(name, node_id, params, default_name_format)
def _createCallback(self, result):
"""
Callback for create.

View File

@@ -46,17 +46,6 @@ class Nat(Node):
return self._interfaces
def create(self, name=None, node_id=None, default_name_format="Nat{0}"):
"""
Creates this nat.
:param name: optional name for this nat
:param node_id: Node identifier on the server
"""
params = {}
self._create(name, node_id, params, default_name_format)
def _createCallback(self, result, error=False, **kwargs):
"""
Callback for create.

View File

@@ -25,14 +25,6 @@ BUILTIN_SETTINGS = {
}
NAT_SETTINGS = {
"name": "",
"default_name_format": "Nat{0}",
"symbol": ":/symbols/cloud.svg",
"category": Node.end_devices,
"ports_mapping": [],
}
CLOUD_SETTINGS = {
"name": "",
"default_name_format": "Cloud{0}",

View File

@@ -137,60 +137,9 @@ class Docker(Module):
:param node_class: Node object
:param server: HTTPClient instance
"""
log.info("instantiating node {}".format(node_class))
# create an instance of the node class
return node_class(self, server, project)
def createNode(self, node, node_name):
"""
Creates a node.
:param node: Node instance
:param node_name: Node name
"""
log.info("creating node {} with id {}".format(node, node.id()))
image = None
if node_name:
for image_key, info in self._docker_containers.items():
if node_name == info["name"]:
image = image_key
if not image:
selected_images = []
for image, info in self._docker_containers.items():
if info["server"] == node.compute().id():
selected_images.append(image)
if not selected_images:
raise ModuleError("No Docker VM on server {}".format(
node.server().url()))
elif len(selected_images) > 1:
from gns3.main_window import MainWindow
mainwindow = MainWindow.instance()
(selection, ok) = QtWidgets.QInputDialog.getItem(
mainwindow, "Docker Image", "Please choose an image",
selected_images, 0, False)
if ok:
image = selection
else:
raise ModuleError("Please select a Docker Image")
else:
image = selected_images[0]
image_settings = {}
for setting_name, value in self._docker_containers[image].items():
if setting_name in node.settings() and value != "" and value is not None:
if setting_name not in ['name', 'image']:
image_settings[setting_name] = value
default_name_format = DOCKER_CONTAINER_SETTINGS["default_name_format"]
if self._docker_containers[image]["default_name_format"]:
default_name_format = self._docker_containers[image]["default_name_format"]
image = self._docker_containers[image]["image"]
node.create(image, base_name=node_name, additional_settings=image_settings, default_name_format=default_name_format)
def reset(self):
"""Resets the servers."""
self._nodes.clear()

View File

@@ -38,7 +38,6 @@ class DockerVM(Node):
def __init__(self, module, server, project):
super().__init__(module, server, project)
log.info("Docker VM is being created")
docker_vm_settings = {"image": "",
"adapters": DOCKER_CONTAINER_SETTINGS["adapters"],
@@ -54,23 +53,6 @@ class DockerVM(Node):
self.settings().update(docker_vm_settings)
def create(self, image, name=None, base_name=None, node_id=None, additional_settings={}, default_name_format="{name}-{0}"):
"""Creates this Docker container.
:param image: image name
:param name: optional name
:param additional_settings: additional settings for this VM
"""
params = {
"image": image,
"adapters": self._settings["adapters"]
}
params.update(additional_settings)
if base_name:
default_name_format = default_name_format.replace('{name}', base_name)
self._create(name=name, node_id=node_id, params=params, default_name_format=default_name_format, timeout=None)
def _createCallback(self, result):
"""
Callback for Docker container creating.

View File

@@ -245,56 +245,9 @@ class Dynamips(Module):
:param project: Project instance
"""
log.info("instantiating node {}".format(node_class))
# create an instance of the node class
return node_class(self, server, project)
def createNode(self, node, node_name):
"""
Creates a node.
:param node: Node instance
:param node_name: Node name
"""
log.info("creating node {}".format(node))
if isinstance(node, Router):
ios_router = None
if node_name:
for ios_key, info in self._ios_routers.items():
if node_name == info["name"]:
ios_router = self._ios_routers[ios_key]
break
if not ios_router:
raise ModuleError("No IOS router for platform {}".format(node.settings()["platform"]))
vm_settings = {}
for setting_name, value in ios_router.items():
if setting_name in node.settings() and setting_name != "name" and value != "" and value is not None:
vm_settings[setting_name] = value
default_name_format = IOS_ROUTER_SETTINGS["default_name_format"]
if ios_router["default_name_format"]:
default_name_format = ios_router["default_name_format"]
# Older GNS3 versions may have the following invalid settings in the VM template
if "console" in vm_settings:
del vm_settings["console"]
if "sensors" in vm_settings:
del vm_settings["sensors"]
if "power_supplies" in vm_settings:
del vm_settings["power_supplies"]
ram = vm_settings.pop("ram")
image = vm_settings.pop("image", None)
if image is None:
raise ModuleError("No IOS image has been associated with this IOS router")
node.create(image, ram, additional_settings=vm_settings, default_name_format=default_name_format)
else:
node.create()
def updateImageIdlepc(self, image_path, idlepc):
"""
Updates the Idle-PC for an IOS image.
@@ -307,7 +260,7 @@ class Dynamips(Module):
if os.path.basename(ios_router["image"]) == image_path:
if ios_router["idlepc"] != idlepc:
ios_router["idlepc"] = idlepc
log.info("Idle-PC value {} saved into '{}' template".format(idlepc, ios_router["name"]))
log.debug("Idle-PC value {} saved into '{}' template".format(idlepc, ios_router["name"]))
self._saveIOSRouters()
def reset(self):
@@ -317,28 +270,6 @@ class Dynamips(Module):
self._nodes.clear()
def exportConfigs(self, directory):
"""
Exports all configs for all nodes to a directory.
:param directory: destination directory path
"""
for node in self._nodes:
if isinstance(node, Router) and node.initialized():
node.exportConfigToDirectory(directory)
def importConfigs(self, directory):
"""
Imports configs to all nodes from a directory.
:param directory: source directory path
"""
for node in self._nodes:
if isinstance(node, Router) and node.initialized():
node.importConfigFromDirectory(directory)
def findAlternativeIOSImage(self, image, node):
"""
Tries to find an alternative IOS image.

View File

@@ -21,17 +21,13 @@ Wizard for IOS routers.
import os
import re
import uuid
from gns3.qt import QtCore, QtGui, QtWidgets, qslot
from gns3.node import Node
from gns3.topology import Topology
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 gns3.dialogs.vm_with_images_wizard import VMWithImagesWizard
from gns3.compute_manager import ComputeManager
from gns3.controller import Controller
from ..ui.ios_router_wizard_ui import Ui_IOSRouterWizard
from ..settings import PLATFORMS_DEFAULT_RAM, PLATFORMS_DEFAULT_NVRAM, CHASSIS, ADAPTER_MATRIX, WIC_MATRIX
@@ -92,8 +88,8 @@ class IOSRouterWizard(VMWithImagesWizard, Ui_IOSRouterWizard):
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_etherswitch_startup_config_template = get_resource(os.path.join("configs", "ios_etherswitch_startup-config.txt"))
self._base_startup_config_template = "ios_base_startup-config.txt"
self._base_etherswitch_startup_config_template = "ios_etherswitch_startup-config.txt"
# FIXME: hide because of issue on Windows.
self.uiTestIOSImagePushButton.hide()
@@ -215,36 +211,20 @@ class IOSRouterWizard(VMWithImagesWizard, Ui_IOSRouterWizard):
self._idle_valid = False
self.uiIdlepcLineEdit.setStyleSheet('QLineEdit { background-color: %s }' % color)
def _idlePCFinderSlot(self):
def _idlePCFinderSlot(self, ):
"""
Slot for the idle-PC finder.
"""
if Topology.instance().project() is None:
project = Topology.instance().createLoadProject({"project_name": str(uuid.uuid4())})
project.project_updated_signal.connect(self._projectCreatedSlot)
else:
self._projectCreatedSlot()
@qslot
def _projectCreatedSlot(self, *args):
if Topology.instance().project() is None:
return
try:
Topology.instance().project().project_updated_signal.disconnect(self._projectCreatedSlot)
self._project_created = True
except TypeError:
pass # If the slot is not connected (project already created)
module = Dynamips.instance()
image = self.uiIOSImageLineEdit.text()
platform = self.uiPlatformComboBox.currentText()
ios_image = self.uiIOSImageLineEdit.text()
ram = self.uiRamSpinBox.value()
router_class = PLATFORM_TO_CLASS[platform]
self._router = router_class(module, ComputeManager.instance().getCompute(self._compute_id), Topology.instance().project())
self._router.create(ios_image, ram, name="AUTOIDLEPC")
self._router.created_signal.connect(self.createdSlot)
self._router.server_error_signal.connect(self.serverErrorSlot)
Controller.instance().postCompute("/autoidlepc",
self._compute_id,
self._computeAutoIdlepcCallback,
timeout=None,
body={
"image": image,
"platform": platform
})
self.uiIdlePCFinderPushButton.setEnabled(False)
def _etherSwitchSlot(self, state):
@@ -261,15 +241,6 @@ class IOSRouterWizard(VMWithImagesWizard, Ui_IOSRouterWizard):
self.uiNameLineEdit.setText(self.uiPlatformComboBox.currentText())
# self.uiNameLineEdit.setEnabled(True)
def createdSlot(self, base_node_id):
"""
The node for the auto Idle-PC has been created.
:param base_node_id: not used
"""
self._router.computeAutoIdlepc(self._computeAutoIdlepcCallback)
def serverErrorSlot(self, base_node_id, message):
"""
The auto idle-pc node could not be created.
@@ -289,13 +260,6 @@ class IOSRouterWizard(VMWithImagesWizard, Ui_IOSRouterWizard):
:param error: indicates an error (boolean)
"""
if self._project_created:
Topology.instance().deleteProject()
self._project_created = False
self._router = None
elif self._router:
self._router.delete()
self._router = None
if error:
QtWidgets.QMessageBox.critical(self, "Idle-PC finder", "Error: {}".format(result["message"]))
else:
@@ -420,7 +384,7 @@ class IOSRouterWizard(VMWithImagesWizard, Ui_IOSRouterWizard):
settings = {
"name": self.uiNameLineEdit.text(),
"image": image,
"startup_config": get_default_base_config(self._base_startup_config_template),
"startup_config": self._base_startup_config_template,
"ram": self.uiRamSpinBox.value(),
"nvram": PLATFORMS_DEFAULT_NVRAM[platform],
"idlepc": self.uiIdlepcLineEdit.text(),
@@ -431,7 +395,7 @@ class IOSRouterWizard(VMWithImagesWizard, Ui_IOSRouterWizard):
if self.uiEtherSwitchCheckBox.isChecked():
settings["default_name_format"] = "ESW{0}"
settings["startup_config"] = get_default_base_config(self._base_etherswitch_startup_config_template)
settings["startup_config"] = self._base_etherswitch_startup_config_template
settings["symbol"] = ":/symbols/multilayer_switch.svg"
settings["disk0"] = 1 # adds 1MB disk to store vlan.dat
settings["category"] = Node.switches

View File

@@ -48,7 +48,6 @@ class Router(Node):
def __init__(self, module, server, project, platform="c7200"):
super().__init__(module, server, project)
log.info("Router {} is being created".format(platform))
self._dynamips_id = None
router_settings = {"platform": platform,
@@ -88,48 +87,6 @@ class Router(Node):
self.settings().update(router_settings)
def create(self, image, ram, name=None, node_id=None, dynamips_id=None, additional_settings={}, default_name_format="R{0}"):
"""
Creates this router.
:param image: IOS image path
:param ram: amount of RAM
:param name: optional name for this router
:param node_id: Node identifier on the server
:param dynamips_id: Dynamips identifier on the server
:param additional_settings: other additional and not mandatory settings
"""
platform = self._settings["platform"]
self._settings["ram"] = ram
self._settings["image"] = image
# Minimum settings to send to the server in order to create a new router
params = {"name": name,
"platform": platform,
"ram": ram,
"image": image}
if dynamips_id:
params["dynamips_id"] = dynamips_id
# push the startup-config
if not node_id and "startup_config" in additional_settings:
base_config_content = self._readBaseConfig(additional_settings["startup_config"])
if base_config_content is not None:
params["startup_config_content"] = base_config_content
del additional_settings["startup_config"]
# push the private-config
if not node_id and "private_config" in additional_settings:
base_config_content = self._readBaseConfig(additional_settings["private_config"])
if base_config_content is not None:
params["private_config_content"] = base_config_content
del additional_settings["private_config"]
params.update(additional_settings)
self._create(name, node_id, params, default_name_format)
def _createCallback(self, result):
"""
Callback for create.
@@ -147,18 +104,6 @@ class Router(Node):
"""
params = {}
if "startup_config" in new_settings:
base_config_content = self._readBaseConfig(new_settings["startup_config"])
if base_config_content is not None:
params["startup_config_content"] = base_config_content
del new_settings["startup_config"]
if "private_config" in new_settings:
if new_settings["private_config"] and os.path.isfile(new_settings["private_config"]):
base_config_content = self._readBaseConfig(new_settings["private_config"])
if base_config_content is not None:
params["private_config_content"] = base_config_content
del new_settings["private_config"]
for name, value in new_settings.items():
if name in self._settings:
@@ -181,9 +126,9 @@ class Router(Node):
for name, value in result.items():
if name in self._settings:
if self._settings[name] != value:
log.info("{}: updating {} from '{}' to '{}'".format(self.name(), name, self._settings[name], value))
log.debug("{}: updating {} from '{}' to '{}'".format(self.name(), name, self._settings[name], value))
self._settings[name] = value
elif name not in ("project_id", "port_name_format", "port_segment_size", "first_port_name", "node_directory", "status", "node_id", "width", "height", "compute_id", "node_type", "startup_config_content", "private_config_content", "dynamips_id", "command_line"):
elif name not in ("project_id", "port_name_format", "port_segment_size", "first_port_name", "node_directory", "status", "node_id", "width", "height", "compute_id", "node_type", "dynamips_id", "command_line"):
# All key should be known, but we raise error only in debug
if logging.getLogger().isEnabledFor(logging.DEBUG):
raise ValueError(name)
@@ -369,50 +314,6 @@ class Router(Node):
slot_info = self._slot_info()
return info + slot_info
def exportConfigToDirectory(self, directory):
"""
Exports the startup-config and private-config to a directory.
:param directory: destination directory path
"""
self.controllerHttpGet("/nodes/{node_id}".format(node_id=self._node_id),
self._exportConfigToDirectoryCallback,
context={"directory": directory})
def _exportConfigToDirectoryCallback(self, result, error=False, context={}, **kwargs):
"""
Callback for exportConfigToDirectory.
: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["message"])
else:
result = result["properties"]
directory = context["directory"]
if "startup_config_content" in result:
config_path = os.path.join(directory, normalize_filename(self.name())) + "_startup-config.cfg"
try:
with open(config_path, "wb") as f:
log.info("saving {} startup-config to {}".format(self.name(), config_path))
if result["startup_config_content"]:
f.write(result["startup_config_content"].encode("utf-8"))
except OSError as e:
self.error_signal.emit(self.id(), "Could not export startup-config to {}: {}".format(config_path, e))
if "private_config_content" in result:
config_path = os.path.join(directory, normalize_filename(self.name())) + "_private-config.cfg"
try:
with open(config_path, "wb") as f:
log.info("saving {} private-config to {}".format(self.name(), config_path))
if result["private_config_content"]:
f.write(result["private_config_content"].encode("utf-8"))
except OSError as e:
self.error_signal.emit(self.id(), "Could not export private-config to {}: {}".format(config_path, e))
def configFiles(self):
"""
Name of the configuration files
@@ -422,52 +323,6 @@ class Router(Node):
"configs/i{}_private-config.cfg".format(self._dynamips_id)
]
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.
:param directory: source directory path
"""
try:
contents = os.listdir(directory)
except OSError as e:
return
startup_config = normalize_filename(self.name()) + "_startup-config.cfg"
private_config = normalize_filename(self.name()) + "_private-config.cfg"
new_settings = {}
if startup_config in contents:
new_settings["startup_config"] = os.path.join(directory, startup_config)
if private_config in contents:
new_settings["private_config"] = os.path.join(directory, private_config)
else:
# private-config is optional
log.debug("{}: no private-config file could be found, expected file name: {}".format(self.name(), private_config))
if new_settings:
self.update(new_settings)
def console(self):
"""
Returns the console port for this router.

View File

@@ -26,6 +26,7 @@ from gns3.qt import QtCore, QtGui, QtWidgets
from gns3.local_server import LocalServer
from gns3.dialogs.node_properties_dialog import ConfigurationError
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
from gns3.controller import Controller
from gns3.node import Node
from ..ui.ios_router_configuration_page_ui import Ui_iosRouterConfigPageWidget
from ..settings import CHASSIS, ADAPTER_MATRIX, WIC_MATRIX
@@ -71,6 +72,10 @@ class IOSRouterConfigurationPage(QtWidgets.QWidget, Ui_iosRouterConfigPageWidget
for name, category in Node.defaultCategories().items():
self.uiCategoryComboBox.addItem(name, category)
if Controller.instance().isRemote():
self.uiStartupConfigToolButton.hide()
self.uiPrivateConfigToolButton.hide()
def _idlePCValidateSlot(self):
"""
Slot to validate the entered Idle-PC Value

View File

@@ -24,7 +24,6 @@ import os
import shutil
from gns3.qt import QtWidgets
from gns3.local_server_config import LocalServerConfig
from gns3.local_config import LocalConfig
from ..module import Module
@@ -175,69 +174,9 @@ class IOU(Module):
:param project: Project instance
"""
log.info("instantiating node {}".format(node_class))
# create an instance of the node class
return node_class(self, server, project)
def createNode(self, node, node_name):
"""
Creates a node.
:param node: Node instance
:param node_name: Node name
"""
log.info("creating node {}".format(node))
iouimage = None
if node_name:
for iou_key, info in self._iou_devices.items():
if node_name == info["name"]:
iouimage = iou_key
if not iouimage:
selected_images = []
for image, info in self._iou_devices.items():
if info["server"] == node.compute().id():
selected_images.append(image)
if not selected_images:
raise ModuleError("No IOU image found for this device")
elif len(selected_images) > 1:
from gns3.main_window import MainWindow
mainwindow = MainWindow.instance()
(selection, ok) = QtWidgets.QInputDialog.getItem(mainwindow, "IOU image", "Please choose an image", selected_images, 0, False)
if ok:
iouimage = selection
else:
raise ModuleError("Please select an IOU image")
else:
iouimage = selected_images[0]
vm_settings = {}
for setting_name, value in self._iou_devices[iouimage].items():
if setting_name in node.settings() and setting_name != "name" and value != "" and value is not None:
vm_settings[setting_name] = value
default_name_format = IOU_DEVICE_SETTINGS["default_name_format"]
if self._iou_devices[iouimage]["default_name_format"]:
default_name_format = self._iou_devices[iouimage]["default_name_format"]
if vm_settings["use_default_iou_values"]:
del vm_settings["ram"]
del vm_settings["nvram"]
if "console" in vm_settings:
# Older GNS3 versions may have a console setting in the VM template
del vm_settings["console"]
iou_path = vm_settings.pop("path")
node.create(iou_path, additional_settings=vm_settings, default_name_format=default_name_format)
def reset(self):
"""
Resets the servers.
@@ -245,28 +184,6 @@ class IOU(Module):
self._nodes.clear()
def exportConfigs(self, directory):
"""
Exports all configs for all nodes to a directory.
:param directory: destination directory path
"""
for node in self._nodes:
if node.initialized():
node.exportConfigToDirectory(directory)
def importConfigs(self, directory):
"""
Imports configs to all nodes from a directory.
:param directory: source directory path
"""
for node in self._nodes:
if node.initialized():
node.importConfigFromDirectory(directory)
def findAlternativeIOUImage(self, image):
"""
Tries to find an alternative IOU image

View File

@@ -25,7 +25,6 @@ import sys
from gns3.qt import QtGui, QtWidgets
from gns3.node import Node
from gns3.utils.get_resource import get_resource
from gns3.utils.get_default_base_config import get_default_base_config
from gns3.dialogs.vm_with_images_wizard import VMWithImagesWizard
from gns3.compute_manager import ComputeManager
@@ -63,8 +62,8 @@ class IOUDeviceWizard(VMWithImagesWizard, Ui_IOUDeviceWizard):
self.uiIOUImageLineEdit.textChanged.connect(self._imageLineEditTextChangedSlot)
# location of the base config templates
self._base_iou_l2_config_template = get_resource(os.path.join("configs", "iou_l2_base_startup-config.txt"))
self._base_iou_l3_config_template = get_resource(os.path.join("configs", "iou_l3_base_startup-config.txt"))
self._base_iou_l2_config_template = "iou_l2_base_startup-config.txt"
self._base_iou_l3_config_template = "iou_l3_base_startup-config.txt"
from ..pages.iou_device_preferences_page import IOUDevicePreferencesPage
self.addImageSelector(self.uiExistingImageRadioButton, self.uiIOUImageListComboBox, self.uiIOUImageLineEdit, self.uiIOUImageToolButton, IOUDevicePreferencesPage.getIOUImage)
@@ -113,7 +112,7 @@ class IOUDeviceWizard(VMWithImagesWizard, Ui_IOUDeviceWizard):
startup_config = ""
if self.uiTypeComboBox.currentText() == "L2 image":
# set the default L2 base startup-config
default_base_config = get_default_base_config(self._base_iou_l2_config_template)
default_base_config = self._base_iou_l2_config_template
if default_base_config:
startup_config = default_base_config
symbol = ":/symbols/multilayer_switch.svg"
@@ -122,7 +121,7 @@ class IOUDeviceWizard(VMWithImagesWizard, Ui_IOUDeviceWizard):
serial_adapters = 0
else:
# set the default L3 base startup-config
default_base_config = get_default_base_config(self._base_iou_l3_config_template)
default_base_config = self._base_iou_l3_config_template
if default_base_config:
startup_config = default_base_config
symbol = ":/symbols/router.svg"
@@ -133,7 +132,6 @@ class IOUDeviceWizard(VMWithImagesWizard, Ui_IOUDeviceWizard):
settings = {
"name": self.uiNameLineEdit.text(),
"path": path,
"image": os.path.basename(path),
"startup_config": startup_config,
"ethernet_adapters": ethernet_adapters,
"serial_adapters": serial_adapters,

View File

@@ -23,7 +23,6 @@ import os
import re
from gns3.node import Node
from gns3.utils.normalize_filename import normalize_filename
from gns3.image_manager import ImageManager
from .settings import IOU_DEVICE_SETTINGS
import logging
@@ -45,8 +44,6 @@ class IOUDevice(Node):
def __init__(self, module, server, project):
super().__init__(module, server, project)
log.info("IOU instance is being created")
iou_device_settings = {"path": "",
"md5sum": "",
"startup_config": "",
@@ -62,33 +59,6 @@ class IOUDevice(Node):
self.settings().update(iou_device_settings)
def create(self, iou_path, name=None, node_id=None, additional_settings={}, default_name_format="IOU{0}"):
"""
Creates this IOU device.
:param iou_path: path to an IOU image
:param name: optional name
:param console: optional TCP console port
"""
params = {"path": iou_path}
# push the startup-config
if "startup_config" in additional_settings:
base_config_content = self._readBaseConfig(additional_settings["startup_config"])
if base_config_content is not None:
params["startup_config_content"] = base_config_content
del additional_settings["startup_config"]
# push the startup-config
if "private_config" in additional_settings:
base_config_content = self._readBaseConfig(additional_settings["private_config"])
if base_config_content is not None:
params["private_config_content"] = base_config_content
del additional_settings["private_config"]
params.update(additional_settings)
self._create(name, node_id, params, default_name_format)
def _createCallback(self, result):
"""
Callback for create.
@@ -106,8 +76,6 @@ class IOUDevice(Node):
log.debug("{} is already running".format(self.name()))
return
params = {}
log.debug("{} is starting".format(self.name()))
self.controllerHttpPost("/nodes/{node_id}/start".format(node_id=self._node_id), self._startCallback, progressText="{} is starting".format(self.name()))
@@ -119,18 +87,6 @@ class IOUDevice(Node):
"""
params = {}
if "startup_config" in new_settings:
base_config_content = self._readBaseConfig(new_settings["startup_config"])
if base_config_content is not None:
params["startup_config_content"] = base_config_content
del new_settings["startup_config"]
if "private_config" in new_settings:
base_config_content = self._readBaseConfig(new_settings["private_config"])
if base_config_content is not None:
params["private_config_content"] = base_config_content
del new_settings["private_config"]
for name, value in new_settings.items():
if name in self._settings and self._settings[name] != value:
params[name] = value
@@ -189,95 +145,6 @@ class IOUDevice(Node):
"""
return ["startup-config.cfg", "private-config.cfg"]
def exportConfigToDirectory(self, directory):
"""
Exports the initial-config to a directory.
:param directory: destination directory path
"""
self.controllerHttpGet("/nodes/{node_id}".format(node_id=self._node_id),
self._exportConfigToDirectoryCallback,
context={"directory": directory})
def _exportConfigToDirectoryCallback(self, result, error=False, context={}, **kwargs):
"""
Callback for exportConfigToDirectory.
:param result: server response
:param error: indicates an error (boolean)
"""
if error:
log.error("error while exporting {} IOU configs: {}".format(self.name(), result["message"]))
self.server_error_signal.emit(self.id(), result["message"])
return
export_directory = context["directory"]
result = result["properties"]
if "startup_config_content" in result:
startup_config_path = os.path.join(export_directory, normalize_filename(self.name())) + "_startup-config.cfg"
try:
with open(startup_config_path, "wb") as f:
log.info("saving {} startup-config to {}".format(self.name(), startup_config_path))
if result["startup_config_content"]:
f.write(result["startup_config_content"].encode("utf-8"))
except OSError as e:
self.error_signal.emit(self.id(), "could not export startup-config to {}: {}".format(startup_config_path, e))
if "private_config_content" in result and result["private_config_content"] is not None and len(result["private_config_content"]) > 0:
private_config_path = os.path.join(export_directory, normalize_filename(self.name())) + "_private-config.cfg"
try:
with open(private_config_path, "wb") as f:
log.info("saving {} private-config to {}".format(self.name(), private_config_path))
if result["private_config_content"]:
f.write(result["private_config_content"].encode("utf-8"))
except OSError as e:
self.error_signal.emit(self.id(), "could not export private-config to {}: {}".format(startup_config_path, e))
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 IOU configs from a directory.
:param directory: source directory path
"""
contents = os.listdir(directory)
startup_config = normalize_filename(self.name()) + "_startup-config.cfg"
private_config = normalize_filename(self.name()) + "_private-config.cfg"
new_settings = {}
if startup_config in contents:
new_settings["startup_config"] = os.path.join(directory, startup_config)
if private_config in contents:
new_settings["private_config"] = os.path.join(directory, private_config)
else:
# private-config is optional
log.debug("{}: no private-config file could be found, expected file name: {}".format(self.name(), private_config))
if new_settings:
self.update(new_settings)
def console(self):
"""
Returns the console port for this IOU device.

View File

@@ -26,8 +26,8 @@ from gns3.local_server import LocalServer
from gns3.dialogs.node_properties_dialog import ConfigurationError
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
from gns3.node import Node
from gns3.controller import Controller
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
@@ -49,6 +49,9 @@ class iouDeviceConfigurationPage(QtWidgets.QWidget, Ui_iouDeviceConfigPageWidget
self.uiDefaultValuesCheckBox.stateChanged.connect(self._useDefaultValuesSlot)
self._current_iou_image = ""
self._compute_id = None
if Controller.instance().isRemote():
self.uiStartupConfigToolButton.hide()
self.uiPrivateConfigToolButton.hide()
# location of the base config templates
self._base_iou_l2_config_template = get_resource(os.path.join("configs", "iou_l2_base_startup-config.txt"))
@@ -86,12 +89,12 @@ class iouDeviceConfigurationPage(QtWidgets.QWidget, Ui_iouDeviceConfigPageWidget
if len(self.uiStartupConfigLineEdit.text().strip()) == 0:
if "l2" in path:
# set the default L2 base startup-config
default_base_config = get_default_base_config(self._base_iou_l2_config_template)
default_base_config = self._base_iou_l2_config_template
if default_base_config:
self.uiStartupConfigLineEdit.setText(default_base_config)
else:
# set the default L3 base startup-config
default_base_config = get_default_base_config(self._base_iou_l3_config_template)
default_base_config = self._base_iou_l3_config_template
if default_base_config:
self.uiStartupConfigLineEdit.setText(default_base_config)

View File

@@ -256,9 +256,9 @@ class IOUDevicePreferencesPage(QtWidgets.QWidget, Ui_IOUDevicePreferencesPageWid
QtWidgets.QMessageBox.critical(parent, "IOU image", "Cannot read ELF magic number: {}".format(e))
return
# file must start with the ELF magic number, be 32-bit, little endian and have an ELF version of 1
# normal IOS image are big endian!
if elf_header_start != b'\x7fELF\x01\x01\x01':
# file must start with the ELF magic number, be 32-bit or 64-bit, little endian and have an ELF version of 1
# (normal IOS image are big endian!)
if elf_header_start != b'\x7fELF\x01\x01\x01' and elf_header_start != b'\x7fELF\x02\x01\x01':
QtWidgets.QMessageBox.critical(parent, "IOU image", "Sorry, this is not a valid IOU image!")
return

View File

@@ -70,3 +70,25 @@ class Module(QtCore.QObject):
"""
raise NotImplementedError()
def exportConfigs(self, directory):
"""
Exports all configs for all nodes to a directory.
:param directory: destination directory path
"""
for node in self._nodes:
if hasattr(node, "initialized") and node.initialized():
node.exportConfigToDirectory(directory)
def importConfigs(self, directory):
"""
Imports configs to all nodes from a directory.
:param directory: source directory path
"""
for node in self._nodes:
if hasattr(node, "initialized") and node.initialized():
node.importConfigFromDirectory(directory)

View File

@@ -174,73 +174,9 @@ class Qemu(Module):
:param server: HTTPClient instance
"""
log.info("instantiating node {}".format(node_class))
# create an instance of the node class
return node_class(self, server, project)
def createNode(self, node, node_name):
"""
Creates a node.
:param node: Node instance
:param node_name: Node name
"""
log.info("creating node {} with id {}".format(node, node.id()))
vm = None
if node_name:
for vm_key, info in self._qemu_vms.items():
if node_name == info["name"]:
vm = vm_key
if not vm:
selected_vms = []
for vm, info in self._qemu_vms.items():
if info["server"] == node.compute().id():
selected_vms.append(vm)
if not selected_vms:
raise ModuleError("No QEMU VM on server {}".format(node.server().host()))
elif len(selected_vms) > 1:
from gns3.main_window import MainWindow
mainwindow = MainWindow.instance()
(selection, ok) = QtWidgets.QInputDialog.getItem(mainwindow, "QEMU VM", "Please choose a VM", selected_vms, 0, False)
if ok:
vm = selection
else:
raise ModuleError("Please select a QEMU VM")
else:
vm = selected_vms[0]
vm_settings = {}
for setting_name, value in self._qemu_vms[vm].items():
if setting_name in node.settings() and value != "" and value is not None:
vm_settings[setting_name] = value
qemu_path = vm_settings.pop("qemu_path")
name = vm_settings.pop("name")
port_name_format = self._qemu_vms[vm]["port_name_format"]
port_segment_size = self._qemu_vms[vm]["port_segment_size"]
first_port_name = self._qemu_vms[vm]["first_port_name"]
default_name_format = QEMU_VM_SETTINGS["default_name_format"]
if self._qemu_vms[vm]["default_name_format"]:
default_name_format = self._qemu_vms[vm]["default_name_format"]
if self._qemu_vms[vm]["linked_base"]:
name = default_name_format.replace('{name}', name)
node.create(qemu_path,
name=name,
port_name_format=port_name_format,
port_segment_size=port_segment_size,
first_port_name=first_port_name,
linked_clone=self._qemu_vms[vm]["linked_base"],
additional_settings=vm_settings,
default_name_format=default_name_format)
def reset(self):
"""
Resets the servers.

View File

@@ -332,8 +332,8 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
# set the device name
self.uiNameLineEdit.setText(settings["name"])
if "linked_base" in settings:
self.uiBaseVMCheckBox.setChecked(settings["linked_base"])
if "linked_clone" in settings:
self.uiBaseVMCheckBox.setChecked(settings["linked_clone"])
else:
self.uiBaseVMCheckBox.hide()
@@ -457,8 +457,8 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
else:
settings["name"] = name
if "linked_base" in settings:
settings["linked_base"] = self.uiBaseVMCheckBox.isChecked()
if "linked_clone" in settings:
settings["linked_clone"] = self.uiBaseVMCheckBox.isChecked()
settings["hda_disk_image"] = self.uiHdaDiskImageLineEdit.text().strip()
settings["hdb_disk_image"] = self.uiHdbDiskImageLineEdit.text().strip()

View File

@@ -70,7 +70,7 @@ class QemuVMPreferencesPage(QtWidgets.QWidget, Ui_QemuVMPreferencesPageWidget):
# fill out the General section
section_item = self._createSectionItem("General")
QtWidgets.QTreeWidgetItem(section_item, ["Template name:", qemu_vm["name"]])
if qemu_vm["linked_base"]:
if qemu_vm["linked_clone"]:
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", qemu_vm["default_name_format"]])
try:
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(qemu_vm["server"]).name()])
@@ -79,7 +79,7 @@ class QemuVMPreferencesPage(QtWidgets.QWidget, Ui_QemuVMPreferencesPageWidget):
QtWidgets.QTreeWidgetItem(section_item, ["Console type:", qemu_vm["console_type"]])
QtWidgets.QTreeWidgetItem(section_item, ["CPUs:", str(qemu_vm["cpus"])])
QtWidgets.QTreeWidgetItem(section_item, ["Memory:", "{} MB".format(qemu_vm["ram"])])
QtWidgets.QTreeWidgetItem(section_item, ["Linked base VM:", "{}".format(qemu_vm["linked_base"])])
QtWidgets.QTreeWidgetItem(section_item, ["Linked base VM:", "{}".format(qemu_vm["linked_clone"])])
if qemu_vm["qemu_path"]:
QtWidgets.QTreeWidgetItem(section_item, ["QEMU binary:", os.path.basename(qemu_vm["qemu_path"])])

View File

@@ -41,7 +41,6 @@ class QemuVM(Node):
def __init__(self, module, server, project):
super().__init__(module, server, project)
log.info("QEMU VM instance is being created")
self._linked_clone = True
qemu_vm_settings = {"usage": "",
@@ -88,24 +87,6 @@ class QemuVM(Node):
self.settings().update(qemu_vm_settings)
def create(self, qemu_path, name=None, node_id=None, port_name_format="Ethernet{0}", port_segment_size=0,
first_port_name="", linked_clone=True, additional_settings={}, default_name_format=None):
"""
Creates this QEMU VM.
:param name: optional name
:param node_id: Node identifier
"""
self._linked_clone = linked_clone
params = {"qemu_path": qemu_path,
"linked_clone": linked_clone,
"port_name_format": port_name_format,
"port_segment_size": port_segment_size,
"first_port_name": first_port_name}
params.update(additional_settings)
self._create(name, node_id, params, default_name_format)
def _createCallback(self, result):
"""
Callback for create.

View File

@@ -61,6 +61,6 @@ QEMU_VM_SETTINGS = {
"kernel_image": "",
"initrd": "",
"kernel_command_line": "",
"linked_base": True,
"linked_clone": True,
"server": "local"
}

View File

@@ -24,7 +24,16 @@
<string>General settings</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_4">
<property name="margin">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item row="4" column="0">
@@ -92,6 +101,11 @@
<string>vnc</string>
</property>
</item>
<item>
<property name="text">
<string>spice</string>
</property>
</item>
</widget>
</item>
<item row="8" column="0">
@@ -189,7 +203,16 @@
<string>HDD</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="margin">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item>
@@ -412,7 +435,16 @@
<string>CD/DVD</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_4">
<property name="margin">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item>
@@ -468,7 +500,16 @@
<string>Network</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_5">
<property name="margin">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item row="6" column="1">
@@ -551,7 +592,7 @@
<number>0</number>
</property>
<property name="maximum">
<number>32</number>
<number>275</number>
</property>
</widget>
</item>
@@ -592,7 +633,16 @@
<string>Advanced settings</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="margin">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item>

View File

@@ -1,9 +1,8 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/qemu/ui/qemu_vm_configuration_page.ui'
# Form implementation generated from reading ui file '/Users/noplay/code/gns3/gns3-gui/gns3/modules/qemu/ui/qemu_vm_configuration_page.ui'
#
# Created: Thu Jan 5 14:49:45 2017
# by: PyQt5 UI code generator 5.2.1
# Created by: PyQt5 UI code generator 5.8
#
# WARNING! All changes made in this file will be lost!
@@ -54,6 +53,7 @@ class Ui_QemuVMConfigPageWidget(object):
self.uiConsoleTypeComboBox.setObjectName("uiConsoleTypeComboBox")
self.uiConsoleTypeComboBox.addItem("")
self.uiConsoleTypeComboBox.addItem("")
self.uiConsoleTypeComboBox.addItem("")
self.gridLayout_4.addWidget(self.uiConsoleTypeComboBox, 8, 1, 1, 1)
self.uiConsoleTypeLabel = QtWidgets.QLabel(self.uiGeneralSettingsTab)
self.uiConsoleTypeLabel.setObjectName("uiConsoleTypeLabel")
@@ -286,7 +286,7 @@ class Ui_QemuVMConfigPageWidget(object):
sizePolicy.setHeightForWidth(self.uiAdaptersSpinBox.sizePolicy().hasHeightForWidth())
self.uiAdaptersSpinBox.setSizePolicy(sizePolicy)
self.uiAdaptersSpinBox.setMinimum(0)
self.uiAdaptersSpinBox.setMaximum(32)
self.uiAdaptersSpinBox.setMaximum(275)
self.uiAdaptersSpinBox.setObjectName("uiAdaptersSpinBox")
self.gridLayout_5.addWidget(self.uiAdaptersSpinBox, 0, 1, 1, 1)
self.uiPortNameFormatLineEdit = QtWidgets.QLineEdit(self.uiNetworkTab)
@@ -404,6 +404,10 @@ class Ui_QemuVMConfigPageWidget(object):
self.uiACPIShutdownCheckBox = QtWidgets.QCheckBox(self.groupBox)
self.uiACPIShutdownCheckBox.setObjectName("uiACPIShutdownCheckBox")
self.gridLayout_3.addWidget(self.uiACPIShutdownCheckBox, 2, 0, 1, 2)
self.uiQemuOptionsLineEdit.raise_()
self.uiQemuOptionsLabel.raise_()
self.uiACPIShutdownCheckBox.raise_()
self.uiBaseVMCheckBox.raise_()
self.verticalLayout_2.addWidget(self.groupBox)
spacerItem4 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout_2.addItem(spacerItem4)
@@ -424,6 +428,7 @@ class Ui_QemuVMConfigPageWidget(object):
self.uiSymbolLabel.setText(_translate("QemuVMConfigPageWidget", "Symbol:"))
self.uiConsoleTypeComboBox.setItemText(0, _translate("QemuVMConfigPageWidget", "telnet"))
self.uiConsoleTypeComboBox.setItemText(1, _translate("QemuVMConfigPageWidget", "vnc"))
self.uiConsoleTypeComboBox.setItemText(2, _translate("QemuVMConfigPageWidget", "spice"))
self.uiConsoleTypeLabel.setText(_translate("QemuVMConfigPageWidget", "Console type:"))
self.uiBootPriorityLabel.setText(_translate("QemuVMConfigPageWidget", "Boot priority:"))
self.uiQemuListLabel.setText(_translate("QemuVMConfigPageWidget", "Qemu binary:"))

View File

@@ -185,6 +185,11 @@
<string>vnc</string>
</property>
</item>
<item>
<property name="text">
<string>spice</string>
</property>
</item>
</widget>
</item>
<item>

View File

@@ -1,16 +1,14 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file '/Users/noplay/code/gns3/gns3-gui/gns3/modules/qemu/ui/qemu_vm_wizard.ui'
# Form implementation generated from reading ui file '/home/dominik/projects/gns3-gui/gns3/modules/qemu/ui/qemu_vm_wizard.ui'
#
# Created by: PyQt5 UI code generator 5.6
# Created by: PyQt5 UI code generator 5.8.2
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_QemuVMWizard(object):
def setupUi(self, QemuVMWizard):
QemuVMWizard.setObjectName("QemuVMWizard")
QemuVMWizard.resize(623, 417)
@@ -99,6 +97,7 @@ class Ui_QemuVMWizard(object):
self.uiQemuConsoleTypeComboBox.setObjectName("uiQemuConsoleTypeComboBox")
self.uiQemuConsoleTypeComboBox.addItem("")
self.uiQemuConsoleTypeComboBox.addItem("")
self.uiQemuConsoleTypeComboBox.addItem("")
self.verticalLayout.addWidget(self.uiQemuConsoleTypeComboBox)
self.label = QtWidgets.QLabel(self.uiConsoleTypeWizardPage)
self.label.setObjectName("label")
@@ -234,6 +233,7 @@ class Ui_QemuVMWizard(object):
self.uiConsoleTypeWizardPage.setSubTitle(_translate("QemuVMWizard", "Please choose the console type. Telnet will connect to the serial console of the machine. VNC will connect to graphical output of the machine."))
self.uiQemuConsoleTypeComboBox.setItemText(0, _translate("QemuVMWizard", "telnet"))
self.uiQemuConsoleTypeComboBox.setItemText(1, _translate("QemuVMWizard", "vnc"))
self.uiQemuConsoleTypeComboBox.setItemText(2, _translate("QemuVMWizard", "spice"))
self.label.setText(_translate("QemuVMWizard", "Note: You don\'t need to install anything on the VM itself."))
self.uiDiskWizardPage.setTitle(_translate("QemuVMWizard", "Disk image"))
self.uiDiskWizardPage.setSubTitle(_translate("QemuVMWizard", "Please choose a base disk image for your virtual machine."))
@@ -250,3 +250,4 @@ class Ui_QemuVMWizard(object):
self.uiKernelImageLabel.setText(_translate("QemuVMWizard", "Kernel image (vmlinuz):"))
self.uiKernelImageToolButton.setText(_translate("QemuVMWizard", "&Browse..."))
self.uiInitrdLabel.setText(_translate("QemuVMWizard", "Initial RAM disk (initrd):"))

View File

@@ -216,74 +216,9 @@ class VirtualBox(Module):
:param project: Project instance
"""
log.info("instantiating node {}".format(node_class))
# create an instance of the node class
return node_class(self, server, project)
def createNode(self, node, node_name):
"""
Creates a node.
:param node: Node instance
:param node_name: Node name
"""
log.info("creating node {} with id {}".format(node, node.id()))
vm = None
if node_name:
for vm_key, info in self._virtualbox_vms.items():
if node_name == info["name"]:
vm = vm_key
if not vm:
selected_vms = []
for vm, info in self._virtualbox_vms.items():
if info["server"] == node.compute().id():
selected_vms.append(vm)
if not selected_vms:
raise ModuleError("No VirtualBox VM on server {}".format(node.server().url()))
elif len(selected_vms) > 1:
from gns3.main_window import MainWindow
mainwindow = MainWindow.instance()
(selection, ok) = QtWidgets.QInputDialog.getItem(mainwindow, "VirtualBox VM", "Please choose a VM", selected_vms, 0, False)
if ok:
vm = selection
else:
raise ModuleError("Please select a VirtualBox VM")
else:
vm = selected_vms[0]
vm_settings = {}
for setting_name, value in self._virtualbox_vms[vm].items():
if setting_name != "name" and setting_name in node.settings() and value != "" and value is not None:
vm_settings[setting_name] = value
name = self._virtualbox_vms[vm]["name"]
vmname = self._virtualbox_vms[vm]["vmname"]
port_name_format = self._virtualbox_vms[vm]["port_name_format"]
port_segment_size = self._virtualbox_vms[vm]["port_segment_size"]
first_port_name = self._virtualbox_vms[vm]["first_port_name"]
default_name_format = VBOX_VM_SETTINGS["default_name_format"]
if self._virtualbox_vms[vm]["default_name_format"]:
default_name_format = self._virtualbox_vms[vm]["default_name_format"]
if self._virtualbox_vms[vm]["linked_base"]:
name = default_name_format.replace('{name}', name)
node.create(vmname,
name=name,
port_name_format=port_name_format,
port_segment_size=port_segment_size,
first_port_name=first_port_name,
linked_clone=self._virtualbox_vms[vm]["linked_base"],
additional_settings=vm_settings,
default_name_format=default_name_format)
def reset(self):
"""
Resets the module.

View File

@@ -100,7 +100,7 @@ class VirtualBoxVMWizard(VMWizard, Ui_VirtualBoxVMWizard):
"vmname": vmname,
"server": self._compute_id,
"ram": vminfo["ram"],
"linked_base": self.uiBaseVMCheckBox.isChecked()
"linked_clone": self.uiBaseVMCheckBox.isChecked()
}
return settings

View File

@@ -86,8 +86,8 @@ class VirtualBoxVMConfigurationPage(QtWidgets.QWidget, Ui_virtualBoxVMConfigPage
self.uiNameLabel.hide()
self.uiNameLineEdit.hide()
if "linked_base" in settings:
self.uiBaseVMCheckBox.setChecked(settings["linked_base"])
if "linked_clone" in settings:
self.uiBaseVMCheckBox.setChecked(settings["linked_clone"])
else:
self.uiBaseVMCheckBox.hide()
@@ -163,8 +163,8 @@ class VirtualBoxVMConfigurationPage(QtWidgets.QWidget, Ui_virtualBoxVMConfigPage
else:
settings["name"] = name
if "linked_base" in settings:
settings["linked_base"] = self.uiBaseVMCheckBox.isChecked()
if "linked_clone" in settings:
settings["linked_clone"] = self.uiBaseVMCheckBox.isChecked()
if not node:
# these are template settings

View File

@@ -70,7 +70,7 @@ class VirtualBoxVMPreferencesPage(QtWidgets.QWidget, Ui_VirtualBoxVMPreferencesP
section_item = self._createSectionItem("General")
QtWidgets.QTreeWidgetItem(section_item, ["Template name:", vbox_vm["name"]])
QtWidgets.QTreeWidgetItem(section_item, ["VirtualBox name:", vbox_vm["vmname"]])
if vbox_vm["linked_base"]:
if vbox_vm["linked_clone"]:
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", vbox_vm["default_name_format"]])
QtWidgets.QTreeWidgetItem(section_item, ["RAM:", str(vbox_vm["ram"])])
try:
@@ -79,7 +79,7 @@ class VirtualBoxVMPreferencesPage(QtWidgets.QWidget, Ui_VirtualBoxVMPreferencesP
pass
QtWidgets.QTreeWidgetItem(section_item, ["Headless mode enabled:", "{}".format(vbox_vm["headless"])])
QtWidgets.QTreeWidgetItem(section_item, ["ACPI shutdown enabled:", "{}".format(vbox_vm["acpi_shutdown"])])
QtWidgets.QTreeWidgetItem(section_item, ["Linked base VM:", "{}".format(vbox_vm["linked_base"])])
QtWidgets.QTreeWidgetItem(section_item, ["Linked base VM:", "{}".format(vbox_vm["linked_clone"])])
# fill out the Network section
section_item = self._createSectionItem("Network")

View File

@@ -40,6 +40,6 @@ VBOX_VM_SETTINGS = {
"adapter_type": "Intel PRO/1000 MT Desktop (82540EM)",
"headless": False,
"acpi_shutdown": False,
"linked_base": False,
"linked_clone": False,
"server": "local"
}

View File

@@ -19,11 +19,8 @@
VirtualBox VM implementation.
"""
import sys
import os
import tempfile
from gns3.node import Node
from gns3.utils.bring_to_front import bring_window_to_front_from_process_name
from .settings import VBOX_VM_SETTINGS
import logging
@@ -45,7 +42,6 @@ class VirtualBoxVM(Node):
def __init__(self, module, server, project):
super().__init__(module, server, project)
log.info("VirtualBox VM instance is being created")
self._linked_clone = False
virtualbox_vm_settings = {"vmname": "",
@@ -63,30 +59,6 @@ class VirtualBoxVM(Node):
self.settings().update(virtualbox_vm_settings)
def create(self, vmname, name=None, node_id=None, port_name_format="Ethernet{0}", port_segment_size=0,
first_port_name="", linked_clone=False, additional_settings={}, default_name_format=None):
"""
Creates this VirtualBox VM.
:param vmname: VM name in VirtualBox
:param name: optional name
:param node_id: Node identifier
:param linked_clone: either the VM is a linked clone
:param additional_settings: additional settings for this VM
"""
if not name:
name = vmname
self._linked_clone = linked_clone
params = {"vmname": vmname,
"linked_clone": linked_clone,
"port_name_format": port_name_format,
"port_segment_size": port_segment_size,
"first_port_name": first_port_name}
params.update(additional_settings)
self._create(name, node_id, params, default_name_format)
def _createCallback(self, result):
"""
Callback for create.
@@ -155,6 +127,19 @@ class VirtualBoxVM(Node):
"""
return self._settings["console"]
def bringToFront(self):
"""
Bring the VM window to front.
"""
if self.status() == Node.started:
# try 2 different window title formats
bring_window_to_front_from_process_name("VirtualBox.exe", title="{} [".format(self._settings["vmname"]))
bring_window_to_front_from_process_name("VirtualBox.exe", title="{} (".format(self._settings["vmname"]))
# bring any console to front
return Node.bringToFront(self)
def configPage(self):
"""
Returns the configuration page widget to be used by the node properties dialog.

View File

@@ -23,13 +23,13 @@ import os
import sys
import shutil
import subprocess
import codecs
from gns3.qt import QtWidgets
from gns3.local_server_config import LocalServerConfig
from gns3.local_config import LocalConfig
from collections import OrderedDict
from gns3.modules.module import Module
from gns3.modules.module_error import ModuleError
from gns3.modules.vmware.vmware_vm import VMwareVM
from gns3.modules.vmware.settings import VMWARE_SETTINGS
from gns3.modules.vmware.settings import VMWARE_VM_SETTINGS
@@ -147,6 +147,46 @@ class VMware(Module):
# Workstation is the default
return "ws"
@staticmethod
def parseVMwareFile(path):
"""
Parses a VMware file (VMX, preferences or inventory).
:param path: path to the VMware file
:returns: dict
"""
pairs = OrderedDict()
encoding = "utf-8"
# get the first line to read the .encoding value
with open(path, "rb") as f:
line = f.readline().decode(encoding, errors="ignore")
if line.startswith("#!"):
# skip the shebang
line = f.readline().decode(encoding, errors="ignore")
try:
key, value = line.split('=', 1)
if key.strip().lower() == ".encoding":
file_encoding = value.strip('" ')
try:
codecs.lookup(file_encoding)
encoding = file_encoding
except LookupError:
log.warning("Invalid file encoding detected in '{}': {}".format(path, file_encoding))
except ValueError:
log.warning("Couldn't find file encoding in {}, using {}...".format(path, encoding))
# read the file with the correct encoding
with open(path, encoding=encoding, errors="ignore") as f:
for line in f.read().splitlines():
try:
key, value = line.split('=', 1)
pairs[key.strip().lower()] = value.strip('" ')
except ValueError:
continue
return pairs
def _loadSettings(self):
"""
Loads the settings from the server settings file.
@@ -283,75 +323,9 @@ class VMware(Module):
:param project: Project instance
"""
log.info("instantiating node {}".format(node_class))
# create an instance of the node class
return node_class(self, server, project)
def createNode(self, node, node_name):
"""
Creates a node.
:param node: Node instance
:param node_name: Node name
"""
log.info("creating node {} with id {}".format(node, node.id()))
vm = None
if node_name:
for vm_key, info in self._vmware_vms.items():
if node_name == info["name"]:
vm = vm_key
if not vm:
selected_vms = []
for vm, info in self._vmware_vms.items():
if info["server"] == node.compute().id():
selected_vms.append(vm)
if not selected_vms:
raise ModuleError("No VMware VM on server {}".format(node.server().url()))
elif len(selected_vms) > 1:
from gns3.main_window import MainWindow
mainwindow = MainWindow.instance()
(selection, ok) = QtWidgets.QInputDialog.getItem(mainwindow, "VMware VM", "Please choose a VM", selected_vms, 0, False)
if ok:
vm = selection
else:
raise ModuleError("Please select a VMware VM")
else:
vm = selected_vms[0]
linked_base = self._vmware_vms[vm]["linked_base"]
vm_settings = {}
for setting_name, value in self._vmware_vms[vm].items():
if setting_name in node.settings():
vm_settings[setting_name] = value
vmx_path = vm_settings.pop("vmx_path")
name = vm_settings.pop("name")
port_name_format = self._vmware_vms[vm]["port_name_format"]
port_segment_size = self._vmware_vms[vm]["port_segment_size"]
first_port_name = self._vmware_vms[vm]["first_port_name"]
default_name_format = VMWARE_VM_SETTINGS["default_name_format"]
if self._vmware_vms[vm]["default_name_format"]:
default_name_format = self._vmware_vms[vm]["default_name_format"]
if linked_base:
name = default_name_format.replace('{name}', name)
node.create(vmx_path,
name=name,
port_name_format=port_name_format,
port_segment_size=port_segment_size,
first_port_name=first_port_name,
linked_clone=linked_base,
additional_settings=vm_settings,
default_name_format=default_name_format)
def reset(self):
"""
Resets the module.

View File

@@ -99,7 +99,7 @@ class VMwareVMWizard(VMWizard, Ui_VMwareVMWizard):
"name": vmname,
"server": self._compute_id,
"vmx_path": vminfo["vmx_path"],
"linked_base": self.uiBaseVMCheckBox.isChecked()
"linked_clone": self.uiBaseVMCheckBox.isChecked()
}
return settings

View File

@@ -84,8 +84,8 @@ class VMwareVMConfigurationPage(QtWidgets.QWidget, Ui_VMwareVMConfigPageWidget):
self.uiNameLabel.hide()
self.uiNameLineEdit.hide()
if "linked_base" in settings:
self.uiBaseVMCheckBox.setChecked(settings["linked_base"])
if "linked_clone" in settings:
self.uiBaseVMCheckBox.setChecked(settings["linked_clone"])
else:
self.uiBaseVMCheckBox.hide()
@@ -160,8 +160,8 @@ class VMwareVMConfigurationPage(QtWidgets.QWidget, Ui_VMwareVMConfigPageWidget):
else:
settings["name"] = name
if "linked_base" in settings:
settings["linked_base"] = self.uiBaseVMCheckBox.isChecked()
if "linked_clone" in settings:
settings["linked_clone"] = self.uiBaseVMCheckBox.isChecked()
if not node:
# these are template settings

View File

@@ -69,7 +69,7 @@ class VMwareVMPreferencesPage(QtWidgets.QWidget, Ui_VMwareVMPreferencesPageWidge
# fill out the General section
section_item = self._createSectionItem("General")
QtWidgets.QTreeWidgetItem(section_item, ["Template name:", vmware_vm["name"]])
if vmware_vm["linked_base"]:
if vmware_vm["linked_clone"]:
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", vmware_vm["default_name_format"]])
try:
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(vmware_vm["server"]).name()])
@@ -77,7 +77,7 @@ class VMwareVMPreferencesPage(QtWidgets.QWidget, Ui_VMwareVMPreferencesPageWidge
pass
QtWidgets.QTreeWidgetItem(section_item, ["Headless mode enabled:", "{}".format(vmware_vm["headless"])])
QtWidgets.QTreeWidgetItem(section_item, ["ACPI shutdown enabled:", "{}".format(vmware_vm["acpi_shutdown"])])
QtWidgets.QTreeWidgetItem(section_item, ["Linked base VM:", "{}".format(vmware_vm["linked_base"])])
QtWidgets.QTreeWidgetItem(section_item, ["Linked base VM:", "{}".format(vmware_vm["linked_clone"])])
# fill out the Network section
section_item = self._createSectionItem("Network")

View File

@@ -48,6 +48,6 @@ VMWARE_VM_SETTINGS = {
"use_any_adapter": False,
"headless": False,
"acpi_shutdown": False,
"linked_base": False,
"linked_clone": False,
"server": "local"
}

View File

@@ -19,12 +19,9 @@
VMware VM implementation.
"""
import os
import sys
import tempfile
from gns3.qt import QtCore
from gns3.node import Node
from gns3.utils.bring_to_front import bring_window_to_front_from_process_name
from .settings import VMWARE_VM_SETTINGS
import logging
@@ -47,7 +44,6 @@ class VMwareVM(Node):
def __init__(self, module, server, project):
super().__init__(module, server, project)
log.info("VMware VM instance is being created")
self._linked_clone = False
vmware_vm_settings = {"vmx_path": "",
@@ -64,27 +60,6 @@ class VMwareVM(Node):
self.settings().update(vmware_vm_settings)
def create(self, vmx_path, name=None, node_id=None, port_name_format="Ethernet{0}", port_segment_size=0,
first_port_name="", linked_clone=False, additional_settings={}, default_name_format=None):
"""
Creates this VMware VM.
:param vmx_path: path to the vmx file
:param name: optional name
:param node_id: Node identifier
:param linked_clone: either the VM is a linked clone
:param additional_settings: additional settings for this VM
"""
self._linked_clone = linked_clone
params = {"vmx_path": vmx_path,
"linked_clone": linked_clone,
"port_name_format": port_name_format,
"port_segment_size": port_segment_size,
"first_port_name": first_port_name}
params.update(additional_settings)
self._create(name, node_id, params, default_name_format)
def _createCallback(self, result):
"""
Callback for create.
@@ -175,6 +150,26 @@ class VMwareVM(Node):
return self._settings["console"]
def bringToFront(self):
"""
Bring the VM window to front.
"""
if self.status() == Node.started:
try:
vmx_pairs = self.module().parseVMwareFile(self.settings()["vmx_path"])
except OSError as e:
log.debug("Could not read VMX file: {}".format(e))
return
if "displayname" in vmx_pairs:
window_name = "{} -".format(vmx_pairs["displayname"])
# try for both VMware Player and Workstation
bring_window_to_front_from_process_name("vmplayer.exe", title=window_name)
bring_window_to_front_from_process_name("vmware.exe", title=window_name)
# bring any console to front
return Node.bringToFront(self)
def configPage(self):
"""
Returns the configuration page widget to be used by the node properties dialog.

View File

@@ -23,14 +23,10 @@ import os
import copy
import shutil
from gns3.qt import QtWidgets
from gns3.local_config import LocalConfig
from gns3.local_server_config import LocalServerConfig
from gns3.utils.get_default_base_config import get_default_base_config
from gns3.utils.get_resource import get_resource
from ..module import Module
from ..module_error import ModuleError
from .vpcs_node import VPCSNode
from .settings import VPCS_SETTINGS
from .settings import VPCS_NODES_SETTINGS
@@ -63,9 +59,6 @@ class VPCS(Module):
Loads the settings from the persistent settings file.
"""
# Copy the default base config in the final location
get_default_base_config(get_resource(os.path.join("configs", "vpcs_base_config.txt")))
self._settings = LocalConfig.instance().loadSectionSettings(self.__class__.__name__, VPCS_SETTINGS)
if not os.path.exists(self._settings["vpcs_path"]):
vpcs_path = shutil.which("vpcs")
@@ -167,33 +160,6 @@ class VPCS(Module):
# create an instance of the node class
return node_class(self, server, project)
def createNode(self, node, node_name):
"""
Creates a node.
:param node: Node instance
:param node_name: Node name
"""
log.info("creating node {}".format(node))
if node_name:
for node_key, info in self._vpcs_nodes.items():
if node_name == info["name"]:
vm_settings = {}
for setting_name, value in self._vpcs_nodes[node_key].items():
if setting_name in node.settings() and setting_name != "name" and value != "" and value is not None:
vm_settings[setting_name] = value
node.create(default_name_format=info["default_name_format"], additional_settings=vm_settings)
return
vm_settings = {
"base_script_file": self._settings.get("base_script_file", get_default_base_config(get_resource(os.path.join("configs", "vpcs_base_config.txt"))))
}
node.create(additional_settings=vm_settings)
def reset(self):
"""
Resets the module.
@@ -201,40 +167,6 @@ class VPCS(Module):
self._nodes.clear()
def exportConfigs(self, directory):
"""
Exports all configs for all nodes to a directory.
:param directory: destination directory path
"""
for node in self._nodes:
if node.initialized():
node.exportConfigToDirectory(directory)
def importConfigs(self, directory):
"""
Imports configs to all nodes from a directory.
:param directory: source directory path
"""
for node in self._nodes:
if node.initialized():
node.importConfigFromDirectory(directory)
@staticmethod
def getNodeClass(name):
"""
Returns the object with the corresponding name.
:param name: object name
"""
if name in globals():
return globals()[name]
return None
@staticmethod
def getNodeType(name, platform=None):
if name == "vpcs":
@@ -291,16 +223,6 @@ class VPCS(Module):
"builtin": True
}
)
for node in self._vpcs_nodes.values():
nodes.append(
{"class": VPCSNode.__name__,
"name": node["name"],
"server": node["server"],
"symbol": node["symbol"],
"categories": [node["category"]]
}
)
return nodes
@staticmethod

View File

@@ -19,16 +19,12 @@
Wizard for VPCS nodes.
"""
import os
from gns3.qt import QtGui, QtWidgets
from gns3.node import Node
from gns3.utils.get_resource import get_resource
from gns3.utils.get_default_base_config import get_default_base_config
from gns3.dialogs.vm_wizard import VMWizard
from ..ui.vpcs_node_wizard_ui import Ui_VPCSNodeWizard
from .. import VPCS
class VPCSNodeWizard(VMWizard, Ui_VPCSNodeWizard):
@@ -54,7 +50,7 @@ class VPCSNodeWizard(VMWizard, Ui_VPCSNodeWizard):
"""
settings = {"name": self.uiNameLineEdit.text(),
"base_script_file": get_default_base_config(get_resource(os.path.join("configs", "vpcs_base_config.txt"))),
"base_script_file": "vpcs_base_config.txt",
"symbol": ":/symbols/vpcs_guest.svg",
"category": Node.end_devices,
"server": self._compute_id}

View File

@@ -23,6 +23,7 @@ import os
from gns3.qt import QtWidgets
from gns3.local_server import LocalServer
from gns3.node import Node
from gns3.controller import Controller
from ..ui.vpcs_node_configuration_page_ui import Ui_VPCSNodeConfigPageWidget
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
@@ -42,6 +43,8 @@ class VPCSNodeConfigurationPage(QtWidgets.QWidget, Ui_VPCSNodeConfigPageWidget):
self.uiSymbolToolButton.clicked.connect(self._symbolBrowserSlot)
self.uiScriptFileToolButton.clicked.connect(self._scriptFileBrowserSlot)
self._default_configs_dir = LocalServer.instance().localServerSettings()["configs_path"]
if Controller.instance().isRemote():
self.uiScriptFileToolButton.hide()
# add the categories
for name, category in Node.defaultCategories().items():

View File

@@ -19,9 +19,7 @@
VPCS node implementation.
"""
import os
from gns3.node import Node
from gns3.utils.normalize_filename import normalize_filename
import logging
log = logging.getLogger(__name__)
@@ -41,43 +39,12 @@ class VPCSNode(Node):
def __init__(self, module, server, project):
super().__init__(module, server, project)
log.info("VPCS instance is being created")
vpcs_settings = {"console_host": None,
"startup_script": None,
"startup_script_path": None,
"base_script_file": None,
"console": None}
self.settings().update(vpcs_settings)
def create(self, name=None, node_id=None, additional_settings={}, default_name_format="PC{0}"):
"""
Creates this VPCS node.
:param name: optional name
:param node_id: Node identifier
:param additional_settings: additional settings for this node
"""
params = {}
if "base_script_file" in additional_settings:
if os.path.isfile(additional_settings["base_script_file"]):
base_config_content = self._readBaseConfig(additional_settings["base_script_file"])
if base_config_content is not None:
additional_settings["startup_script"] = base_config_content
del additional_settings["base_script_file"]
if "startup_script_path" in additional_settings:
del additional_settings["startup_script_path"]
# If we have an vm id that mean the VM already exits and we should not send startup_script
if "startup_script" in additional_settings and node_id is not None:
del additional_settings["startup_script"]
params.update(additional_settings)
self._create(name, node_id, params, default_name_format)
def update(self, new_settings):
"""
Updates the settings for this VPCS node.
@@ -85,19 +52,6 @@ class VPCSNode(Node):
:param new_settings: settings dictionary
"""
if "script_file" in new_settings:
if os.path.isfile(new_settings["script_file"]):
base_config_content = self._readBaseConfig(new_settings["script_file"])
if base_config_content is not None:
new_settings["startup_script"] = base_config_content
del new_settings["script_file"]
if "base_script_file" in new_settings:
del new_settings["base_script_file"]
if "startup_script_path" in new_settings:
del new_settings["startup_script_path"]
params = {}
for name, value in new_settings.items():
if name in self._settings and self._settings[name] != value:
@@ -139,74 +93,12 @@ class VPCSNode(Node):
return info + port_info
def exportConfigToDirectory(self, directory):
"""
Exports the script-file to a directory.
:param directory: destination directory path
"""
self.controllerHttpGet("/nodes/{node_id}".format(node_id=self._node_id),
self._exportConfigToDirectoryCallback,
context={"directory": directory})
def _exportConfigToDirectoryCallback(self, result, error=False, context={}, **kwargs):
"""
Callback for exportConfigToDirectory.
: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["message"])
elif "startup_script" in result["properties"]:
export_directory = context["directory"]
config_path = os.path.join(export_directory, normalize_filename(self.name())) + "_startup.vpc"
try:
with open(config_path, "wb") as f:
log.info("saving {} script file to {}".format(self.name(), config_path))
if result["properties"]["startup_script"]:
f.write(result["properties"]["startup_script"].encode("utf-8"))
except OSError as e:
self.error_signal.emit(self.id(), "could not export the script file to {}: {}".format(config_path, e))
def configFiles(self):
"""
Name of the configuration files
"""
return ["startup.vpc"]
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.
:param directory: source directory path
"""
try:
contents = os.listdir(directory)
except OSError as e:
return
script_file = normalize_filename(self.name()) + "_startup.vpc"
new_settings = {}
if script_file in contents:
new_settings["script_file"] = os.path.join(directory, script_file)
else:
return
self.update(new_settings)
def console(self):
"""
Returns the console port for this VPCS node.

View File

@@ -16,13 +16,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import uuid
import pathlib
from gns3.local_server import LocalServer
from gns3.controller import Controller
from gns3.ports.ethernet_port import EthernetPort
from gns3.ports.serial_port import SerialPort
from gns3.utils.bring_to_front import bring_window_to_front_from_title
from gns3.qt import QtGui, QtCore
from .base_node import BaseNode
@@ -72,10 +72,6 @@ class Node(BaseNode):
except OSError as e:
log.erro("Can't write %s: %s", context["path"], str(e))
def creator(self):
return self._creator
def settings(self):
return self._settings
@@ -154,6 +150,8 @@ class Node(BaseNode):
console_type = self.consoleType()
if console_type == "vnc":
return general_settings["vnc_console_command"]
if console_type == "spice":
return general_settings["spice_console_command"]
return general_settings["telnet_console_command"]
def consoleType(self):
@@ -224,66 +222,6 @@ class Node(BaseNode):
return body
def _create(self, name=None, node_id=None, params=None, default_name_format="Node{0}", timeout=120):
"""
Create the node on the controller
"""
self._creator = True
if params is None:
params = {}
if "symbol" in self._settings:
params["symbol"] = self._settings["symbol"]
params["x"] = self._settings["x"]
params["y"] = self._settings["y"]
if "label" in self._settings:
params["label"] = self._settings["label"]
if not name:
# use the default name format if no name is provided
name = default_name_format
params["name"] = name
if node_id is not None:
self._node_id = node_id
body = self._prepareBody(params)
self.controllerHttpPost("/nodes", self.createNodeCallback, body=body, timeout=timeout)
def createNodeCallback(self, result, error=False, **kwargs):
"""
Callback for create.
:param result: server response
:param error: indicates an error (boolean)
:returns: Boolean success or not
"""
if error:
self.server_error_signal.emit(self.id(), "Error while setting up node: {}".format(result["message"]))
self.deleted_signal.emit()
self._module.removeNode(self)
return False
result = self._parseResponse(result)
self._created = True
self._createCallback(result)
if self._loading:
self.loaded_signal.emit()
else:
self.setInitialized(True)
log.info("Node instance {} has been created".format(self.name()))
self.created_signal.emit(self.id())
self._module.addNode(self)
def _createCallback(self, result):
"""
Create callback compatible with the compute api.
"""
pass
def _update(self, params, timeout=60):
"""
Update the node on the controller
@@ -303,7 +241,6 @@ class Node(BaseNode):
"""
if error:
log.error("error while updating {}: {}".format(self.name(), result["message"]))
self.server_error_signal.emit(self.id(), result["message"])
return False
@@ -313,6 +250,27 @@ class Node(BaseNode):
self.updated_signal.emit()
return True
def duplicate(self, x, y, z):
"""
Duplicate the node
"""
body = {
"x": int(x),
"y": int(y),
"z": int(z)
}
self.controllerHttpPost("/nodes/{node_id}/duplicate".format(
node_id=self._node_id),
self._duplicateCallback,
body=body,
timeout=None)
def _duplicateCallback(self, result, error=False, **kwargs):
if error:
if "message" in result:
log.error("Error while duplicating: {}".format(result["message"]))
return
def _parseResponse(self, result):
"""
Parse node object from API
@@ -382,6 +340,37 @@ class Node(BaseNode):
new_port.setStatus(self.status())
self._ports.append(new_port)
def createNodeCallback(self, result, error=False, **kwargs):
"""
Callback for create.
:param result: server response
:param error: indicates an error (boolean)
:returns: Boolean success or not
"""
if error:
self.server_error_signal.emit(self.id(), "Error while setting up node: {}".format(result["message"]))
self.deleted_signal.emit()
self._module.removeNode(self)
return False
result = self._parseResponse(result)
self._created = True
self._createCallback(result)
if self._loading:
self.loaded_signal.emit()
else:
self.setInitialized(True)
self.created_signal.emit(self.id())
self._module.addNode(self)
def _createCallback(self, result):
"""
Create callback compatible with the compute api.
"""
pass
def _updateCallback(self, result):
"""
Update callback compatible with the compute api.
@@ -396,7 +385,6 @@ class Node(BaseNode):
:param skip_controller: True to not delete on the controller (often it's when it's already deleted on the server)
"""
log.info("{} is being deleted".format(self.name()))
if not skip_controller:
self.controllerHttpDelete("/nodes/{node_id}".format(node_id=self._node_id), self._deleteCallback)
else:
@@ -415,7 +403,6 @@ class Node(BaseNode):
log.error("error while deleting {}: {}".format(self.name(), result["message"]))
self.server_error_signal.emit(self.id(), result["message"])
return
log.info("{} has been deleted".format(self.name()))
self.deleted_signal.emit()
self._module.removeNode(self)
@@ -482,7 +469,7 @@ class Node(BaseNode):
def suspend(self):
"""
Suspends this router.
Suspends this node.
"""
if self.status() == Node.suspended:
@@ -525,39 +512,6 @@ class Node(BaseNode):
if error:
log.error("error while reloading {}: {}".format(self.name(), result["message"]))
self.server_error_signal.emit(self.id(), result["message"])
else:
log.info("{} has reloaded".format(self.name()))
def _readBaseConfig(self, config_path):
"""
Returns a base config content.
:param config_path: path to the configuration file.
:returns: config content
"""
if config_path is None or len(config_path.strip()) == 0:
return None
if not os.path.isabs(config_path):
config_path = os.path.join(LocalServer.instance().localServerSettings()["configs_path"], config_path)
if not os.path.isfile(config_path):
return None
try:
with open(config_path, "rb") as f:
log.info("Opening configuration file: {}".format(config_path))
config = f.read().decode("utf-8")
config = config.replace('\r', "")
return config
except OSError as e:
self.error_signal.emit(self.id(), "Could not read configuration file {}: {}".format(config_path, e))
return None
except UnicodeDecodeError as e:
self.error_signal.emit(self.id(), "Invalid configuration file {}: {}".format(config_path, e))
return None
def openConsole(self, command=None, aux=False):
if command is None:
@@ -585,9 +539,24 @@ class Node(BaseNode):
elif console_type == "vnc":
from .vnc_console import vncConsole
vncConsole(self.consoleHost(), console_port, command)
elif console_type == "spice":
from .spice_console import spiceConsole
spiceConsole(self.consoleHost(), console_port, command)
elif console_type == "http" or console_type == "https":
QtGui.QDesktopServices.openUrl(QtCore.QUrl("{console_type}://{host}:{port}{path}".format(console_type=console_type, host=self.consoleHost(), port=console_port, path=self.consoleHttpPath())))
def bringToFront(self):
"""
Bring the console window to front.
"""
if self.status() == Node.started:
if bring_window_to_front_from_title(self.name()):
return True
else:
log.debug("Could not find window title '{}' to bring it to front".format(self.name()))
return False
def setName(self, name):
"""
Set a name for a node.

63
gns3/nodes_dock_widget.py Normal file
View File

@@ -0,0 +1,63 @@
#!/usr/bin/env python
#
# Copyright (C) 2016 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 .qt import QtWidgets
from .settings import NODES_VIEW_SETTINGS
from .local_config import LocalConfig
class NodesDockWidget(QtWidgets.QDockWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._settings = LocalConfig.instance().loadSectionSettings("NodesView", NODES_VIEW_SETTINGS)
def _filterTextChangedSlot(self, text):
self.window().uiNodesView.setCurrentSearch(text)
self.window().uiNodesView.refresh()
def _filterIndexChangedSlot(self, index):
self._settings["nodes_view_filter"] = index
LocalConfig.instance().saveSectionSettings("NodesView", self._settings)
if index == 0:
self.window().uiNodesView.setShowInstalledAppliances(True)
self.window().uiNodesView.setShowBuiltinAvailableAppliances(True)
self.window().uiNodesView.setShowMyAvailableAppliances(True)
elif index == 1:
self.window().uiNodesView.setShowInstalledAppliances(True)
self.window().uiNodesView.setShowBuiltinAvailableAppliances(False)
self.window().uiNodesView.setShowMyAvailableAppliances(False)
elif index == 2:
self.window().uiNodesView.setShowInstalledAppliances(False)
self.window().uiNodesView.setShowBuiltinAvailableAppliances(True)
self.window().uiNodesView.setShowMyAvailableAppliances(True)
else:
self.window().uiNodesView.setShowInstalledAppliances(False)
self.window().uiNodesView.setShowBuiltinAvailableAppliances(False)
self.window().uiNodesView.setShowMyAvailableAppliances(True)
self.window().uiNodesView.refresh()
def populateNodesView(self, category):
if self.window().uiNodesFilterComboBox.currentIndex() != self._settings["nodes_view_filter"]:
self.window().uiNodesFilterComboBox.setCurrentIndex(self._settings["nodes_view_filter"])
self._filterIndexChangedSlot(self._settings["nodes_view_filter"])
self.window().uiNodesFilterComboBox.activated.connect(self._filterIndexChangedSlot)
self.window().uiNodesFilterLineEdit.textChanged.connect(self._filterTextChangedSlot)
self.window().uiNodesView.clear()
text = self.window().uiNodesFilterLineEdit.text().strip().lower()
self.window().uiNodesView.populateNodesView(category, text)

View File

@@ -16,20 +16,31 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Nodes view that list all the available nodes to be dragged and dropped on the QGraphics scene.
Nodes view that list all the available nodes to be dragged and dropped
on the QGraphics scene.
"""
import pickle
import tempfile
import json
import sip
from .qt import QtCore, QtGui, QtWidgets, qpartial
from .modules import MODULES
from .node import Node
from .controller import Controller
from .appliance_manager import ApplianceManager
from .dialogs.configuration_dialog import ConfigurationDialog
from .local_config import LocalConfig
CATEGORY_TO_ID = {
"firewall": 3,
"guest": 2,
"switch": 1,
"multilayer_switch": 1,
"router": 0
}
class NodesView(QtWidgets.QTreeWidget):
"""
@@ -42,40 +53,84 @@ class NodesView(QtWidgets.QTreeWidget):
super().__init__(parent)
self._current_category = None
self._current_search = ""
self._show_installed_appliances = True
self._show_builtin_available_appliances = True
self._show_my_available_appliances = True
# enables the possibility to drag items.
self.setDragEnabled(True)
Controller.instance().connected_signal.connect(self.refresh)
ApplianceManager.instance().appliances_changed_signal.connect(self.refresh)
def setCurrentSearch(self, search):
self._current_search = search
def setShowInstalledAppliances(self, value):
self._show_installed_appliances = value
def setShowBuiltinAvailableAppliances(self, value):
self._show_builtin_available_appliances = value
def setShowMyAvailableAppliances(self, value):
self._show_my_available_appliances = value
def refresh(self):
self.clear()
self.populateNodesView(self._current_category)
self.populateNodesView(self._current_category, self._current_search)
def populateNodesView(self, category):
def populateNodesView(self, category, search):
"""
Populates the nodes view with the device list of the specified
category (None = all devices).
:param category: category of device to list
:param search: filter
"""
if not Controller.instance().connected():
return
self.setIconSize(QtCore.QSize(32, 32))
self._current_category = category
for module in MODULES:
for node in module.instance().nodes():
if category is not None and category not in node["categories"]:
continue
item = QtWidgets.QTreeWidgetItem(self)
item.setText(0, node["name"])
item.setData(0, QtCore.Qt.UserRole, node)
item.setSizeHint(0, QtCore.QSize(32, 32))
Controller.instance().getSymbolIcon(node["symbol"], qpartial(self._setItemIcon, item))
self._current_search = search
if not self.topLevelItemCount() and category == Node.routers:
QtWidgets.QMessageBox.warning(self, 'Routers', 'No routers have been configured.<br>You must provide your own router images in order to use GNS3.<br><br><a href="https://gns3.com/support/docs">Show documentation</a>')
display_appliances = set()
if self._show_installed_appliances:
for appliance in ApplianceManager.instance().appliances():
if category is not None and category != CATEGORY_TO_ID[appliance["category"]]:
continue
if search != "" and search.lower() not in appliance["name"].lower():
continue
display_appliances.add(appliance["name"])
item = QtWidgets.QTreeWidgetItem(self)
item.setText(0, appliance["name"])
item.setData(0, QtCore.Qt.UserRole, appliance["appliance_id"])
item.setData(1, QtCore.Qt.UserRole, "appliance")
item.setSizeHint(0, QtCore.QSize(32, 32))
Controller.instance().getSymbolIcon(appliance.get("symbol"), qpartial(self._setItemIcon, item), fallback=":/symbols/" + appliance["category"] + ".svg")
for appliance in ApplianceManager.instance().appliance_templates():
if not appliance["builtin"] and not self._show_my_available_appliances:
continue
if appliance["builtin"] and not self._show_builtin_available_appliances:
continue
if category is not None and category != CATEGORY_TO_ID[appliance["category"]]:
continue
if search != "" and search.lower() not in appliance["name"].lower():
continue
if appliance["name"] in display_appliances:
continue
item = QtWidgets.QTreeWidgetItem(self)
item.setForeground(0, QtGui.QBrush(QtGui.QColor("gray")))
item.setText(0, appliance["name"])
item.setData(0, QtCore.Qt.UserRole, appliance)
item.setData(1, QtCore.Qt.UserRole, "appliance_template")
item.setSizeHint(0, QtCore.QSize(32, 32))
Controller.instance().getSymbolIcon(appliance.get("symbol"), qpartial(self._setItemIcon, item), fallback=":/symbols/" + appliance["category"] + ".svg")
self.sortByColumn(0, QtCore.Qt.AscendingOrder)
@@ -108,16 +163,25 @@ class NodesView(QtWidgets.QTreeWidget):
# Check that an item has been selected and left button clicked
if self.currentItem() is not None and event.buttons() == QtCore.Qt.LeftButton:
item = self.currentItem()
icon = item.icon(0)
# retrieve the node class from the item data
node = item.data(0, QtCore.Qt.UserRole)
if item.data(1, QtCore.Qt.UserRole) == "appliance_template":
f = tempfile.NamedTemporaryFile(mode="w+", suffix=".builtin.gns3a", delete=False)
json.dump(item.data(0, QtCore.Qt.UserRole), f)
f.close()
self.window().loadPath(f.name)
return
icon = item.icon(0)
mimedata = QtCore.QMimeData()
# pickle the node class, set the Mime type and data
# and start dragging the item.
data = pickle.dumps(node)
mimedata.setData("application/x-gns3-node", data)
if item.data(1, QtCore.Qt.UserRole) == "appliance":
appliance_id = item.data(0, QtCore.Qt.UserRole)
mimedata.setData("application/x-gns3-appliance", appliance_id.encode())
elif item.data(1, QtCore.Qt.UserRole) == "node":
appliance_id = item.data(0, QtCore.Qt.UserRole)
mimedata.setData("application/x-gns3-appliance", appliance_id.encode())
drag = QtGui.QDrag(self)
drag.setMimeData(mimedata)
drag.setPixmap(icon.pixmap(self.iconSize()))
@@ -127,15 +191,17 @@ class NodesView(QtWidgets.QTreeWidget):
def _showContextualMenu(self):
item = self.currentItem()
node = item.data(0, QtCore.Qt.UserRole)
node = ApplianceManager.instance().getAppliance(item.data(0, QtCore.Qt.UserRole))
if not node:
return
for module in MODULES:
node_class = module.getNodeClass(node["class"])
node_class = module.getNodeType(node["node_type"])
if node_class:
break
# We can not edit stuff like EthernetSwitch
# or without config template like VPCS
if "builtin" not in node and hasattr(module, "vmConfigurationPage"):
if not node["builtin"] and hasattr(module, "vmConfigurationPage"):
for vm_key, vm in module.instance().VMs().items():
if vm["name"] == node["name"]:
break

View File

@@ -91,9 +91,9 @@ class PacketCapture:
if link:
if link.capturing():
if self._autostart[link]:
if self._autostart[link] and link not in self._tail_process:
self.startPacketCaptureReader(link)
log.info("Has successfully started capturing packets on {} to {}".format(link.id(), link.capture_file_path()))
log.debug("Has successfully started capturing packets on {} to {}".format(link.id(), link.capture_file_path()))
else:
self.stopPacketCaptureReader(link)
@@ -106,7 +106,7 @@ class PacketCapture:
"""
link.stopCapture()
log.info("Has successfully stopped capturing packets on {}".format(link.id()))
log.debug("Has successfully stopped capturing packets on {}".format(link.id()))
def startPacketCaptureReader(self, link):
"""

View File

@@ -52,11 +52,13 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
self.uiSymbolsPathToolButton.clicked.connect(self._symbolsPathSlot)
self.uiImagesPathToolButton.clicked.connect(self._imagesPathSlot)
self.uiConfigsPathToolButton.clicked.connect(self._configsPathSlot)
self.uiAppliancesPathToolButton.clicked.connect(self._appliancesPathSlot)
self.uiImportConfigurationFilePushButton.clicked.connect(self._importConfigurationFileSlot)
self.uiExportConfigurationFilePushButton.clicked.connect(self._exportConfigurationFileSlot)
self.uiRestoreDefaultsPushButton.clicked.connect(self._restoreDefaultsSlot)
self.uiTelnetConsolePreconfiguredCommandPushButton.clicked.connect(self._telnetConsolePreconfiguredCommandSlot)
self.uiVNCConsolePreconfiguredCommandPushButton.clicked.connect(self._vncConsolePreconfiguredCommandSlot)
self.uiSPICEConsolePreconfiguredCommandPushButton.clicked.connect(self._spiceConsolePreconfiguredCommandSlot)
self.uiDefaultLabelFontPushButton.clicked.connect(self._setDefaultLabelFontSlot)
self.uiDefaultLabelColorPushButton.clicked.connect(self._setDefaultLabelColorSlot)
self.uiBrowseConfigurationPushButton.clicked.connect(self._browseConfigurationDirectorySlot)
@@ -132,6 +134,18 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
self.uiConfigsPathLineEdit.setText(path)
self.uiConfigsPathLineEdit.setCursorPosition(0)
def _appliancesPathSlot(self):
"""
Slot to select the appliances directory path.
"""
local_server = LocalServer.instance().localServerSettings()
directory = local_server["appliances_path"]
path = QtWidgets.QFileDialog.getExistingDirectory(self, "My custom appliances directory", directory, QtWidgets.QFileDialog.ShowDirsOnly)
if path:
self.uiAppliancesPathLineEdit.setText(path)
self.uiAppliancesPathLineEdit.setCursorPosition(0)
def _restoreDefaultsSlot(self):
"""
Slot to restore default settings
@@ -160,6 +174,16 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
if ok:
self.uiVNCConsoleCommandLineEdit.setText(cmd)
def _spiceConsolePreconfiguredCommandSlot(self):
"""
Slot to set a chosen pre-configured SPICE console command.
"""
cmd = self.uiSPICEConsoleCommandLineEdit.text()
(ok, cmd) = ConsoleCommandDialog.getCommand(self, console_type="spice", current=cmd)
if ok:
self.uiSPICEConsoleCommandLineEdit.setText(cmd)
def _importConfigurationFileSlot(self):
"""
Slot to import a configuration file.
@@ -254,7 +278,9 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
self.uiSymbolsPathLineEdit.setText(local_server["symbols_path"])
self.uiImagesPathLineEdit.setText(local_server["images_path"])
self.uiConfigsPathLineEdit.setText(local_server["configs_path"])
self.uiAppliancesPathLineEdit.setText(local_server["appliances_path"])
self.uiStatsCheckBox.setChecked(settings["send_stats"])
self.uiOverlayNotificationsCheckBox.setChecked(settings["overlay_notifications"])
self.uiCrashReportCheckBox.setChecked(local_server["report_errors"])
self.uiCheckForUpdateCheckBox.setChecked(settings["check_for_update"])
self.uiExperimentalFeaturesCheckBox.setChecked(settings["experimental_features"])
@@ -269,6 +295,9 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
self.uiVNCConsoleCommandLineEdit.setText(settings["vnc_console_command"])
self.uiVNCConsoleCommandLineEdit.setCursorPosition(0)
self.uiSPICEConsoleCommandLineEdit.setText(settings["spice_console_command"])
self.uiSPICEConsoleCommandLineEdit.setCursorPosition(0)
self.uiMultiProfilesCheckBox.setChecked(settings["multi_profiles"])
self.uiImageDirectoriesListWidget.clear()
@@ -322,6 +351,7 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
"projects_path": self.uiProjectsPathLineEdit.text(),
"symbols_path": self.uiSymbolsPathLineEdit.text(),
"configs_path": self.uiConfigsPathLineEdit.text(),
"appliances_path": self.uiAppliancesPathLineEdit.text(),
"report_errors": self.uiCrashReportCheckBox.isChecked(),
"additional_images_paths": ":".join(additional_images_paths)}
LocalServer.instance().updateLocalServerSettings(new_local_server_settings)
@@ -331,8 +361,10 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
"experimental_features": self.uiExperimentalFeaturesCheckBox.isChecked(),
"hdpi": self.uiHdpiCheckBox.isChecked(),
"check_for_update": self.uiCheckForUpdateCheckBox.isChecked(),
"overlay_notifications": self.uiOverlayNotificationsCheckBox.isChecked(),
"telnet_console_command": self.uiTelnetConsoleCommandLineEdit.text(),
"vnc_console_command": self.uiVNCConsoleCommandLineEdit.text(),
"spice_console_command": self.uiSPICEConsoleCommandLineEdit.text(),
"delay_console_all": self.uiDelayConsoleAllSpinBox.value(),
"send_stats": self.uiStatsCheckBox.isChecked(),
"multi_profiles": self.uiMultiProfilesCheckBox.isChecked()

View File

@@ -30,11 +30,9 @@ log = logging.getLogger(__name__)
from gns3.qt import QtNetwork, QtWidgets
from ..ui.server_preferences_page_ui import Ui_ServerPreferencesPageWidget
from ..topology import Topology
from ..utils.progress_dialog import ProgressDialog
from ..settings import LOCAL_SERVER_SETTINGS
from ..dialogs.edit_compute_dialog import EditComputeDialog
from ..local_server import LocalServer
from ..local_config import LocalConfig
from ..compute_manager import ComputeManager
@@ -63,10 +61,11 @@ class ServerPreferencesPage(QtWidgets.QWidget, Ui_ServerPreferencesPageWidget):
# load all available addresses
for address in QtNetwork.QNetworkInterface.allAddresses():
if address.protocol() == QtNetwork.QAbstractSocket.IPv4Protocol:
if address.protocol() in [QtNetwork.QAbstractSocket.IPv4Protocol, QtNetwork.QAbstractSocket.IPv6Protocol]:
address_string = address.toString()
self.uiLocalServerHostComboBox.addItem(address_string, address_string)
self.uiLocalServerHostComboBox.addItem("0.0.0.0", "0.0.0.0")
self.uiLocalServerHostComboBox.addItem("::", "::") # all IPv6 addresses
self.uiLocalServerHostComboBox.addItem("0.0.0.0", "0.0.0.0") # all IPv4 addresses
# default is 127.0.0.1
index = self.uiLocalServerHostComboBox.findText("127.0.0.1")
@@ -190,7 +189,6 @@ class ServerPreferencesPage(QtWidgets.QWidget, Ui_ServerPreferencesPageWidget):
self.uiRemoteMainServerPortSpinBox.setValue(servers_settings["port"])
self.uiRemoteMainServerUserLineEdit.setText(servers_settings["user"])
self.uiRemoteMainServerPasswordLineEdit.setText(servers_settings["password"])
self.uiRemoteMainServerProtocolComboBox.setCurrentText(servers_settings["protocol"])
self.uiRemoteMainServerAuthCheckBox.setChecked(servers_settings["auth"])
self.uiLocalServerAutoStartCheckBox.setChecked(servers_settings["auto_start"])
@@ -283,7 +281,7 @@ class ServerPreferencesPage(QtWidgets.QWidget, Ui_ServerPreferencesPageWidget):
else:
new_local_server_settings["host"] = self.uiRemoteMainServerHostLineEdit.text()
new_local_server_settings["port"] = self.uiRemoteMainServerPortSpinBox.value()
new_local_server_settings["protocol"] = self.uiRemoteMainServerProtocolComboBox.currentText()
new_local_server_settings["protocol"] = "http"
new_local_server_settings["user"] = self.uiRemoteMainServerUserLineEdit.text()
new_local_server_settings["password"] = self.uiRemoteMainServerPasswordLineEdit.text()
new_local_server_settings["auth"] = self.uiRemoteMainServerAuthCheckBox.isChecked()

View File

@@ -16,14 +16,16 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
from .qt import QtCore, qpartial, QtWidgets, QtNetwork
import json
from .qt import QtCore, qpartial, QtWidgets, QtNetwork, qslot
from gns3.controller import Controller
from gns3.compute_manager import ComputeManager
from gns3.topology import Topology
from gns3.local_config import LocalConfig
from gns3.settings import GRAPHICS_VIEW_SETTINGS
from gns3.appliance_manager import ApplianceManager
from gns3.utils import parse_version
import logging
log = logging.getLogger(__name__)
@@ -44,6 +46,9 @@ class Project(QtCore.QObject):
project_updated_signal = QtCore.Signal()
# Called when project is fully loaded
project_loaded_signal = QtCore.Signal()
def __init__(self):
self._id = None
@@ -58,6 +63,11 @@ class Project(QtCore.QObject):
graphic_settings = LocalConfig.instance().loadSectionSettings(self.__class__.__name__, GRAPHICS_VIEW_SETTINGS)
self._scene_width = graphic_settings["scene_width"]
self._scene_height = graphic_settings["scene_height"]
self._zoom = graphic_settings.get("zoom", None)
self._show_layers = graphic_settings.get("show_layers", False)
self._snap_to_grid = graphic_settings.get("snap_to_grid", False)
self._show_grid = graphic_settings.get("show_grid", False)
self._show_interface_labels = graphic_settings.get("show_interface_labels", False)
self._name = "untitled"
self._filename = None
@@ -114,6 +124,71 @@ class Project(QtCore.QObject):
def autoStart(self):
return self._auto_start
def setZoom(self, zoom):
"""
Sets zoom factor of the view
"""
self._zoom = zoom
def zoom(self):
"""
Returns zoom factor of project
:return: float or None when not defined
"""
return self._zoom
def setShowLayers(self, show_layers):
"""
Sets show layers mode
"""
self._show_layers = show_layers
def showLayers(self):
"""
Returns if show layers mode is ON
:return: boolean
"""
return self._show_layers
def setSnapToGrid(self, snap_to_grid):
"""
Sets snap to grid mode
"""
self._snap_to_grid = snap_to_grid
def snapToGrid(self):
"""
Returns if snap to grid mode is ON
:return: boolean
"""
return self._snap_to_grid
def setShowGrid(self, show_grid):
"""
Sets show grid mode
"""
self._show_grid = show_grid
def showGrid(self):
"""
Returns if show grid mode is ON
:return: boolean
"""
return self._show_grid
def setShowInterfaceLabels(self, show_interface_labels):
"""
Sets show interface labels mode
"""
self._show_interface_labels = show_interface_labels
def showInterfaceLabels(self):
"""
Returns if show interface labels mode is ON
:return: boolean
"""
return self._show_interface_labels
def setName(self, name):
"""
Set project name
@@ -195,7 +270,7 @@ class Project(QtCore.QObject):
def _duplicateCallback(self, callback, result, error=False, **kwargs):
if error:
if "message" in result:
QtWidgets.QMessageBox.critical(None, "Duplicate project", "Error while duplicate: {}".format(result["message"]))
QtWidgets.QMessageBox.critical(None, "Duplicate project", "Error while duplicating: {}".format(result["message"]))
return
if callback:
callback(result["project_id"])
@@ -312,7 +387,12 @@ class Project(QtCore.QObject):
"auto_close": self._auto_close,
"auto_start": self._auto_start,
"scene_width": self._scene_width,
"scene_height": self._scene_height
"scene_height": self._scene_height,
"zoom": self._zoom,
"show_layers": self._show_layers,
"snap_to_grid": self._snap_to_grid,
"show_grid": self._show_grid,
"show_interface_labels": self._show_interface_labels
}
self.put("", self._projectUpdatedCallback, body=body)
@@ -347,6 +427,11 @@ class Project(QtCore.QObject):
self._auto_close = result.get("auto_close", False)
self._scene_width = result.get("scene_width", 2000)
self._scene_height = result.get("scene_height", 1000)
self._zoom = result.get("zoom", None)
self._show_layers = result.get("show_layers", False)
self._snap_to_grid = result.get("snap_to_grid", False)
self._show_grid = result.get("show_grid", False)
self._show_interface_labels = result.get("show_interface_labels", False)
def load(self, path=None):
if not path:
@@ -397,6 +482,7 @@ class Project(QtCore.QObject):
topo = Topology.instance()
for drawing in result:
topo.createDrawing(drawing)
self.project_loaded_signal.emit()
def close(self, local_server_shutdown=False):
"""Close project"""
@@ -427,7 +513,7 @@ class Project(QtCore.QObject):
log.error("Error while closing project {}: {}".format(self._id, result["message"]))
else:
self.stopListenNotifications()
log.info("Project {} closed".format(self._id))
log.debug("Project {} closed".format(self._id))
self._closed = True
self.project_closed_signal.emit()
@@ -443,13 +529,22 @@ class Project(QtCore.QObject):
def _startListenNotifications(self):
if not Controller.instance().connected():
return
path = "/projects/{project_id}/notifications".format(project_id=self._id)
self._notification_stream = Controller.instance().createHTTPQuery("GET", path, self._endListenNotificationCallback,
downloadProgressCallback=self._event_received,
networkManager=self._notification_network_manager,
timeout=None,
showProgress=False,
ignoreErrors=True)
# Qt websocket before Qt 5.6 doesn't support auth
if parse_version(QtCore.QT_VERSION_STR) < parse_version("5.6.0"):
path = "/projects/{project_id}/notifications".format(project_id=self._id)
self._notification_stream = Controller.instance().createHTTPQuery("GET", path, self._endListenNotificationCallback,
downloadProgressCallback=self._event_received,
networkManager=self._notification_network_manager,
timeout=None,
showProgress=False,
ignoreErrors=True)
else:
path = "/projects/{project_id}/notifications/ws".format(project_id=self._id)
self._notification_stream = Controller.instance().connectWebSocket(path)
self._notification_stream.textMessageReceived.connect(self._websocket_event_received)
self._notification_stream.error.connect(self._websocket_error)
def _endListenNotificationCallback(self, result, error=False, **kwargs):
"""
@@ -459,9 +554,21 @@ class Project(QtCore.QObject):
self._notification_stream = None
self._startListenNotifications()
def _event_received(self, result, server=None, **kwargs):
@qslot
def _websocket_error(self, error):
if self._notification_stream:
log.error(self._notification_stream.errorString())
self._notification_stream = None
self._startListenNotifications()
log.debug("Event received: %s", result)
@qslot
def _websocket_event_received(self, event):
self._event_received(json.loads(event))
def _event_received(self, result, *args, **kwargs):
# Log only relevant events
if result["action"] not in ("ping", "compute.updated"):
log.debug("Event received: %s", result)
if result["action"] == "node.created":
node = Topology.instance().getNodeFromUuid(result["event"]["node_id"])
if node is None:
@@ -515,5 +622,6 @@ class Project(QtCore.QObject):
cm.computeDataReceivedCallback(result["event"])
elif result["action"] == "settings.updated":
LocalConfig.instance().refreshConfigFromController()
ApplianceManager.instance().refresh()
elif result["action"] == "ping":
pass

View File

@@ -46,12 +46,18 @@ try:
except ImportError:
raise SystemExit("Please install the PyQt5.QtSvg module")
try:
from PyQt5 import QtWebSockets
sys.modules[__name__ + '.QtWebSockets'] = QtWebSockets
except ImportError:
raise SystemExit("Please install the PyQt5.QtWebSockets module")
QtCore.Signal = QtCore.pyqtSignal
QtCore.Slot = QtCore.pyqtSlot
QtCore.Property = QtCore.pyqtProperty
from PyQt5.QtWidgets import QFileDialog as OldFileDialog
from PyQt5.QtNetwork import QNetworkAccessManager
# Do not use system proxy because it could be a parental control, virus or "Security software"...
QtNetwork.QNetworkProxyFactory.setUseSystemConfiguration(False)
@@ -155,14 +161,12 @@ if hasattr(sys, '_called_from_test'):
self._instances.add(self)
def connect(self, func, style=None):
log.debug("{caller} connect to signal".format(caller=sys._getframe(1).f_code.co_name))
self._callbacks.add(func)
def disconnect(self, func):
self._callbacks.remove(func)
def emit(self, *args):
log.debug("{caller} emit signal".format(caller=sys._getframe(1).f_code.co_name))
for callback in list(self._callbacks):
callback(*args)
@@ -224,6 +228,31 @@ class StatsQtWidgetsQDialog(QtWidgets.QDialog):
QtWidgets.QDialog = StatsQtWidgetsQDialog
class PatchNetworkAccessManager(QNetworkAccessManager):
"""
Patch the network acces manager in order to solve
hibernation issues on windows and Linux
See: https://github.com/GNS3/gns3-gui/issues/2104
"""
def __init__(self, *params, **kwargs):
super().__init__(*params, **kwargs)
self.setNetworkAccessible(QNetworkAccessManager.Accessible)
self.networkAccessibleChanged.connect(self.networkAccessibleChangedSlot)
def networkAccessibleChangedSlot(self, status):
"""
When we lost the network we switch to another available network
"""
if status == QtNetwork.QNetworkAccessManager.Accessible:
return
self.setConfiguration(QtNetwork.QNetworkConfigurationManager().defaultConfiguration())
QtNetwork.QNetworkAccessManager = PatchNetworkAccessManager
def qpartial(func, *args, **kwargs):
"""
A functools partial that you can use on qobject. If the targeted qobject is

View File

@@ -42,18 +42,20 @@ class QImageSvgRenderer(QtSvg.QSvgRenderer):
def load(self, path_or_data):
try:
if not os.path.exists(path_or_data) and not path_or_data.startswith(":"):
self._svg = path_or_data
path_or_data = path_or_data.encode("utf-8")
return super().load(path_or_data)
except ValueError:
pass # On windows we can get an error because the path is too long (it's the svg data)
path_exists = os.path.exists(path_or_data)
except ValueError: # On windows we can get an error because the path is too long (it's the svg data)
path_exists = False
if not path_exists and not path_or_data.startswith(":"):
self._svg = path_or_data
path_or_data = path_or_data.encode("utf-8")
return super().load(path_or_data)
try:
# We load the SVG with ElementTree before
# because Qt when failing loading send noise to logs
# and their is no way to prevent that
if not path_or_data.startswith(":") and os.path.exists(path_or_data):
if not path_or_data.startswith(":") and path_exists:
ET.parse(path_or_data)
res = super().load(path_or_data)
# If we can't render a SVG we load and base64 the image to create a SVG

View File

@@ -35,15 +35,21 @@ class Appliance(collections.Mapping):
def __init__(self, registry, path):
"""
:params registry: Instance of the registry where images are located
:params path: Path of the appliance file on disk
:params path: Path of the appliance file on disk or file content
"""
self._registry = registry
try:
with open(path, encoding="utf-8") as f:
self._appliance = json.load(f)
except (OSError, ValueError) as e:
raise ApplianceError("Could not read appliance {}: {}".format(os.path.abspath(path), str(e)))
if os.path.isabs(path):
try:
with open(path, encoding="utf-8") as f:
self._appliance = json.load(f)
except (OSError, ValueError) as e:
raise ApplianceError("Could not read appliance {}: {}".format(os.path.abspath(path), str(e)))
else:
try:
self._appliance = json.loads(path)
except ValueError as e:
raise ApplianceError("Could not read appliance {}: {}".format(os.path.abspath(path), str(e)))
self._check_config()
self._resolve_version()
@@ -113,7 +119,7 @@ class Appliance(collections.Mapping):
"""
Duplicate a version in order to create a new version
"""
if len(self._appliance["versions"]) == 0:
if 'versions' not in self._appliance.keys() or len(self._appliance["versions"]) == 0:
raise ApplianceError("Your appliance file doesn't contain any versions")
ref = self._appliance["versions"][0]

View File

@@ -57,6 +57,13 @@ class Config:
"""
return LocalServerConfig.instance().loadSettings("Server", LOCAL_SERVER_SETTINGS)["images_path"]
@property
def appliances_dir(self):
"""
:returns: Location of the images directory on the server
"""
return LocalServerConfig.instance().loadSettings("Server", LOCAL_SERVER_SETTINGS)["appliances_path"]
@property
def symbols_dir(self):
"""
@@ -262,8 +269,8 @@ class Config:
if "port_segment_size" in appliance_config:
new_config["port_segment_size"] = appliance_config["port_segment_size"]
if "linked_base" in appliance_config:
new_config["linked_base"] = appliance_config["linked_base"]
if "linked_clone" in appliance_config:
new_config["linked_clone"] = appliance_config["linked_clone"]
log.debug("Add appliance QEMU: %s", str(new_config))
self._config["Qemu"].setdefault("vms", [])

View File

@@ -95,7 +95,7 @@
"type": "integer",
"title": "Optional port segment size. A port segment is a block of port. For example Ethernet0/0 Ethernet0/1 is the module 0 with a port segment size of 2"
},
"linked_base": {
"linked_clone": {
"type": "boolean",
"title": "False if you don't want to use a single image for all nodes"
},

View File

@@ -36,6 +36,9 @@ DEFAULT_SYMBOLS_PATH = os.path.normpath(os.path.expanduser("~/GNS3/symbols"))
# Default configs directory location
DEFAULT_CONFIGS_PATH = os.path.normpath(os.path.expanduser("~/GNS3/configs"))
# Default appliances location
DEFAULT_APPLIANCES_PATH = os.path.normpath(os.path.expanduser("~/GNS3/appliances"))
DEFAULT_LOCAL_SERVER_HOST = "127.0.0.1"
DEFAULT_LOCAL_SERVER_PORT = 3080
@@ -163,7 +166,8 @@ elif sys.platform.startswith("darwin"):
" -e ' display dialog \"WARNING OSX VNC support is limited if you have trouble connecting to a device please use an alternative client like Chicken of the VNC.\" buttons {\"OK\"} default button 1 with icon caution with title \"GNS3\"'"
" -e ' open location \"vnc://%h:%p\"'"
" -e 'end tell'",
'Chicken of the VNC': "/Applications/Chicken\ of\ the\ VNC.app/Contents/MacOS/Chicken\ of\ the\ VNC %h:%p",
'Chicken of the VNC': "/Applications/Chicken.app/Contents/MacOS/Chicken %h:%p",
'Chicken of the VNC < 2.2': "/Applications/Chicken\ of\ the\ VNC.app/Contents/MacOS/Chicken\ of\ the\ VNC %h:%p",
'Royal TSX': "open 'rtsx://vnc%3A%2F%2F%h:%p'",
}
@@ -180,6 +184,33 @@ else:
# default VNC console command on other systems
DEFAULT_VNC_CONSOLE_COMMAND = PRECONFIGURED_VNC_CONSOLE_COMMANDS['TightVNC']
# Pre-configured SPICE console commands on various OSes
if sys.platform.startswith("win"):
# Windows
PRECONFIGURED_SPICE_CONSOLE_COMMANDS = {
'Remote Viewer (included with GNS3)': '"c:\\Program Files\\VirtViewer v5.0-256\\bin\\remote-viewer.exe" spice://%h:%p',
}
# default Windows SPICE console command
DEFAULT_SPICE_CONSOLE_COMMAND = PRECONFIGURED_SPICE_CONSOLE_COMMANDS['Remote Viewer (included with GNS3)']
elif sys.platform.startswith("darwin"):
# Mac OS X
PRECONFIGURED_SPICE_CONSOLE_COMMANDS = {
'Remote Viewer': '/Applications/RemoteViewer.app/Contents/MacOS/RemoteViewer spice://%h:%p',
}
# default Mac OS X SPICE console command
DEFAULT_SPICE_CONSOLE_COMMAND = PRECONFIGURED_SPICE_CONSOLE_COMMANDS['Remote Viewer']
else:
PRECONFIGURED_SPICE_CONSOLE_COMMANDS = {
'Remote Viewer': 'remote-viewer spice://%h:%p',
}
# default SPICE console command on other systems
DEFAULT_SPICE_CONSOLE_COMMAND = PRECONFIGURED_SPICE_CONSOLE_COMMANDS['Remote Viewer']
# Pre-configured packet capture reader commands on various OSes
WIRESHARK_NORMAL_CAPTURE = "Wireshark Traditional Capture"
WIRESHARK_LIVE_TRAFFIC_CAPTURE = "Wireshark Live Traffic Capture"
@@ -219,12 +250,14 @@ else:
GENERAL_SETTINGS = {
"style": DEFAULT_STYLE,
"check_for_update": True,
"overlay_notifications": True,
"experimental_features": False,
"send_stats": True,
"stats_visitor_id": str(uuid.uuid4()), # An anonymous id for stats
"last_check_for_update": 0,
"telnet_console_command": DEFAULT_TELNET_CONSOLE_COMMAND,
"vnc_console_command": DEFAULT_VNC_CONSOLE_COMMAND,
"spice_console_command": DEFAULT_SPICE_CONSOLE_COMMAND,
"delay_console_all": 500,
"hide_getting_started_dialog": False,
"hide_setup_wizard": False,
@@ -239,6 +272,10 @@ GENERAL_SETTINGS = {
"hdpi": not sys.platform.startswith("linux")
}
NODES_VIEW_SETTINGS = {
"nodes_view_filter": 0,
}
GRAPHICS_VIEW_SETTINGS = {
"scene_width": 2000,
"scene_height": 1000,
@@ -246,6 +283,11 @@ GRAPHICS_VIEW_SETTINGS = {
"draw_link_status_points": True,
"default_label_font": "TypeWriter,10,-1,5,75,0,0,0,0,0",
"default_label_color": "#000000",
"zoom": None,
"show_layers": False,
"snap_to_grid": False,
"show_grid": False,
"show_interface_labels": False
}
LOCAL_SERVER_SETTINGS = {
@@ -255,6 +297,7 @@ LOCAL_SERVER_SETTINGS = {
"port": DEFAULT_LOCAL_SERVER_PORT,
"images_path": DEFAULT_IMAGES_PATH,
"projects_path": DEFAULT_PROJECTS_PATH,
"appliances_path": DEFAULT_APPLIANCES_PATH,
"additional_images_paths": "",
"symbols_path": DEFAULT_SYMBOLS_PATH,
"configs_path": DEFAULT_CONFIGS_PATH,
@@ -281,5 +324,6 @@ PACKET_CAPTURE_SETTINGS = {
CUSTOM_CONSOLE_COMMANDS_SETTINGS = {
"telnet": {},
"vnc": {},
"serial": {}
"serial": {},
"spice": {}
}

63
gns3/spice_console.py Normal file
View File

@@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2017 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/>.
"""
Functions to start SPICE console programs.
"""
import sys
import os
import shlex
import subprocess
import logging
log = logging.getLogger(__name__)
def spiceConsole(host, port, command):
"""
Start a SPICE console program.
:param host: host or IP address
:param port: port number
:param command: command to be executed
"""
if len(command.strip(' ')) == 0:
log.warning('SPICE client is not configured')
return
# ipv6 support
if ":" in host:
host = "[{}]".format(host)
# replace the place-holders by the actual values
command = command.replace("%h", host)
command = command.replace("%p", str(port))
try:
log.info('starting SPICE program "{}"'.format(command))
if sys.platform.startswith("win"):
# use the string on Windows
subprocess.Popen(command)
else:
# use arguments on other platforms
args = shlex.split(command)
subprocess.Popen(args, env=os.environ)
except (OSError, ValueError, subprocess.SubprocessError) as e:
log.warning('could not start SPICE program "{}": {}'.format(command, e))
raise

92
gns3/status_bar.py Normal file
View File

@@ -0,0 +1,92 @@
#!/usr/bin/env python
#
# Copyright (C) 2017 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
from .qt import QtWidgets, QtGui, qslot
class StatusBarHandler(logging.StreamHandler):
def __init__(self, widget):
super().__init__()
self._widget = widget
self.setLevel(logging.WARNING)
def emit(self, record):
if record.levelno > logging.WARNING:
self._widget.addError()
else:
self._widget.addWarning()
class StatusBar(QtWidgets.QStatusBar):
"""
The status bar
"""
def __init__(self, parent, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
self._parent = parent
self._errors = 0
self._warnings = 0
self._errors_button = QtWidgets.QPushButton()
self._errors_button.setFlat(True)
self._errors_button.setIcon(QtGui.QIcon(":/icons/warning.svg"))
self._errors_button.clicked.connect(self._errorButtonPushedSlot)
self.addPermanentWidget(self._errors_button)
self._refresh()
def addError(self):
"""
Increment error count
"""
self._errors += 1
self._refresh()
def addWarning(self):
"""
Increment warning count
"""
self._warnings += 1
self._refresh()
def _refresh(self):
if self._errors == 0 and self._warnings == 0:
self._errors_button.setText("")
self._errors_button.hide()
return
self._errors_button.show()
text = ""
if self._errors == 1:
text += "1 error"
elif self._errors > 1:
text += "{} errors".format(self._errors)
if self._warnings == 1:
text += " 1 warning"
elif self._warnings > 1:
text += " {} warnings".format(self._warnings)
self._errors_button.setText(text)
@qslot
def _errorButtonPushedSlot(self, *args, **kwargs):
self._parent.uiConsoleDockWidget.toggleViewAction().trigger()
self._warnings = 0
self._errors = 0
self._refresh()

View File

@@ -72,6 +72,7 @@ class Style:
self._mw.uiInsertImageAction.setIcon(QtGui.QIcon(":/icons/image.svg"))
self._mw.uiDrawRectangleAction.setIcon(self._getStyleIcon(":/icons/rectangle.svg", ":/icons/rectangle-hover.svg"))
self._mw.uiDrawEllipseAction.setIcon(self._getStyleIcon(":/icons/ellipse.svg", ":/icons/ellipse-hover.svg"))
self._mw.uiDrawLineAction.setIcon(QtGui.QIcon(":/icons/vertically.svg"))
self._mw.uiEditReadmeAction.setIcon(QtGui.QIcon(":/icons/edit.svg"))
self._mw.uiOnlineHelpAction.setIcon(QtGui.QIcon(":/icons/help.svg"))
self._mw.uiBrowseRoutersAction.setIcon(self._getStyleIcon(":/icons/router.png", ":/icons/router-hover.png"))
@@ -100,7 +101,7 @@ class Style:
self._mw.uiPreferencesAction.setIcon(self._getStyleIcon(":/classic_icons/preferences.svg", ":/classic_icons/preferences-hover.svg"))
self._mw.uiZoomInAction.setIcon(self._getStyleIcon(":/classic_icons/zoom-in.svg", ":/classic_icons/zoom-in-hover.svg"))
self._mw.uiZoomOutAction.setIcon(self._getStyleIcon(":/classic_icons/zoom-out.svg", ":/classic_icons/zoom-out-hover.svg"))
self._mw.uiShowPortNamesAction.setIcon(self._getStyleIcon(":/classic_icons/show-interface-names.svg",":/classic_icons/show-interface-names-hover.svg"))
self._mw.uiShowPortNamesAction.setIcon(self._getStyleIcon(":/classic_icons/show-interface-names.svg", ":/classic_icons/show-interface-names-hover.svg"))
self._mw.uiStartAllAction.setIcon(self._getStyleIcon(":/classic_icons/start.svg", ":/classic_icons/start-hover.svg"))
self._mw.uiSuspendAllAction.setIcon(self._getStyleIcon(":/classic_icons/pause.svg", ":/classic_icons/pause-hover.svg"))
self._mw.uiStopAllAction.setIcon(self._getStyleIcon(":/classic_icons/stop.svg", ":/classic_icons/stop-hover.svg"))
@@ -111,6 +112,7 @@ class Style:
self._mw.uiInsertImageAction.setIcon(self._getStyleIcon(":/classic_icons/image.svg", ":/classic_icons/image-hover.svg"))
self._mw.uiDrawRectangleAction.setIcon(self._getStyleIcon(":/classic_icons/rectangle.svg", ":/classic_icons/rectangle-hover.svg"))
self._mw.uiDrawEllipseAction.setIcon(self._getStyleIcon(":/classic_icons/ellipse.svg", ":/classic_icons/ellipse-hover.svg"))
self._mw.uiDrawLineAction.setIcon(self._getStyleIcon(":/classic_icons/line.svg", ":/classic_icons/line-hover.svg"))
self._mw.uiEditReadmeAction.setIcon(self._getStyleIcon(":/classic_icons/edit.svg", ":/classic_icons/edit.svg"))
self._mw.uiOnlineHelpAction.setIcon(self._getStyleIcon(":/classic_icons/help.svg", ":/classic_icons/help-hover.svg"))
self._mw.uiBrowseRoutersAction.setIcon(self._getStyleIcon(":/classic_icons/router.svg", ":/classic_icons/router-hover.svg"))
@@ -161,6 +163,7 @@ class Style:
self._mw.uiInsertImageAction.setIcon(self._getStyleIcon(":/charcoal_icons/image.svg", ":/charcoal_icons/image-hover.svg"))
self._mw.uiDrawRectangleAction.setIcon(self._getStyleIcon(":/charcoal_icons/rectangle.svg", ":/charcoal_icons/rectangle-hover.svg"))
self._mw.uiDrawEllipseAction.setIcon(self._getStyleIcon(":/charcoal_icons/ellipse.svg", ":/charcoal_icons/ellipse-hover.svg"))
self._mw.uiDrawLineAction.setIcon(self._getStyleIcon(":/charcoal_icons/line.svg", ":/charcoal_icons/line-hover.svg"))
self._mw.uiEditReadmeAction.setIcon(self._getStyleIcon(":/charcoal_icons/edit.svg", ":/charcoal_icons/edit.svg"))
self._mw.uiOnlineHelpAction.setIcon(self._getStyleIcon(":/charcoal_icons/help.svg", ":/charcoal_icons/help-hover.svg"))
self._mw.uiBrowseRoutersAction.setIcon(self._getStyleIcon(":/charcoal_icons/router.svg", ":/charcoal_icons/router-hover.svg"))

View File

@@ -33,6 +33,7 @@ log = logging.getLogger(__name__)
console_mutex = QtCore.QMutex()
class ConsoleThread(QtCore.QThread):
consoleError = QtCore.pyqtSignal(str)
@@ -71,6 +72,7 @@ class ConsoleThread(QtCore.QThread):
command = command.replace("%p", str(port))
command = command.replace("%d", self._name)
command = command.replace("%i", self._node.project().id())
command = command.replace("%n", str(self._node.id()))
command = command.replace("%c", Controller.instance().httpClient().fullUrl())
# If the console use an apple script we lock to avoid multiple console
@@ -84,7 +86,7 @@ class ConsoleThread(QtCore.QThread):
pass
# log.warning('could not start Telnet console "{}": {}'.format(self._command, e))
finally:
log.info('Telnet console {}:{} closed'.format(host, port))
log.debug('Telnet console {}:{} closed'.format(host, port))
if sys.platform.startswith("darwin") and "osascript" in command:
console_mutex.unlock()
@@ -106,7 +108,7 @@ def nodeTelnetConsole(node, port, command=None):
if not command:
return
log.info('Starting telnet console in thread "{}"'.format(command))
log.debug('Starting telnet console in thread "{}"'.format(command))
console_thread = ConsoleThread(MainWindow.instance(), command, node, port)
console_thread.consoleError.connect(_consoleErrorSlot)
console_thread.start()
@@ -114,5 +116,3 @@ def nodeTelnetConsole(node, port, command=None):
def _consoleErrorSlot(message):
QtWidgets.QMessageBox.critical(MainWindow.instance(), "Error", message)

View File

@@ -126,6 +126,7 @@ class Topology(QtCore.QObject):
if project:
self._project.project_updated_signal.connect(self._projectUpdatedSlot)
self._project.project_creation_error_signal.connect(self._projectCreationErrorSlot)
self._project.project_loaded_signal.connect(self._projectLoadedSlot)
self._main_window.setWindowTitle("{name} - GNS3".format(name=self._project.name()))
self._main_window.uiGraphicsView.setSceneSize(project.sceneWidth(), project.sceneHeight())
else:
@@ -146,6 +147,23 @@ class Topology(QtCore.QObject):
self._main_window.updateRecentProjectsSettings(self._project.id(), self._project.name(), self._project.path())
self._main_window.updateRecentProjectActions()
def _projectLoadedSlot(self):
# when project is loaded we can make updates in GUI
if self._project is not None:
self._main_window.uiShowLayersAction.setChecked(self._project.showLayers())
self._main_window.showLayers(self._project.showLayers())
self._main_window.uiShowGridAction.setChecked(self._project.showGrid())
self._main_window.showGrid(self._project.showGrid())
self._main_window.uiSnapToGridAction.setChecked(self._project.snapToGrid())
self._main_window.snapToGrid(self._project.snapToGrid())
self._main_window.uiShowPortNamesAction.setChecked(self._project.showInterfaceLabels())
self._main_window.showInterfaceLabels(self._project.showInterfaceLabels())
self._main_window.uiGraphicsView.setZoom(self._project.zoom())
def createLoadProject(self, project_settings):
"""
Create load a project based on settings, not on the .gns3
@@ -198,8 +216,10 @@ class Topology(QtCore.QObject):
dialog.exec_()
def _projectCreationErrorSlot(self, message):
self.setProject(None)
QtWidgets.QMessageBox.critical(self._main_window, "New project", message)
if self._project:
self._project.project_creation_error_signal.disconnect(self._projectCreationErrorSlot)
self.setProject(None)
QtWidgets.QMessageBox.critical(self._main_window, "New project", message)
def exportProject(self):
include_image_question = """Would you like to include any base image?
@@ -542,6 +562,8 @@ It is your responsability to check if you have the right to distribute the image
type = "rect"
elif tag == "text":
type = "text"
elif tag == "line":
type = "line"
else:
type = "image"
except IndexError:

View File

@@ -107,6 +107,7 @@ class TopologyNodeItem(QtWidgets.QTreeWidgetItem):
self.takeChildren()
capturing = False
filtering = False
for link in self._node.links():
item = QtWidgets.QTreeWidgetItem()
port = link.getNodePort(self._node)
@@ -115,10 +116,19 @@ class TopologyNodeItem(QtWidgets.QTreeWidgetItem):
if link.capturing():
item.setIcon(0, QtGui.QIcon(':/icons/inspect.svg'))
capturing = True
if len(link.filters()) > 0:
item.setIcon(0, QtGui.QIcon(':/icons/filter.svg'))
filtering = True
if link.capturing() and len(link.filters()) > 0:
item.setIcon(0, QtGui.QIcon(':/icons/filter-capture.svg'))
if link.suspended():
item.setIcon(0, QtGui.QIcon(':/icons/pause.svg'))
self.addChild(item)
if self._parent.show_only_devices_with_capture and capturing is False:
self.setHidden(True)
elif self._parent.show_only_devices_with_filters and filtering is False:
self.setHidden(True)
else:
self.setHidden(False)
@@ -156,6 +166,7 @@ class TopologySummaryView(QtWidgets.QTreeWidget):
self._topology.project_changed_signal.connect(self._projectChangedSlot)
self.itemSelectionChanged.connect(self._itemSelectionChangedSlot)
self.show_only_devices_with_capture = False
self.show_only_devices_with_filters = False
self.setExpandsOnDoubleClick(False)
self.itemDoubleClicked.connect(self._itemDoubleClickedSlot)
@@ -221,8 +232,8 @@ class TopologySummaryView(QtWidgets.QTreeWidget):
elif isinstance(item, LinkItem):
item.setHovered(False)
if not isinstance(current_item, TopologyNodeItem):
port = current_item.data(0, QtCore.Qt.UserRole)
if item.sourcePort() == port or item.destinationPort() == port:
link = current_item.data(0, QtCore.Qt.UserRole)
if item.link() == link:
item.setHovered(True)
@qslot
@@ -239,8 +250,8 @@ class TopologySummaryView(QtWidgets.QTreeWidget):
view.centerOn(item)
elif isinstance(item, LinkItem):
if not isinstance(current_item, TopologyNodeItem):
port = current_item.data(0, QtCore.Qt.UserRole)
if item.sourcePort() == port or item.destinationPort() == port:
link = current_item.data(0, QtCore.Qt.UserRole)
if item.link() == link:
view.centerOn(item)
def mousePressEvent(self, event):
@@ -271,11 +282,17 @@ class TopologySummaryView(QtWidgets.QTreeWidget):
collapse_all.triggered.connect(self._collapseAllSlot)
menu.addAction(collapse_all)
if self.show_only_devices_with_capture is False:
if self.show_only_devices_with_capture is False and self.show_only_devices_with_filters is False:
devices_with_capture = QtWidgets.QAction("Show devices with capture(s)", menu)
devices_with_capture.setIcon(QtGui.QIcon(":/icons/inspect.svg"))
devices_with_capture.triggered.connect(self._devicesWithCaptureSlot)
menu.addAction(devices_with_capture)
devices_with_filters = QtWidgets.QAction("Show devices with packet filter(s)", menu)
devices_with_filters.setIcon(QtGui.QIcon(":/icons/filter.svg"))
devices_with_filters.triggered.connect(self._devicesWithFiltersSlot)
menu.addAction(devices_with_filters)
else:
show_all_devices = QtWidgets.QAction("Show all devices", menu)
# show_all_devices.setIcon(QtGui.QIcon(":/icons/inspect.svg"))
@@ -287,6 +304,11 @@ class TopologySummaryView(QtWidgets.QTreeWidget):
stop_all_captures.triggered.connect(self._stopAllCapturesSlot)
menu.addAction(stop_all_captures)
reset_all_filters = QtWidgets.QAction("Reset all packet filters", menu)
reset_all_filters.setIcon(QtGui.QIcon(":/icons/filter-reset.svg"))
reset_all_filters.triggered.connect(self._resetAllFiltersSlot)
menu.addAction(reset_all_filters)
current_item = self.currentItem()
from .main_window import MainWindow
view = MainWindow.instance().uiGraphicsView
@@ -295,9 +317,9 @@ class TopologySummaryView(QtWidgets.QTreeWidget):
if isinstance(current_item, TopologyNodeItem):
view.populateDeviceContextualMenu(menu)
else:
port = current_item.data(0, QtCore.Qt.UserRole)
link = current_item.data(0, QtCore.Qt.UserRole)
for item in view.scene().items():
if isinstance(item, LinkItem) and (item.sourcePort() == port or item.destinationPort() == port):
if isinstance(item, LinkItem) and item.link() == link:
item.populateLinkContextualMenu(menu)
break
@@ -328,6 +350,15 @@ class TopologySummaryView(QtWidgets.QTreeWidget):
self.show_only_devices_with_capture = True
self.refreshAllLinks()
@qslot
def _devicesWithFiltersSlot(self, *args):
"""
Show only devices with filters.
"""
self.show_only_devices_with_filters = True
self.refreshAllLinks()
@qslot
def _showAllDevicesSlot(self, *args):
"""
@@ -335,6 +366,7 @@ class TopologySummaryView(QtWidgets.QTreeWidget):
"""
self.show_only_devices_with_capture = False
self.show_only_devices_with_filters = False
self.refreshAllLinks()
@qslot
@@ -346,3 +378,15 @@ class TopologySummaryView(QtWidgets.QTreeWidget):
for link in self._topology.links():
if link.capturing():
PacketCapture.instance().stopCapture(link)
@qslot
def _resetAllFiltersSlot(self, *args):
"""
Reset all packet filters
"""
for link in self._topology.links():
if len(link.filters()) > 0:
filters = {}
link.setFilters(filters)
link.update()

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