Compare commits

...

308 Commits

Author SHA1 Message Date
ziajka
25e17d718c Release 2.1.0 2017-11-09 07:29:33 +01:00
ziajka
89108070df Development on v2.1.0dev10 2017-11-07 10:19:46 +01:00
ziajka
f4df3ff9c0 Release v2.1.0rc4 2017-11-07 08:48:00 +01:00
Jeremy Grossmann
90f80b9804 Merge pull request #2322 from GNS3/upload-dialogs-progress
Accurate upload progress dialogs for large files
2017-11-06 20:50:59 +08:00
ziajka
3e86044132 Fix tests 2017-11-06 09:31:39 +01:00
ziajka
78d805cebc Disable showProgress during obtaining endpoint 2017-11-06 09:28:07 +01:00
ziajka
289f754108 Accurate upload progress dialogs for large files 2017-11-03 11:38:04 +01:00
Jeremy Grossmann
1a0c1f826b Merge pull request #2313 from GNS3/disable-direct-file-upload
Disable direct file upload on default
2017-10-30 17:18:36 +07:00
ziajka
9837d661a5 Disable direct file upload on default 2017-10-26 13:41:27 +02:00
grossmj
ff60776769 Merge remote-tracking branch 'origin/2.1' into 2.1 2017-10-26 15:47:02 +07:00
grossmj
7ac442631a Add registry version 5 2017-10-26 15:46:39 +07:00
Jeremy Grossmann
8da2ff3a97 Merge pull request #2310 from GNS3/image-upload-manager
Safe approach to send files to computes and dialogs fixes, Fixes: #2307, Ref: #2188
2017-10-26 13:01:14 +07:00
Jeremy Grossmann
a04d9784f2 Merge branch '2.1' into image-upload-manager 2017-10-26 12:24:17 +07:00
ziajka
623aa4a2de Code style and tests 2017-10-25 16:10:24 +02:00
ziajka
ef3c2afab9 Direct file upload enabled on default 2017-10-25 15:22:39 +02:00
ziajka
73e59d92ca Progress Dialog: don't count finished queries done in background 2017-10-25 13:50:35 +02:00
ziajka
8f3d5bf038 Add debug messages to file upload 2017-10-25 10:27:17 +02:00
ziajka
d638c6e0d7 Image Upload Manager for uploading 2017-10-24 17:20:30 +02:00
grossmj
c5688cacf9 Restore timer for refreshing the progress dialog status. 2017-10-24 20:12:01 +07:00
Jeremy Grossmann
b34bcd6369 Merge pull request #2305 from GNS3/race-nodes-view
Fix race condition on NodesDockWidget, fixes: #2304
2017-10-23 16:10:40 +07:00
ziajka
5e0fc3675f Fix race condition on NodesDockWidget, fixes: #2304 2017-10-23 11:08:33 +02:00
grossmj
e75a21e2ed Do not write an error message when importing non existing config from a directory. Fixes #2296. 2017-10-21 12:30:08 +07:00
grossmj
aeee44e597 Fix bug when replacing Telnet path on OSX. Ref #2274. 2017-10-19 16:32:16 +07:00
ziajka
3cebee64ad Back to development on 2.1.0rc3 2017-10-19 09:56:56 +02:00
ziajka
fd1619cfd3 Fix Travis deploy - urlib3 2017-10-19 09:51:05 +02:00
ziajka
4573d2aed8 Fix Travis deploy - urlib3 2017-10-19 09:47:22 +02:00
ziajka
7f29c497cc Fix Travis deploy 2017-10-19 09:21:57 +02:00
ziajka
6da42b5013 Development on 2.1.0dev9 2017-10-19 08:58:15 +02:00
ziajka
430366947f Release 2.1.0 rc3 2017-10-19 08:56:10 +02:00
grossmj
3870f8ecdc Add debug when using Telnet path on OSX. Ref #2274. 2017-10-18 18:47:24 +07:00
grossmj
f68626e4cc Force to use the telnet client embedded in DMG. Ref #2274. 2017-10-18 17:26:44 +07:00
Jeremy Grossmann
6b4126b688 Merge pull request #2297 from GNS3/direct-file-upload
Direct file upload, Fixes #2264
2017-10-18 00:36:28 +07:00
ziajka
2ef9890dc1 Upload directly to compute 2017-10-17 12:32:38 +02:00
ziajka
100e3dbf27 Load endpoing and execute post image 2017-10-16 09:44:16 +02:00
Jeremy Grossmann
b85db6e24f Merge pull request #2294 from ddragic/qxcb-log-filter
Filter additional QXcbConnection log messages
2017-10-14 15:38:52 +02:00
Dušan Dragić
05d2077b16 Filter additional QXcbConnection log messages 2017-10-14 13:21:55 +02:00
ziajka
357d039434 Preparation to load endpoint before usage 2017-10-13 12:05:31 +02:00
ziajka
0288384c85 Direct file upload settings 2017-10-13 11:22:32 +02:00
grossmj
84347848e9 Do not add missing file extension for screenshot file names on Mac. Fixes #2287. 2017-10-11 04:58:59 +08:00
grossmj
24e5ef885c Add preferred path to use Telnet console in DMG package. Ref #2274. 2017-10-10 23:57:34 +08:00
grossmj
0d8255ecaf Merge remote-tracking branch 'origin/2.1' into 2.1 2017-10-10 22:32:18 +08:00
grossmj
679548e4ad Log Qt messages as info instead of error. Ref #2281. 2017-10-10 22:13:22 +08:00
ziajka
e5384af45d Fix Travis bug with missing twine library. Fixes: #2283 2017-10-06 09:55:43 +02:00
ziajka
ae68d4d84b Development on 2.1.0dev8 2017-10-04 11:40:14 +02:00
ziajka
2ab81816ef Release 2.1.0 rc2 2017-10-04 11:36:51 +02:00
grossmj
4c4241183a Only show "can't get settings from controller" message in debug mode. 2017-10-04 16:24:29 +08:00
grossmj
46406d1e7b Remove explicit Telnet path on OS X. Ref #2274 2017-10-03 04:31:08 +08:00
Jeremy Grossmann
a92573394f Merge pull request #2280 from GNS3/fix-pyqt-version-check
Disable WebSocket notification for lower PyQT version than 5.6. Fixes…
2017-10-02 18:03:31 +02:00
ziajka
6baf628997 Disable WebSocket notification for lower PyQT version than 5.6. Fixes #2272 2017-10-02 10:44:28 +02:00
grossmj
8f9190e094 Increase timeout to 5 minutes when creating and restoring a snapshot. 2017-10-02 05:02:26 +08:00
grossmj
7e14e734b2 Bump version to 2.1.0dev7 2017-10-02 04:04:38 +08:00
grossmj
76fc2f07ce Add more information when a request timeouts. Ref #2277. 2017-10-02 00:46:23 +08:00
grossmj
eee066d5f3 Do not show the progress dialog when moving a node. Ref #2275. 2017-10-02 00:44:58 +08:00
grossmj
9dead47a37 Increase timer before showing a progress dialog from 250ms to 500ms. Ref #2275. 2017-10-01 23:56:28 +08:00
grossmj
a22bd8e9be Use embedded Telnet client on OS X. Ref #2274. 2017-10-01 23:33:12 +08:00
grossmj
a4b897d458 Fix small bug when adding an appliance template and the name already exists. 2017-09-19 16:32:19 +07:00
grossmj
e784f21c0f Use RAW sockets by default on Linux for VMware VM connections. 2017-09-19 12:47:30 +07:00
grossmj
3a000cdc60 Increase timeout to get compute servers from controller. Ref #2269. 2017-09-15 19:40:42 +07:00
grossmj
405f3b3382 Fix "Node doesn't exist" after deletion, but still on the canvas. Fixes #2266. 2017-09-15 17:23:15 +07:00
grossmj
7397f76566 Remove debug test. 2017-09-15 16:24:57 +07:00
grossmj
178cb35d6a Make sure the warning button icon appears in cloud properties dialog on Windows. Fixes #2245. 2017-09-15 16:21:05 +07:00
grossmj
012e5d331d Fix bug when cancelling the importation of a configuration file. Fixes #2260. 2017-09-15 15:52:36 +07:00
ziajka
bd81d36635 Development on v2.1.0dev6 2017-09-13 09:30:56 +02:00
ziajka
234eab57c8 Release 2.1.0rc1 2017-09-13 09:28:55 +02:00
grossmj
e4b19714f4 Fix missing spice console option in appliance template schema. Fixes #2255. 2017-09-13 13:55:30 +07:00
ziajka
7bfba1015b Back to development at 2.1.0dev5 2017-09-05 11:24:08 +02:00
ziajka
498ba2d2b1 Re-release 2.1.0b2 2017-09-05 11:16:02 +02:00
ziajka
f3756b8401 Fix unicode error during appliance tests 2017-09-05 09:56:56 +02:00
ziajka
68f6d37aab Fix link tests 2017-09-05 09:43:33 +02:00
ziajka
6ce35fa5b5 Development on 2.1.0dev5 2017-09-05 08:41:08 +02:00
ziajka
e376753859 Release 2.1.0 beta 2 2017-09-05 08:37:42 +02:00
Jeremy Grossmann
7b03c3eae7 Merge pull request #2250 from GNS3/dont-move-under-layer-0
Disabled possibility of moving items under zero layer (Fixes #2220)
2017-09-01 16:20:04 +07:00
ziajka
902ba42be1 Merge pull request #2252 from GNS3/fix-resources
Fix resources dependencies for cloud configuration page (Fixes: #2251)
2017-09-01 11:06:58 +02:00
ziajka
73fe898eda Fix resources dependencies for cloud configuration page (Fixes: #2251) 2017-09-01 11:05:53 +02:00
ziajka
1ff488d39a Disabled possibility of moving items under zero layer (Fixes #2220) 2017-09-01 10:13:19 +02:00
Jeremy Grossmann
1622a79383 Merge pull request #2248 from GNS3/dialog-warning-fallback
dialog-warning.svg fallback for themed icon (Ref. #2245)
2017-09-01 12:51:38 +07:00
Jeremy Grossmann
1564c63a42 Merge pull request #2247 from GNS3/wide-packet-filters-dialog
Change width of packet filters dialog (Fixes #2244)
2017-09-01 12:50:24 +07:00
ziajka
29f651aaea dialog-warning.svg fallback for themed icon (Ref. #2245) 2017-08-31 11:37:32 +02:00
ziajka
9ee0222339 Change width of packet filters dialog (Fixes #2244) 2017-08-31 09:46:08 +02:00
grossmj
6e1384c985 Fix high CPU usage when using packet filters. Fixes #2240. 2017-08-28 11:40:50 +07:00
Jeremy Grossmann
20190c5816 Merge pull request #2232 from GNS3/toggle-node-menu-item
Toggle Node menu item (Fixes #2227)
2017-08-25 16:32:49 +08:00
ziajka
cab3412ddc Toggle Node menu item (Fixes #2227) 2017-08-22 13:01:50 +02:00
Jeremy Grossmann
6d74ce4070 Merge pull request #2215 from GNS3/fix-qemu-edit-symbol
Fixes loading symbols for QEMU at Edit Page (#2214)
2017-08-10 21:41:58 +08:00
Jeremy Grossmann
159d21af9a Merge pull request #2217 from GNS3/fixes-lineitem
Fixes multiselection styles change crash on LineItem (#2216)
2017-08-10 21:39:42 +08:00
ziajka
713feff11f Fixes multiselection styles change crash on LineItem (#2216) 2017-08-10 09:38:22 +02:00
ziajka
64c5ca712e Fixes loading symbols for QEMU at Edit Page (#2214) 2017-08-10 09:09:07 +02:00
Jeremy Grossmann
1572a6f67f Merge pull request #2212 from GNS3/2211
Fixes exception when right click on Dynamips router in the device dock
2017-08-08 22:16:53 +08:00
ziajka
fcee5c6916 Fixes exception when right click on Dynamips router in the device dock (#2211) 2017-08-08 13:59:47 +02:00
Jeremy Grossmann
d93ad5e9d5 Merge pull request #2209 from CapnCheapo/2.1
Update frame_relay_switch_configuration_page.ui
2017-08-08 11:14:08 +08:00
Stephen C. Moore
1f281a807b Update frame_relay_switch_configuration_page.ui
Fixes #2205
2017-08-07 14:03:00 -05:00
ziajka
2ca250d2c2 Development on 2.1.0dev4 2017-08-04 11:36:47 +02:00
ziajka
b82b031168 Release 2.1.0 beta 1 2017-08-04 11:35:21 +02:00
Julien Duponchelle
c48048f013 Info added to the Nat node
Ref #2197
2017-08-02 13:19:24 +02:00
Julien Duponchelle
9aaca9955a Add missing popup information in cloud and docker node
Fix #2197
2017-08-02 12:14:30 +02:00
Julien Duponchelle
a0e6a82ea2 Handle invalid json in websockets
Fix #2192
2017-08-01 16:32:52 +02:00
Julien Duponchelle
9a3e320e95 Avoid invalid bad request error when receiving partial answer
Fix #2194
2017-08-01 16:29:31 +02:00
Julien Duponchelle
c3fce51493 Catch parse error for broken SVG
Fix #2193
2017-08-01 16:14:08 +02:00
Julien Duponchelle
116cf55758 Filter QXcbConnection log messages
It's Qt noise on Linux we can't do nothing to avoid it.

Fix #2191
2017-08-01 16:14:08 +02:00
Julien Duponchelle
269c6bd0cd Catch class 'PyQt5.QtNetwork.QNetworkReply'> returned a result with an error set
Fix #2195
2017-08-01 16:14:08 +02:00
Julien Duponchelle
31aa612a62 Fix KeyError: 'overlay_notifications'
Fix #2196
2017-08-01 16:14:07 +02:00
ziajka
55f396694f Development on 2.1.0dev3 2017-07-31 11:35:40 +02:00
ziajka
b51fd9c92f Release 2.1.0 alpha 2 2017-07-31 11:31:42 +02:00
Julien Duponchelle
5857d3709b Fix permission error when importing a project on a remote server
Fix #2082
2017-07-27 10:20:28 +02:00
Julien Duponchelle
0c00e1309c Fix RecursionError
Fix #2185
2017-07-26 15:46:40 +02:00
Julien Duponchelle
06dbf9f7d8 Fix 'NodesDockWidget' object has no attribute 'loadPath'
Fix #2182
2017-07-26 14:59:13 +02:00
Julien Duponchelle
ef651d9e9a Fix 'MainWindow' object has no attribute '_settings
Fix #2183
2017-07-26 14:55:13 +02:00
Julien Duponchelle
65dd3a23c6 Fix object has no attribute 'warning_signal'
Fix #2184
2017-07-26 14:53:25 +02:00
Julien Duponchelle
85f697d47b Fix timeout issues when using an appliance 2017-07-26 11:08:39 +02:00
Jeremy Grossmann
0988fdca09 Merge pull request #2180 from GNS3/ubridge_dir
Make sure ubridge path is not a directory
2017-07-25 06:40:36 -07:00
Julien Duponchelle
eba3d5751e Make sure ubridge path is not a directory
Ref https://twitter.com/andreppires/status/889594139800719360
2017-07-25 09:03:57 +02:00
Julien Duponchelle
93e140ae05 2.1.0dev2 2017-07-24 16:19:52 +02:00
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
Julien Duponchelle
31e82bb410 2.0.3 2017-06-13 10:33:13 +02:00
Julien Duponchelle
cab3baf2c6 Cleanup 2017-06-12 16:04:11 +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
574da9c80a Display error when we can't export files
Fix #2097, #2098
2017-06-09 15:06:33 +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
8321883199 Fix auth header not sent is some conditions
Fix #2099
2017-06-09 14:12:38 +02:00
Julien Duponchelle
9608614aa9 Merge branch 'master' into 2.1 2017-06-09 11:39:24 +02:00
Julien Duponchelle
98e4aefa65 If we have auth issue at server startup continue to get better error 2017-06-09 11:37:54 +02:00
Julien Duponchelle
67d816baa3 Do not override IOU configuration file when you change the image
Fix #2091
2017-06-08 16:20:04 +02:00
Julien Duponchelle
13a8bd4500 Fix some PNG loading issues on Windows
Fix #2085
2017-06-08 14:59:47 +02:00
Julien Duponchelle
802b80b764 Handle label with missing elements
Fix #2096
2017-06-07 19:02:38 +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
59292ff6cb Support floating value for font size
Fix #2092
2017-06-07 16:34:05 +02:00
Julien Duponchelle
7810d19f4d Handle partial json in a response
Fix #2093
2017-06-07 14:49:31 +02:00
ziajka
0788ce569f Extended 'Thanks to' section - tab selection on first element 2017-06-06 16:14:53 +02:00
ziajka
4b0379892d Extended section 2017-06-06 15:55:15 +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
8f8994e7df 2.0.3dev1 2017-05-30 09:10:54 +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
365 changed files with 139000 additions and 133498 deletions

3
.gitignore vendored
View File

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

View File

@@ -6,6 +6,9 @@ notifications:
script:
- docker build -t gns3-gui-test .
- docker run gns3-gui-test
before_deploy:
- sudo pip install twine
- sudo pip install urllib3[secure]
deploy:
provider: pypi
user: noplay

102
CHANGELOG
View File

@@ -1,5 +1,107 @@
# Change Log
## 2.1.0 09/11/2017
* Update dynamips binary on OSX
## 2.1.0rc4 07/11/2017
* Accurate upload progress dialogs for large files
* Disable direct file upload on default
* Add registry version 5
* Direct file upload enabled on default
* Progress Dialog: don't count finished queries done in background
* Add debug messages to file upload
* Image Upload Manager for uploading
* Fix race condition on NodesDockWidget, fixes: #2304
* Do not write an error message when importing non existing config from a directory. Fixes #2296.
* Fix bug when replacing Telnet path on OSX. Ref #2274.
* Back to development on 2.1.0rc3
## 2.1.0rc3 19/10/2017
* Add debug when using Telnet path on OSX. Ref #2274.
* Force to use the telnet client embedded in DMG. Ref #2274.
* Upload directly to compute - experimental feature
* Filter additional QXcbConnection log messages
* Do not add missing file extension for screenshot file names on Mac. Fixes #2287.
* Log Qt messages as info instead of error. Ref #2281.
## 2.1.0rc2 04/10/2017
* Only show "can't get settings from controller" message in debug mode.
* Remove explicit Telnet path on OS X. Ref #2274
* Disable WebSocket notification for lower PyQT version than 5.6. Fixes #2272
* Increase timeout to 5 minutes when creating and restoring a snapshot.
* Add more information when a request timeouts. Ref #2277.
* Do not show the progress dialog when moving a node. Ref #2275.
* Increase timer before showing a progress dialog from 250ms to 500ms. Ref #2275.
* Use embedded Telnet client on OS X. Ref #2274.
* Fix small bug when adding an appliance template and the name already exists.
* Use RAW sockets by default on Linux for VMware VM connections.
* Increase timeout to get compute servers from controller. Ref #2269.
* Fix "Node doesn't exist" after deletion, but still on the canvas. Fixes #2266.
* Make sure the warning button icon appears in cloud properties dialog on Windows. Fixes #2245.
* Fix bug when cancelling the importation of a configuration file. Fixes #2260.
## 2.1.0rc1 13/09/2017
* Fix missing spice console option in appliance template schema. Fixes #2255.
## 2.1.0b2 05/09/2017
* Fix resources dependencies for cloud configuration page (Fixes: #2251)
* Disabled possibility of moving items under zero layer (Fixes #2220)
* dialog-warning.svg fallback for themed icon (Ref. #2245)
* Change width of packet filters dialog (Fixes #2244)
* Fix high CPU usage when using packet filters. Fixes #2240.
* Toggle Node menu item (Fixes #2227)
* Fixes multiselection styles change crash on LineItem (#2216)
* Fixes loading symbols for QEMU at Edit Page (#2214)
* Fixes exception when right click on Dynamips router in the device dock (#2211)
* Update frame_relay_switch_configuration_page.ui
## 2.1.0b1 04/08/2017
* Info added to the Nat node
* Add missing popup information in cloud and docker node
* Handle invalid json in websockets
* Avoid invalid bad request error when receiving partial answer
* Catch parse error for broken SVG
* Filter QXcbConnection log messages
* Catch class 'PyQt5.QtNetwork.QNetworkReply'> returned a result with an error set
* Fix KeyError: 'overlay_notifications'
## 2.1.0a2 31/07/2017
* Fix permission error when importing a project on a remote server
* Fix RecursionError
* Fix 'NodesDockWidget' object has no attribute 'loadPath'
* Fix 'MainWindow' object has no attribute '_settings
* Fix object has no attribute 'warning_signal'
* Fix timeout issues when using an appliance
* Make sure ubridge path is not a directory
## 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
* Fix auth header not sent is some conditions
* If we have auth issue at server startup continue to get better error
* Do not override IOU configuration file when you change the image
* Fix some PNG loading issues on Windows
* Handle label with missing elements
* Support floating value for font size
* Handle partial json in a response
* Add Dominik as a new team member
## 2.0.2 30/05/2017
* Show a default symbol in case of corrupted file

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

116
gns3/appliance_manager.py Normal file
View File

@@ -0,0 +1,116 @@
#!/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)
},
timeout=None)
else:
self._controller.post("/projects/" + project.id() + "/appliances/" + appliance_id, self._createNodeFromApplianceCallback, {
"x": int(x),
"y": int(y)
},
timeout=None)
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
@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.error_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:
log.warning("{}: config file '{}' not found".format(self.name(), 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

@@ -58,12 +58,12 @@ class ComputeManager(QtCore.QObject):
if self._controller.connected() and datetime.datetime.now().timestamp() - self._last_computes_refresh > 5:
self._last_computes_refresh = datetime.datetime.now().timestamp()
self._refreshingComputes = True
self._controller.get("/computes", self._listComputesCallback, showProgress=False, timeout=15)
self._controller.get("/computes", self._listComputesCallback, showProgress=False, timeout=30)
def _controllerConnectedSlot(self):
if self._controller.connected():
self._refreshingComputes = True
self._controller.get("/computes", self._listComputesCallback, showProgress=False, timeout=15)
self._controller.get("/computes", self._listComputesCallback, showProgress=False, timeout=30)
def _controllerDisconnectedSlot(self):
for compute_id in list(self._computes):

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

@@ -92,6 +92,12 @@ class Controller(QtCore.QObject):
self._http_client.connection_disconnected_signal.connect(self._httpClientDisconnectedSlot)
self._connectingToServer()
def getHttpClient(self):
"""
:return: Instance of HTTP client to communicate with the server
"""
return self._http_client
def setDisplayError(self, val):
"""
Allow error to be visible or not
@@ -181,6 +187,14 @@ class Controller(QtCore.QObject):
return compute_id
return compute_id
def getEndpoint(self, path, compute_id, *args, **kwargs):
"""
API post on a specific compute
"""
compute_id = self.__fix_compute_id(compute_id)
path = "/computes/endpoint/{}{}".format(compute_id, path)
return self.get(path, *args, **kwargs)
def put(self, *args, **kwargs):
return self.createHTTPQuery("PUT", *args, **kwargs)
@@ -197,6 +211,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 +225,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 +247,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 +274,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://063691a489374eda912ad454a1d80777:5ddb34d6b23c4a08b040efce23aaac78@sentry.io/38506"
DSN = "sync+https://0e7b82c1729b4b08969aabc0153247bf:526f31cf89814a99a39cf4e086711feb@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
@@ -31,6 +32,7 @@ from ..utils.progress_dialog import ProgressDialog
from ..compute_manager import ComputeManager
from ..controller import Controller
from ..local_config import LocalConfig
from ..image_upload_manager import ImageUploadManager
class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
@@ -71,7 +73,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 +139,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 +155,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 +172,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)
@@ -398,7 +420,11 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
except OSError as e:
QtWidgets.QMessageBox.warning(self.parent(), "Add appliance", "Can't access to the image file {}: {}.".format(path, str(e)))
return
image.upload(self._compute_id, callback=self._imageUploadedCallback)
image_upload_manger = ImageUploadManager(
image, Controller.instance(), self._compute_id,
self._imageUploadedCallback, LocalConfig.instance().directFileUpload())
image_upload_manger.upload()
def _getQemuBinariesFromServerCallback(self, result, error=False, **kwargs):
"""
@@ -449,6 +475,8 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
while len(appliance_configuration["name"]) == 0 or not config.is_name_available(appliance_configuration["name"]):
QtWidgets.QMessageBox.warning(self.parent(), "Add appliance", "The name \"{}\" is already used by another appliance".format(appliance_configuration["name"]))
appliance_configuration["name"], ok = QtWidgets.QInputDialog.getText(self.parent(), "Add appliance", "New name:", QtWidgets.QLineEdit.Normal, appliance_configuration["name"])
if not ok:
return False
appliance_configuration["name"] = appliance_configuration["name"].strip()
if "qemu" in appliance_configuration:
@@ -476,7 +504,12 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
for image in appliance_configuration["images"]:
if image["location"] == "local":
image = Image(self._appliance.emulator(), image["path"], filename=image["filename"])
image.upload(self._compute_id, self._applianceImageUploadedCallback)
image_upload_manger = ImageUploadManager(
image, Controller.instance(), self._compute_id,
self._applianceImageUploadedCallback, LocalConfig.instance().directFileUpload())
image_upload_manger.upload()
self._image_uploading_count += 1
def _applianceImageUploadedCallback(self, result, error=False, **kwargs):

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,190 @@
# -*- 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 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().get("overlay_notifications", True):
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

@@ -85,7 +85,7 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
snapshot_name, ok = QtWidgets.QInputDialog.getText(self, "Snapshot", "Snapshot name:", QtWidgets.QLineEdit.Normal, "Unnamed")
if ok and snapshot_name and self._project:
Controller.instance().post("/projects/{}/snapshots".format(self._project.id()), self._createSnapshotsCallback, {"name": snapshot_name})
Controller.instance().post("/projects/{}/snapshots".format(self._project.id()), self._createSnapshotsCallback, {"name": snapshot_name}, timeout=300)
def _createSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
if error:
@@ -131,7 +131,7 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
if reply == QtWidgets.QMessageBox.Cancel:
return
Controller.instance().post("/projects/{}/snapshots/{}/restore".format(self._project.id(), snapshot_id), self._restoreSnapshotsCallback)
Controller.instance().post("/projects/{}/snapshots/{}/restore".format(self._project.id(), snapshot_id), self._restoreSnapshotsCallback, timeout=300)
def _restoreSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
if error:

View File

@@ -21,6 +21,7 @@ Style editor to edit Shape items.
from ..qt import QtCore, QtWidgets, QtGui
from ..ui.style_editor_dialog_ui import Ui_StyleEditorDialog
from ..items.shape_item import ShapeItem
class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
@@ -52,12 +53,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 +109,17 @@ 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)
# on multiselection it's possible to select many type of items
# but brush can be applied only on ShapeItem,
if brush and isinstance(item, ShapeItem):
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:
@@ -122,6 +135,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
self.scene().addItem(item)
super().setEnabled(enabled)
self.toggleUiDeviceMenu()
def reset(self):
"""
Remove all the items from the scene and
@@ -233,6 +248,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,9 +466,17 @@ 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)
self.toggleUiDeviceMenu()
def mouseReleaseEvent(self, event):
"""
Handles all mouse release events.
@@ -462,6 +499,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
item.setSelected(True)
super().mouseReleaseEvent(event)
self.toggleUiDeviceMenu()
def wheelEvent(self, event):
"""
Handles zoom in or out using the mouse wheel.
@@ -474,6 +513,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 +527,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 +596,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 +631,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 +646,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 +658,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 +721,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 +752,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 +813,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 +825,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 +1002,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 +1061,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 +1071,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)
@@ -1091,6 +1146,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
self._import_config_dir,
"All files (*.*);;Config files (*.cfg)",
"Config files (*.cfg)")
if not path:
continue
self._import_config_dir = os.path.dirname(path)
item.node().importFile(config_file, path)
@@ -1138,12 +1195,9 @@ class GraphicsView(QtWidgets.QGraphicsView):
for item in items:
for config_file in item.node().configFiles():
path, ok = QtWidgets.QFileDialog.getSaveFileName(self, "Export file", os.path.join(self._export_configs_to_dir, item.node().name() + "_" + os.path.basename(config_file)), "All files (*.*);;Config files (*.cfg)")
if not path:
continue
self._export_configs_to_dir = os.path.dirname(path)
item.node().exportFile(config_file, path)
def getCommandLineSlot(self):
@@ -1171,6 +1225,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 +1259,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 +1293,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 +1323,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 +1334,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)
@@ -1393,6 +1459,9 @@ class GraphicsView(QtWidgets.QGraphicsView):
elif item.parentItem() is None:
item.delete()
self.scene().clearSelection()
self.toggleUiDeviceMenu()
def allocateCompute(self, node_data, module_instance):
"""
Allocates a server.
@@ -1413,48 +1482,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)
@@ -1470,9 +1509,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
"""
node = Topology.instance().getNode(node_id)
name = "Node"
if node:
if node.name():
name = node.name()
if node and node.name():
name = node.name()
if self._main_window and not sip.isdeleted(self._main_window):
QtWidgets.QMessageBox.critical(self._main_window, name, message.strip())
@@ -1482,6 +1520,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":
@@ -1513,3 +1553,11 @@ class GraphicsView(QtWidgets.QGraphicsView):
painter.drawLine(rect.left(), y, rect.right(), y)
y += gridSize
painter.restore()
def toggleUiDeviceMenu(self):
""" Hook which enables/disables uiDeviceMenu based on the current items selection"""
items = self.scene().selectedItems()
if len(items) > 0:
self._main_window.uiDeviceMenu.setEnabled(True)
else:
self._main_window.uiDeviceMenu.setEnabled(False)

View File

@@ -25,9 +25,11 @@ import base64
import datetime
import ipaddress
import urllib.request
import urllib.parse
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
@@ -49,17 +51,13 @@ class HTTPClient(QtCore.QObject):
:param network_manager: A QT network manager
"""
# How many times we need to retry a connection
MAX_RETRY_CONNECTION = 5
# Callback class used for displaying progress
_progress_callback = None
connection_connected_signal = QtCore.Signal()
connection_disconnected_signal = QtCore.Signal()
def __init__(self, settings, network_manager=None):
def __init__(self, settings, network_manager=None, max_retry_connection=5):
super().__init__()
self._protocol = settings.get("protocol", "http")
@@ -74,8 +72,9 @@ class HTTPClient(QtCore.QObject):
self._port = int(settings["port"])
self._user = settings.get("user", None)
self._password = settings.get("password", None)
# How many time we have retry connection
# How many time we have already retried connection
self._retry = 0
self._max_retry_connection = max_retry_connection
self._connected = False
self._shutdown = False # Shutdown in progress
self._accept_insecure_certificate = settings.get("accept_insecure_certificate", None)
@@ -84,7 +83,6 @@ class HTTPClient(QtCore.QObject):
# query and disconnect if time is too long between two query
self._last_query_timestamp = None
self._max_time_difference_between_queries = None
if network_manager:
self._network_manager = network_manager
else:
@@ -95,6 +93,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
@@ -167,6 +167,25 @@ class HTTPClient(QtCore.QObject):
self.createHTTPQuery("POST", "/shutdown", None, showProgress=False)
self._shutdown = True
def getNetworkManager(self):
"""
:return: instance of NetworkManager
"""
return self._network_manager
def setMaxRetryConnection(self, retries):
"""
Sets how many times we need to retry a connection
:param retries: integer
"""
self._max_retry_connection = retries
def getMaxRetryConnection(self):
"""
Returns how many times we need to retry a connection
"""
return self._max_retry_connection
def _notify_progress_start_query(self, query_id, progress_text, response):
"""
Called when a query start
@@ -190,7 +209,7 @@ class HTTPClient(QtCore.QObject):
Called when a query upload progress
"""
if not sip_is_deleted(HTTPClient._progress_callback):
HTTPClient._progress_callback.progress_signal.emit(query_id, sent, total)
HTTPClient._progress_callback.progress_signal.emit(query_id, str(sent), str(total))
def _notify_progress_download(self, query_id, sent, total):
"""
@@ -199,7 +218,7 @@ class HTTPClient(QtCore.QObject):
if not sip_is_deleted(HTTPClient._progress_callback):
# abs() for maxium because sometimes the system send negative
# values
HTTPClient._progress_callback.progress_signal.emit(query_id, sent, abs(total))
HTTPClient._progress_callback.progress_signal.emit(query_id, str(sent), str(abs(total)))
@classmethod
def setProgressCallback(cls, progress_callback):
@@ -307,10 +326,10 @@ class HTTPClient(QtCore.QObject):
return request()
else:
self._query_waiting_connections.append((request, callback))
# If we are not connected and we enqueue the first query we open the conection
# enqueue the first query and open the connection if we are not connected
if len(self._query_waiting_connections) == 1:
log.info("Connection to {}".format(self.url()))
self._executeHTTPQuery("GET", "/version", self._callbackConnect, {}, server=server, timeout=5, showProgress=False)
log.debug("Connection to {}".format(self.url()))
self._executeHTTPQuery("GET", "/version", self._callbackConnect, {}, server=server, timeout=10, showProgress=False)
def _connectionError(self, callback, msg="", server=None):
"""
@@ -327,7 +346,7 @@ class HTTPClient(QtCore.QObject):
msg = "Cannot connect to {}. Please check if GNS3 is allowed in your antivirus and firewall. And that server version is {}.".format(self.url(), __version__)
for request, callback in self._query_waiting_connections:
if callback is not None:
callback({"message": msg}, error=True, server=server)
callback({"message": msg}, error=True, server=server, connection_error=True)
self._query_waiting_connections = []
def _retryConnection(self, server=None):
@@ -347,7 +366,7 @@ class HTTPClient(QtCore.QObject):
"""
if error is not False:
if self._retry < self.MAX_RETRY_CONNECTION:
if self._retry < self.getMaxRetryConnection():
self._retryConnection(server=server)
return
for request, callback in self._query_waiting_connections:
@@ -356,7 +375,7 @@ class HTTPClient(QtCore.QObject):
return
if "version" not in params or "local" not in params:
if self._retry < self.MAX_RETRY_CONNECTION:
if self._retry < self.getMaxRetryConnection():
self._retryConnection(server=server)
return
msg = "The remote server {} is not a GNS3 server".format(self.url())
@@ -441,6 +460,46 @@ 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 _paramsToQueryString(self, params):
"""
:param params: Dictionnary of query string parameters
:returns: String of the query string
"""
if params == {}:
query_string = ""
else:
query_string = "?"
params = params.copy()
for key, value in params.copy().items():
if value is None:
del params[key]
query_string += urllib.parse.urlencode(params)
return query_string
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,18 +520,8 @@ 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
if params == {}:
query_string = ""
else:
query_string = "?" + urllib.parse.urlencode(params)
host = self._getHostForQuery()
query_string = self._paramsToQueryString(params)
log.debug("{method} {protocol}://{host}:{port}{prefix}{path} {body}{query_string}".format(method=method, protocol=self._protocol, host=host, port=self._port, path=path, body=body, prefix=prefix, query_string=query_string))
if self._user:
@@ -491,7 +540,11 @@ class HTTPClient(QtCore.QObject):
if not networkManager:
networkManager = self._network_manager
response = networkManager.sendCustomRequest(request, method.encode(), body)
try:
response = networkManager.sendCustomRequest(request, method.encode(), body)
except SystemError as e:
log.error("Can't send query: {}".format(str(e)))
return
context = copy.copy(context)
context["query_id"] = str(uuid.uuid4())
@@ -514,7 +567,7 @@ class HTTPClient(QtCore.QObject):
self._notify_progress_start_query(context["query_id"], progressText, response)
if timeout is not None:
QtCore.QTimer.singleShot(timeout * 1000, qpartial(self._timeoutSlot, response))
QtCore.QTimer.singleShot(timeout * 1000, qpartial(self._timeoutSlot, response, timeout))
return response
@@ -549,14 +602,14 @@ class HTTPClient(QtCore.QObject):
else:
callback(content, server=server, context=context)
def _timeoutSlot(self, response):
def _timeoutSlot(self, response, timeout):
"""
Beware it's call for all request you need to check the status of the response
"""
# We check if we received HTTP headers
if not sip.isdeleted(response) and response.isRunning() and not len(response.rawHeaderList()) > 0:
if not response.error() != QtNetwork.QNetworkReply.NoError:
log.warn("Timeout request {}".format(response.url().toString()))
log.warning("Timeout after {} seconds for request {}".format(timeout, response.url().toString()))
response.abort()
def disconnect(self):
@@ -607,7 +660,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,9 +690,12 @@ class HTTPClient(QtCore.QObject):
except UnicodeDecodeError:
body = None
content_type = response.header(QtNetwork.QNetworkRequest.ContentTypeHeader)
log.debug(body)
if body and len(body.strip(" \n\t")) > 0 and content_type == "application/json":
params = json.loads(body)
try:
params = json.loads(body)
except ValueError: # Partial JSON
params = {}
status = 504
else:
params = {}
if callback is not None:
@@ -698,3 +754,23 @@ class HTTPClient(QtCore.QObject):
except (OSError, http.client.BadStatusLine, ValueError) as e:
log.debug("Error during get on {}:{}: {}".format(self.host(), self.port(), e))
return 0, None
@classmethod
def fromUrl(cls, url, network_manager=None, base_settings=None):
"""
Returns HttpClient instance based on the url
:param url: Url to parse
:param network_manager: Optional network_manager
:param base_settings: Source of the settings, if necessary
:return: HttpClient
"""
settings = {}
if base_settings is not None:
settings.update(**base_settings)
parse_results = urllib.parse.urlparse(url)
settings['protocol'] = parse_results.scheme
settings['host'] = parse_results.hostname
settings['port'] = parse_results.port
settings['user'] = parse_results.username
settings['password'] = parse_results.password
return cls(settings, network_manager)

View File

@@ -0,0 +1,91 @@
# -*- 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/>.
import pathlib
import urllib.parse
from gns3.http_client import HTTPClient
import logging
log = logging.getLogger(__name__)
class ImageUploadManager(object):
"""
Manager over the image upload. Encapsulates file uploads to computes or via controller.
"""
def __init__(self, image, controller, compute_id, callback=None, directFileUpload=False):
self._image = image
self._compute_id = compute_id
self._callback = callback
self._directFileUpload = directFileUpload
self._controller = controller
def upload(self):
if self._directFileUpload:
# first obtain endpoint and know when target request
self._controller.getEndpoint(
self._getComputePath(), self._compute_id, self._onLoadEndpointCallback, showProgress=False)
else:
self._fileUploadToController()
def _getComputePath(self):
return '/{emulator}/images/{filename}'.format(
emulator=self._image.emulator, filename=self._image.filename)
def _onLoadEndpointCallback(self, result, error=False, **kwargs):
if error:
if "message" in result:
log.error("Error while getting endpoint: {}".format(result["message"]))
return
# we know where is the endpoint and we trying to post there a file
endpoint = result['endpoint']
self._fileUploadToCompute(endpoint)
def _checkIfSuccessfulCallback(self, result, error=False, **kwargs):
if error:
connection_error = kwargs.get('connection_error', False)
if connection_error:
log.debug("During direct file upload compute is not visible. Fallback to upload via controller.")
# there was an issue with connection, probably we don't have a direct access to compute
# we need to fallback to uploading files via controller
self._fileUploadToController()
else:
if "message" in result:
log.error("Error while direct file upload: {}".format(result["message"]))
return
self._callback(result, error, **kwargs)
def _fileUploadToCompute(self, endpoint):
log.info("Uploading file to compute: {}".format(endpoint))
parse_results = urllib.parse.urlparse(endpoint)
network_manager = self._controller.getHttpClient().getNetworkManager()
client = HTTPClient.fromUrl(endpoint, network_manager=network_manager)
# We don't retry connection as in case of fail we try direct file upload
client.setMaxRetryConnection(0)
client.createHTTPQuery(
'POST', parse_results.path, self._checkIfSuccessfulCallback, body=pathlib.Path(self._image.path),
progressText="Uploading {}".format(self._image.filename), timeout=None, prefix="")
def _fileUploadToController(self):
log.info("Uploading file to controller: {}".format(self._getComputePath()))
self._controller.postCompute(
self._getComputePath(), self._compute_id, self._callback, body=pathlib.Path(self._image.path),
progressText="Uploading {}".format(self._image.filename), timeout=None)

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
@@ -78,7 +88,7 @@ class DrawingItem:
def updateDrawing(self):
if self._id:
self._project.put("/drawings/" + self._id, self.updateDrawingCallback, body=self.__json__())
self._project.put("/drawings/" + self._id, self.updateDrawingCallback, body=self.__json__(), showProgress=False)
@qslot
def updateDrawingCallback(self, result, error=False, **kwargs):
@@ -155,6 +165,7 @@ class DrawingItem:
"""
QtWidgets.QGraphicsItem.setZValue(self, value)
if self.zValue() < 0:
self.setFlag(self.ItemIsSelectable, False)
self.setFlag(self.ItemIsMovable, False)
@@ -215,3 +226,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

@@ -48,6 +48,13 @@ class EllipseItem(QtWidgets.QGraphicsEllipseItem, ShapeItem):
super().paint(painter, option, widget)
self.drawLayerInfo(painter)
def setZValue(self, value):
"""
Sets Z value of the item
:param value: z layer
"""
return ShapeItem.setZValue(self, value)
def toSvg(self):
"""
Return an SVG version of the shape

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()

View File

@@ -64,6 +64,13 @@ class ImageItem(QtSvg.QGraphicsSvgItem, DrawingItem):
super().paint(painter, option, widget)
self.drawLayerInfo(painter)
def setZValue(self, value):
"""
Sets Z value of the item
:param value: z layer
"""
return DrawingItem.setZValue(self, value)
def fromSvg(self, svg):
renderer = QImageSvgRenderer(svg)
self.setSharedRenderer(renderer)

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

@@ -0,0 +1,223 @@
# -*- 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 setZValue(self, value):
"""
Sets Z value of the item
:param value: z layer
"""
return DrawingItem.setZValue(self, value)
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

@@ -374,8 +374,8 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
if self._node_label.toPlainText() != label_data["text"]:
self._node_label.setPlainText(label_data["text"])
self._node_label.setStyle(label_data["style"])
self._node_label.setRotation(label_data["rotation"])
self._node_label.setStyle(label_data.get("style", ""))
self._node_label.setRotation(label_data.get("rotation", 0))
if label_data["x"] is None:
self._centerLabel()
self.updateNode()

View File

@@ -201,7 +201,7 @@ class NoteItem(QtWidgets.QGraphicsTextItem):
val = val.strip()
if key == "font-size":
font.setPointSize(int(val))
font.setPointSizeF(float(val))
elif key == "font-family":
font.setFamily(val)
elif key == "font-style" and val == "italic":
@@ -252,7 +252,7 @@ class NoteItem(QtWidgets.QGraphicsTextItem):
style = ""
style += "font-family: {};".format(self.font().family())
style += "font-size: {};".format(self.font().pointSize())
style += "font-size: {};".format(self.font().pointSizeF())
if self.font().italic():
style += "font-style: italic;"

View File

@@ -46,6 +46,13 @@ class RectangleItem(QtWidgets.QGraphicsRectItem, ShapeItem):
super().paint(painter, option, widget)
self.drawLayerInfo(painter)
def setZValue(self, value):
"""
Sets Z value of the item
:param value: z layer
"""
return ShapeItem.setZValue(self, value)
def toSvg(self):
"""
Return an SVG version of the shape
@@ -61,4 +68,3 @@ class RectangleItem(QtWidgets.QGraphicsRectItem, ShapeItem):
rect = self._styleSvg(rect)
return ET.tostring(svg, encoding="utf-8").decode("utf-8")

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

@@ -111,6 +111,13 @@ class TextItem(QtWidgets.QGraphicsTextItem, DrawingItem):
super().paint(painter, option, widget)
self.drawLayerInfo(painter)
def setZValue(self, value):
"""
Sets Z value of the item
:param value: z layer
"""
return DrawingItem.setZValue(self, value)
def toSvg(self):
"""
Return an SVG version of the text
@@ -121,7 +128,7 @@ class TextItem(QtWidgets.QGraphicsTextItem, DrawingItem):
text = ET.SubElement(svg, "text")
text.set("font-family", self.font().family())
text.set("font-size", str(self.font().pointSize()))
text.set("font-size", str(self.font().pointSizeF()))
if self.font().italic():
text.set("font-style", "italic")
if self.font().bold():
@@ -157,7 +164,7 @@ class TextItem(QtWidgets.QGraphicsTextItem, DrawingItem):
color.setAlphaF(float(opacity))
self.setDefaultTextColor(color)
font.setPointSize(int(text.get("font-size", self.font().pointSize())))
font.setPointSizeF(float(text.get("font-size", self.font().pointSizeF())))
font.setFamily(text.get("font-family", self.font().family()))
if text.get("font-style") == "italic":
font.setItalic(True)

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

@@ -150,7 +150,7 @@ class LocalConfig(QtCore.QObject):
def _getSettingsCallback(self, result, error=False, **kwargs):
self._refreshingSettings = False
if error:
log.error("Can't get settings from controller")
log.debug("Can't get settings from controller")
return
if result == {} and self._settings != {}:
self._settings_retrieved_from_controller = True
@@ -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)
@@ -455,6 +461,20 @@ class LocalConfig(QtCore.QObject):
settings["multi_profiles"] = value
self.saveSectionSettings("MainWindow", settings)
def directFileUpload(self):
"""
:returns: Boolean. True if direct_file_upload is enabled
"""
from gns3.settings import GENERAL_SETTINGS
return self.loadSectionSettings("MainWindow", GENERAL_SETTINGS)["direct_file_upload"]
def setDirectFileUpload(self, value):
from gns3.settings import GENERAL_SETTINGS
settings = self.loadSectionSettings("MainWindow", GENERAL_SETTINGS)
settings["direct_file_upload"] = value
self.saveSectionSettings("MainWindow", settings)
@staticmethod
def instance():
"""

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
@@ -129,7 +132,7 @@ class LocalServer(QtCore.QObject):
if win32serviceutil.QueryServiceStatus(service_name, None)[1] != win32service.SERVICE_RUNNING:
return False
except pywintypes.error as e:
if e.winerror == 1060:
if e.winerror == 1060: # service is not installed
return False
else:
log.error("Could not check if the {} service is running: {}".format(service_name, e.strerror))
@@ -142,7 +145,7 @@ class LocalServer(QtCore.QObject):
path = os.path.abspath(self._settings["ubridge_path"])
if not path or len(path) == 0 or not os.path.exists(path):
if not path or len(path) == 0 or not os.path.exists(path) or not os.path.isfile(path):
return False
if sys.platform.startswith("win"):
@@ -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):
@@ -517,7 +515,11 @@ class LocalServer(QtCore.QObject):
status, json_data = getSynchronous(self._settings["protocol"], self._settings["host"], self._port, "version",
timeout=2, user=self._settings["user"], password=self._settings["password"])
if json_data is None or status != 200:
if status == 401: # Auth issue that need to be solved later
return True
elif json_data is None:
return False
elif status != 200:
return False
else:
version = json_data.get("version", None)
@@ -535,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__)
@@ -73,8 +75,15 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
"""
super().__init__(parent)
self._settings = {}
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,8 +91,8 @@ 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))
self._first_file_load = True
@@ -114,6 +123,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
action.setIconText("All devices")
self.uiDocksMenu.addAction(action)
# Sometimes the parent seem invalid https://github.com/GNS3/gns3-gui/issues/2182
self.uiNodesDockWidget.setParent(self)
# make sure the dock widget is not open
self.uiNodesDockWidget.setVisible(False)
@@ -212,6 +223,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 +244,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 +312,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 +344,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 +357,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 +574,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.
@@ -549,10 +640,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
return
self._screenshots_dir = os.path.dirname(path)
# add the extension if missing
file_format = "." + selected_filter[:4].lower().strip()
if not path.endswith(file_format):
path += file_format
# add the extension if missing (Mac OS automatically adds an extension already)
if not sys.platform.startswith("darwin"):
file_format = "." + selected_filter[:4].lower().strip()
if not path.endswith(file_format):
path += file_format
if not self.createScreenshot(path):
QtWidgets.QMessageBox.critical(self, "Screenshot", "Could not create screenshot file {}".format(path))
@@ -605,6 +697,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 +706,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 +714,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
"""
self.uiGraphicsView.resetTransform()
self._updateZoomSettings()
def _fitInViewActionSlot(self):
"""
@@ -635,11 +730,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 +753,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 +837,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 +854,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 +949,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 +1024,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 +1110,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.
@@ -108,7 +94,9 @@ class Cloud(Node):
info = """Cloud device {name} is always-on
This is a node for external connections
""".format(name=self.name())
Device run on {host}
""".format(name=self.name(),
host=self.compute().name())
port_info = ""
for port in self._ports:

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.
@@ -101,7 +90,9 @@ class Nat(Node):
info = """Nat device {name} is always-on
This is a node for external connections
""".format(name=self.name())
Device run on {host}
""".format(name=self.name(),
host=self.compute().name())
port_info = ""
for port in self._ports:

View File

@@ -19,7 +19,7 @@
Configuration page for clouds.
"""
from gns3.qt import QtCore, QtWidgets
from gns3.qt import QtCore, QtGui, QtWidgets
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
from gns3.controller import Controller
from gns3.node import Node
@@ -68,6 +68,12 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
self.uiShowSpecialInterfacesCheckBox.stateChanged.connect(self._showSpecialInterfacesSlot)
self.uiSymbolToolButton.clicked.connect(self._symbolBrowserSlot)
# add an icon to warning button
icon = QtGui.QIcon.fromTheme("dialog-warning")
if icon.isNull():
icon = QtGui.QIcon(':/icons/dialog-warning.svg')
self.uiEthernetWarningPushButton.setIcon(icon)
def _EthernetChangedSlot(self):
"""
Enables the use of the delete button.

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

@@ -82,9 +82,6 @@
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="dialog-warning"/>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/cloud_configuration_page.ui'
#
# Created by: PyQt5 UI code generator 5.5.1
# Created by: PyQt5 UI code generator 5.9
#
# WARNING! All changes made in this file will be lost!
@@ -11,7 +11,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_cloudConfigPageWidget(object):
def setupUi(self, cloudConfigPageWidget):
cloudConfigPageWidget.setObjectName("cloudConfigPageWidget")
cloudConfigPageWidget.resize(758, 299)
cloudConfigPageWidget.resize(821, 363)
self.verticalLayout = QtWidgets.QVBoxLayout(cloudConfigPageWidget)
self.verticalLayout.setObjectName("verticalLayout")
self.uiTabWidget = QtWidgets.QTabWidget(cloudConfigPageWidget)
@@ -46,8 +46,6 @@ class Ui_cloudConfigPageWidget(object):
self.gridLayout_3.addWidget(self.uiEthernetListWidget, 1, 0, 1, 5)
self.uiEthernetWarningPushButton = QtWidgets.QPushButton(self.EthernetTab)
self.uiEthernetWarningPushButton.setText("")
icon = QtGui.QIcon.fromTheme("dialog-warning")
self.uiEthernetWarningPushButton.setIcon(icon)
self.uiEthernetWarningPushButton.setObjectName("uiEthernetWarningPushButton")
self.gridLayout_3.addWidget(self.uiEthernetWarningPushButton, 0, 1, 1, 1)
self.uiShowSpecialInterfacesCheckBox = QtWidgets.QCheckBox(self.EthernetTab)

View File

@@ -14,7 +14,7 @@
<string>Frame Relay Switch</string>
</property>
<property name="whatsThis">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This is a simple Frame Relay switch. Only serial links can be connected to it. &lt;span style=&quot; font-weight:600;&quot;&gt;Note that only the Frame-Relay LMI AINSI type is supported.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This is a simple Frame Relay switch. Only serial links can be connected to it. &lt;span style=&quot; font-weight:600;&quot;&gt;Note that only the Frame-Relay LMI ANSI type is supported.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0" colspan="2">

View File

@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/frame_relay_switch_configuration_page.ui'
# Form implementation generated from reading ui file '/home/dominik/projects/gns3-gui/gns3/modules/builtin/ui/frame_relay_switch_configuration_page.ui'
#
# Created by: PyQt5 UI code generator 5.5.1
# Created by: PyQt5 UI code generator 5.8.2
#
# WARNING! All changes made in this file will be lost!
@@ -134,7 +134,7 @@ class Ui_frameRelaySwitchConfigPageWidget(object):
def retranslateUi(self, frameRelaySwitchConfigPageWidget):
_translate = QtCore.QCoreApplication.translate
frameRelaySwitchConfigPageWidget.setWindowTitle(_translate("frameRelaySwitchConfigPageWidget", "Frame Relay Switch"))
frameRelaySwitchConfigPageWidget.setWhatsThis(_translate("frameRelaySwitchConfigPageWidget", "<html><head/><body><p>This is a simple Frame Relay switch. Only serial links can be connected to it. <span style=\" font-weight:600;\">Note that only the Frame-Relay LMI AINSI type is supported.</span></p></body></html>"))
frameRelaySwitchConfigPageWidget.setWhatsThis(_translate("frameRelaySwitchConfigPageWidget", "<html><head/><body><p>This is a simple Frame Relay switch. Only serial links can be connected to it. <span style=\" font-weight:600;\">Note that only the Frame-Relay LMI ANSI type is supported.</span></p></body></html>"))
self.uiGeneralGroupBox.setTitle(_translate("frameRelaySwitchConfigPageWidget", "General"))
self.uiNameLabel.setText(_translate("frameRelaySwitchConfigPageWidget", "Name:"))
self.uiFrameRelayMappingGroupBox.setTitle(_translate("frameRelaySwitchConfigPageWidget", "Mapping"))

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.
@@ -106,10 +88,15 @@ class DockerVM(Node):
info = """Docker container {name} is {state}
Node ID is {id}, server's Docker container ID is {node_id}
Docker VM's server run on {host}
Console is on port {console} and type is {console_type}
""".format(name=self.name(),
id=self.id(),
node_id=self._node_id,
state=state)
state=state,
host=self.compute().name(),
console=self._settings["console"],
console_type=self._settings["console_type"])
port_info = ""
for port in self._ports:

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"))
@@ -83,16 +86,17 @@ class iouDeviceConfigurationPage(QtWidgets.QWidget, Ui_iouDeviceConfigPageWidget
self.uiIOUImageLineEdit.clear()
self.uiIOUImageLineEdit.setText(path)
if "l2" in path:
# set the default L2 base startup-config
default_base_config = get_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)
if default_base_config:
self.uiStartupConfigLineEdit.setText(default_base_config)
if len(self.uiStartupConfigLineEdit.text().strip()) == 0:
if "l2" in path:
# set the default L2 base startup-config
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 = self._base_iou_l3_config_template
if default_base_config:
self.uiStartupConfigLineEdit.setText(default_base_config)
def _startupConfigBrowserSlot(self):
"""

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"])])
@@ -199,7 +199,8 @@ class QemuVMPreferencesPage(QtWidgets.QWidget, Ui_QemuVMPreferencesPageWidget):
dialog.show()
if dialog.exec_():
# update the icon
item.setIcon(0, QtGui.QIcon(qemu_vm["symbol"]))
Controller.instance().getSymbolIcon(qemu_vm["symbol"], qpartial(self._setItemIcon, item))
if qemu_vm["name"] != item.text(0):
new_key = "{server}:{name}".format(server=qemu_vm["server"], name=qemu_vm["name"])
if new_key in self._qemu_vms:

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

@@ -54,8 +54,6 @@ class VMwarePreferencesPage(QtWidgets.QWidget, Ui_VMwarePreferencesPageWidget):
else:
# VMnet limit on Linux is 255
self.uiVMnetEndRangeSpinBox.setMaximum(255)
# Block host network traffic is only supported on Windows for now
self.uiBlockHostTrafficCheckBox.setEnabled(False)
def _vmrunPathBrowserSlot(self):
"""

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

@@ -32,7 +32,7 @@ VMWARE_SETTINGS = {
"host_type": "ws",
"vmnet_start_range": 2,
"vmnet_end_range": DEFAULT_VMNET_END_RANGE,
"block_host_traffic": True,
"block_host_traffic": sys.platform.startswith("win"), # block host traffic on Windows only (due to winpcap packet duplication issue).
}
VMWARE_VM_SETTINGS = {
@@ -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
@@ -66,11 +66,11 @@ class Node(BaseNode):
def _exportFileCallback(self, result, error=False, raw_body=None, context={}, **kwargs):
if not error:
with open(context["path"], "wb+") as f:
f.write(raw_body)
def creator(self):
return self._creator
try:
with open(context["path"], "wb+") as f:
f.write(raw_body)
except OSError as e:
log.erro("Can't write %s: %s", context["path"], str(e))
def settings(self):
return self._settings
@@ -150,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):
@@ -220,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
@@ -288,7 +230,7 @@ class Node(BaseNode):
if self.initialized():
log.debug("{} is updating settings: {}".format(self.name(), params))
body = self._prepareBody(params)
self.controllerHttpPut("/nodes/{node_id}".format(node_id=self._node_id), self.updateNodeCallback, body=body, timeout=timeout)
self.controllerHttpPut("/nodes/{node_id}".format(node_id=self._node_id), self.updateNodeCallback, body=body, timeout=timeout, showProgress=False)
def updateNodeCallback(self, result, error=False, **kwargs):
"""
@@ -299,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
@@ -309,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
@@ -378,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.
@@ -392,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:
@@ -410,8 +402,7 @@ class Node(BaseNode):
if error:
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()))
# delete the node even if there is an error on server side
self.deleted_signal.emit()
self._module.removeNode(self)
@@ -478,7 +469,7 @@ class Node(BaseNode):
def suspend(self):
"""
Suspends this router.
Suspends this node.
"""
if self.status() == Node.suspended:
@@ -521,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:
@@ -581,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.

66
gns3/nodes_dock_widget.py Normal file
View File

@@ -0,0 +1,66 @@
#!/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):
# it's common race condition that uiNodesFilterComboBox that doesn't exist
# ref. https://github.com/GNS3/gns3-gui/issues/2304
if hasattr(self.window(), 'uiNodesFilterComboBox'):
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,21 @@ 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"])
if node["node_type"] == "dynamips":
node_class = module.getNodeType(node["node_type"], node["platform"])
else:
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

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